style: format seluruh workspace dengan cargo fmt
Menyeragamkan format kode sesuai rustfmt (126 file). Sebelumnya lefthook pre-commit 'cargo fmt --check' akan gagal pada commit apa pun.
This commit is contained in:
@@ -21,20 +21,13 @@ impl LoopbackServer {
|
||||
format!("http://127.0.0.1:{}/callback", self.port)
|
||||
}
|
||||
|
||||
pub fn wait_for_code(
|
||||
&self,
|
||||
timeout_ms: u64,
|
||||
expected_state: &str,
|
||||
) -> std::io::Result<String> {
|
||||
pub fn wait_for_code(&self, timeout_ms: u64, expected_state: &str) -> std::io::Result<String> {
|
||||
let (mut stream, _) = self.listener.accept()?;
|
||||
stream.set_read_timeout(Some(std::time::Duration::from_millis(timeout_ms)))?;
|
||||
Self::read_callback(&mut stream, expected_state)
|
||||
}
|
||||
|
||||
fn read_callback(
|
||||
stream: &mut TcpStream,
|
||||
expected_state: &str,
|
||||
) -> std::io::Result<String> {
|
||||
fn read_callback(stream: &mut TcpStream, expected_state: &str) -> std::io::Result<String> {
|
||||
let mut buf = [0u8; 4096];
|
||||
let n = stream.read(&mut buf)?;
|
||||
let request = String::from_utf8_lossy(&buf[..n]);
|
||||
@@ -68,7 +61,10 @@ impl LoopbackServer {
|
||||
));
|
||||
}
|
||||
code.ok_or_else(|| {
|
||||
std::io::Error::new(std::io::ErrorKind::InvalidData, "code not found in callback")
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"code not found in callback",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -59,17 +59,25 @@ pub struct AuditReport {
|
||||
impl AuditReport {
|
||||
/// True if any ERROR-level violations exist.
|
||||
pub fn has_errors(&self) -> bool {
|
||||
self.violations.iter().any(|v| v.severity == Severity::Error)
|
||||
self.violations
|
||||
.iter()
|
||||
.any(|v| v.severity == Severity::Error)
|
||||
}
|
||||
|
||||
/// Number of errors.
|
||||
pub fn error_count(&self) -> usize {
|
||||
self.violations.iter().filter(|v| v.severity == Severity::Error).count()
|
||||
self.violations
|
||||
.iter()
|
||||
.filter(|v| v.severity == Severity::Error)
|
||||
.count()
|
||||
}
|
||||
|
||||
/// Number of warnings.
|
||||
pub fn warning_count(&self) -> usize {
|
||||
self.violations.iter().filter(|v| v.severity == Severity::Warning).count()
|
||||
self.violations
|
||||
.iter()
|
||||
.filter(|v| v.severity == Severity::Warning)
|
||||
.count()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,11 +160,7 @@ fn forbidden_imports(layer: &str) -> &'static [&'static str] {
|
||||
}
|
||||
|
||||
/// Scan a single Rust source file for forbidden imports.
|
||||
fn scan_file(
|
||||
file_path: &Path,
|
||||
layer: &'static str,
|
||||
root: &Path,
|
||||
) -> Vec<Violation> {
|
||||
fn scan_file(file_path: &Path, layer: &'static str, root: &Path) -> Vec<Violation> {
|
||||
let mut violations = Vec::new();
|
||||
let content = match std::fs::read_to_string(file_path) {
|
||||
Ok(c) => c,
|
||||
@@ -185,10 +189,12 @@ fn scan_file(
|
||||
// `use crate::` in domain could reference domain-only items — skip.
|
||||
continue;
|
||||
}
|
||||
if trimmed.starts_with(&pattern) || trimmed.starts_with(&format!("use {forbidden}::")) {
|
||||
if trimmed.starts_with(&pattern)
|
||||
|| trimmed.starts_with(&format!("use {forbidden}::"))
|
||||
{
|
||||
// Skip test code — test modules commonly import outer layers.
|
||||
let is_test = content[..content.len().saturating_sub(1)]
|
||||
.contains("#[cfg(test)]");
|
||||
let is_test =
|
||||
content[..content.len().saturating_sub(1)].contains("#[cfg(test)]");
|
||||
if is_test {
|
||||
continue;
|
||||
}
|
||||
@@ -243,10 +249,7 @@ pub fn audit_layering(root: &Path) -> Result<AuditReport> {
|
||||
}
|
||||
|
||||
// Determine which crate this file belongs to by walking up.
|
||||
let layer = path
|
||||
.ancestors()
|
||||
.skip(1)
|
||||
.find_map(|p| classify_layer(p));
|
||||
let layer = path.ancestors().skip(1).find_map(|p| classify_layer(p));
|
||||
|
||||
if let Some(layer) = layer {
|
||||
files_scanned += 1;
|
||||
@@ -371,7 +374,10 @@ mod tests {
|
||||
fn function_metrics_short_function_ok() {
|
||||
let content = "fn ok() {\n let x = 1;\n}\n";
|
||||
let violations = check_function_metrics(content);
|
||||
let long: Vec<_> = violations.iter().filter(|v| v.message.contains("Function too long")).collect();
|
||||
let long: Vec<_> = violations
|
||||
.iter()
|
||||
.filter(|v| v.message.contains("Function too long"))
|
||||
.collect();
|
||||
assert!(long.is_empty(), "short function should not trigger");
|
||||
}
|
||||
|
||||
@@ -383,7 +389,10 @@ mod tests {
|
||||
}
|
||||
lines.push_str("}\n");
|
||||
let violations = check_function_metrics(&lines);
|
||||
let long: Vec<_> = violations.iter().filter(|v| v.message.contains("Function too long")).collect();
|
||||
let long: Vec<_> = violations
|
||||
.iter()
|
||||
.filter(|v| v.message.contains("Function too long"))
|
||||
.collect();
|
||||
assert!(!long.is_empty(), "long function should trigger warning");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,9 @@ pub struct CodeQualityReport {
|
||||
|
||||
impl CodeQualityReport {
|
||||
pub fn has_errors(&self) -> bool {
|
||||
self.findings.iter().any(|f| f.severity == super::arch_audit::Severity::Error)
|
||||
self.findings
|
||||
.iter()
|
||||
.any(|f| f.severity == super::arch_audit::Severity::Error)
|
||||
}
|
||||
pub fn count_by_rule(&self) -> Vec<(&'static str, usize)> {
|
||||
let mut counts: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
|
||||
@@ -147,7 +149,8 @@ pub fn scan_quality_file(file_path: &Path, root: &Path) -> Vec<Finding> {
|
||||
|
||||
// ── Rule: Missing doc comments on pub items ────────────────────
|
||||
if (trimmed.starts_with("pub ") || trimmed.starts_with("pub("))
|
||||
&& !prev_line_doc && !prev_line_empty
|
||||
&& !prev_line_doc
|
||||
&& !prev_line_empty
|
||||
{
|
||||
// Check it's a struct/enum/fn/trait/type/const/mod
|
||||
let is_item = trimmed.starts_with("pub fn ")
|
||||
@@ -246,7 +249,10 @@ mod tests {
|
||||
std::fs::write(&file, "fn x() { let y = foo.unwrap(); }\n").unwrap();
|
||||
|
||||
let findings = scan_quality_file(&file, &dir);
|
||||
let unwrap_findings: Vec<_> = findings.iter().filter(|f| f.rule == "unwrap-in-production").collect();
|
||||
let unwrap_findings: Vec<_> = findings
|
||||
.iter()
|
||||
.filter(|f| f.rule == "unwrap-in-production")
|
||||
.collect();
|
||||
assert!(!unwrap_findings.is_empty(), "should detect unwrap");
|
||||
}
|
||||
|
||||
@@ -262,8 +268,14 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let findings = scan_quality_file(&file, &dir);
|
||||
let unwrap_findings: Vec<_> = findings.iter().filter(|f| f.rule == "unwrap-in-production").collect();
|
||||
assert!(unwrap_findings.is_empty(), "should skip unwrap in test blocks");
|
||||
let unwrap_findings: Vec<_> = findings
|
||||
.iter()
|
||||
.filter(|f| f.rule == "unwrap-in-production")
|
||||
.collect();
|
||||
assert!(
|
||||
unwrap_findings.is_empty(),
|
||||
"should skip unwrap in test blocks"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -274,7 +286,13 @@ mod tests {
|
||||
std::fs::write(&file, "#[allow(clippy::too_many_arguments)]\nfn x() {}\n").unwrap();
|
||||
|
||||
let findings = scan_quality_file(&file, &dir);
|
||||
let bypass_findings: Vec<_> = findings.iter().filter(|f| f.rule == "compiler-bypass").collect();
|
||||
assert!(!bypass_findings.is_empty(), "should detect allow attributes");
|
||||
let bypass_findings: Vec<_> = findings
|
||||
.iter()
|
||||
.filter(|f| f.rule == "compiler-bypass")
|
||||
.collect();
|
||||
assert!(
|
||||
!bypass_findings.is_empty(),
|
||||
"should detect allow attributes"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,10 +50,9 @@ pub fn validate_commit_message(msg: &str) -> Result<(), Vec<String>> {
|
||||
}
|
||||
|
||||
// Parse: `type(scope): description` or `type!: description` or `type: description`
|
||||
let re = Regex::new(
|
||||
r"^(?P<type>[a-z]+)(?:\((?P<scope>[^)]+)\))?(?P<breaking>!)?:\s+(?P<desc>.+)$",
|
||||
)
|
||||
.expect("valid regex for commit parsing");
|
||||
let re =
|
||||
Regex::new(r"^(?P<type>[a-z]+)(?:\((?P<scope>[^)]+)\))?(?P<breaking>!)?:\s+(?P<desc>.+)$")
|
||||
.expect("valid regex for commit parsing");
|
||||
|
||||
match re.captures(subject) {
|
||||
None => {
|
||||
@@ -94,21 +93,19 @@ pub fn validate_commit_message(msg: &str) -> Result<(), Vec<String>> {
|
||||
} else {
|
||||
let first_char = desc.chars().next().unwrap_or(' ');
|
||||
if first_char.is_uppercase() {
|
||||
errors.push(format!(
|
||||
"Description must start with lowercase: '{desc}'"
|
||||
));
|
||||
errors.push(format!("Description must start with lowercase: '{desc}'"));
|
||||
}
|
||||
if desc.ends_with('.') {
|
||||
errors.push(format!(
|
||||
"Description must not end with a period: '{desc}'"
|
||||
));
|
||||
errors.push(format!("Description must not end with a period: '{desc}'"));
|
||||
}
|
||||
}
|
||||
|
||||
// Type-specific rules.
|
||||
match type_ {
|
||||
"chore" | "docs" | "refactor" | "test" | "style" | "perf" | "ci" | "build"
|
||||
| "revert" if scope.is_some() => {
|
||||
| "revert"
|
||||
if scope.is_some() =>
|
||||
{
|
||||
errors.push(format!(
|
||||
"'{type_}' commits should not use a scope. \
|
||||
Only 'feat' and 'fix' require scopes."
|
||||
@@ -140,10 +137,9 @@ pub fn parse_commit_message(msg: &str) -> Option<CommitInfo> {
|
||||
let msg = msg.trim();
|
||||
let subject = msg.lines().next()?;
|
||||
|
||||
let re = Regex::new(
|
||||
r"^(?P<type>[a-z]+)(?:\((?P<scope>[^)]+)\))?(?P<breaking>!)?:\s+(?P<desc>.+)$",
|
||||
)
|
||||
.expect("valid regex");
|
||||
let re =
|
||||
Regex::new(r"^(?P<type>[a-z]+)(?:\((?P<scope>[^)]+)\))?(?P<breaking>!)?:\s+(?P<desc>.+)$")
|
||||
.expect("valid regex");
|
||||
|
||||
let caps = re.captures(subject)?;
|
||||
|
||||
@@ -155,10 +151,18 @@ pub fn parse_commit_message(msg: &str) -> Option<CommitInfo> {
|
||||
};
|
||||
|
||||
Some(CommitInfo {
|
||||
type_: caps.name("type").map(|m| m.as_str()).unwrap_or("").to_string(),
|
||||
type_: caps
|
||||
.name("type")
|
||||
.map(|m| m.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string(),
|
||||
scope: caps.name("scope").map(|m| m.as_str().to_string()),
|
||||
breaking: caps.name("breaking").is_some(),
|
||||
description: caps.name("desc").map(|m| m.as_str()).unwrap_or("").to_string(),
|
||||
description: caps
|
||||
.name("desc")
|
||||
.map(|m| m.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string(),
|
||||
body,
|
||||
})
|
||||
}
|
||||
@@ -232,7 +236,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parses_valid_commit() {
|
||||
let parsed = parse_commit_message("feat(agent): add parallel execution\n\nWith cycle orchestration.").unwrap();
|
||||
let parsed = parse_commit_message(
|
||||
"feat(agent): add parallel execution\n\nWith cycle orchestration.",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(parsed.type_, "feat");
|
||||
assert_eq!(parsed.scope, Some("agent".to_string()));
|
||||
assert!(!parsed.breaking);
|
||||
|
||||
@@ -51,7 +51,6 @@ const EXPLORE_DIRECTIVES: [&str; 3] = [
|
||||
4. Identify main entry points (main.rs, main.py, index.ts, etc.).\n\
|
||||
5. Count files by extension type.\n\
|
||||
Use the ls_dir, read, grep, and glob tools. Be concise.",
|
||||
|
||||
// Agent 1: Symbol Index
|
||||
"You are a symbol index explorer.\n\
|
||||
1. Call the 'rebuild_index' tool to rebuild the symbol index.\n\
|
||||
@@ -59,7 +58,6 @@ const EXPLORE_DIRECTIVES: [&str; 3] = [
|
||||
3. Identify public APIs, entry points, and key types.\n\
|
||||
4. Group symbols by language and kind.\n\
|
||||
Be concise. Report what symbols exist and where they live.",
|
||||
|
||||
// Agent 2: Semantic Context
|
||||
"You are a semantic context explorer.\n\
|
||||
1. Call the 'rebuild_index' tool to ensure the index is fresh.\n\
|
||||
@@ -231,8 +229,8 @@ async fn run_explore_phase(
|
||||
let tc = tool_ctx.clone();
|
||||
|
||||
let handle = thread::spawn(move || {
|
||||
let rt = tokio::runtime::Runtime::new()
|
||||
.context("create explore subagent tokio runtime")?;
|
||||
let rt =
|
||||
tokio::runtime::Runtime::new().context("create explore subagent tokio runtime")?;
|
||||
rt.block_on(run_agent(ctx, &directive, AccessTier::Read, tc))
|
||||
});
|
||||
|
||||
@@ -252,12 +250,22 @@ async fn run_explore_phase(
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
warn!(agent = i, error = %e, "explore subagent failed");
|
||||
emit_failed(&turn_events_clone, EXPLORE_IDS[i], EXPLORE_LABELS[i], &e.to_string());
|
||||
emit_failed(
|
||||
&turn_events_clone,
|
||||
EXPLORE_IDS[i],
|
||||
EXPLORE_LABELS[i],
|
||||
&e.to_string(),
|
||||
);
|
||||
(i, format!("Error: {e}"), false)
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(agent = i, error = ?e, "explore subagent panicked");
|
||||
emit_failed(&turn_events_clone, EXPLORE_IDS[i], EXPLORE_LABELS[i], "thread panicked");
|
||||
emit_failed(
|
||||
&turn_events_clone,
|
||||
EXPLORE_IDS[i],
|
||||
EXPLORE_LABELS[i],
|
||||
"thread panicked",
|
||||
);
|
||||
(i, format!("Thread panic: {e:?}"), false)
|
||||
}
|
||||
};
|
||||
@@ -281,9 +289,7 @@ fn build_explore_context(results: &[(usize, String, bool)]) -> String {
|
||||
let success_count = results.iter().filter(|r| r.2).count();
|
||||
let total = results.len();
|
||||
|
||||
let mut msg = format!(
|
||||
"[Explore Phase — {success_count}/{total} agents succeeded]\n\n"
|
||||
);
|
||||
let mut msg = format!("[Explore Phase — {success_count}/{total} agents succeeded]\n\n");
|
||||
|
||||
for (i, output, success) in results {
|
||||
let label = EXPLORE_LABELS.get(*i).unwrap_or(&"❓ Unknown");
|
||||
|
||||
@@ -107,10 +107,7 @@ impl BestPracticeEngine {
|
||||
let layering = self.audit_layering(workspace_root)?;
|
||||
let quality = self.scan_quality(workspace_root)?;
|
||||
|
||||
Ok(CombinedAuditReport {
|
||||
layering,
|
||||
quality,
|
||||
})
|
||||
Ok(CombinedAuditReport { layering, quality })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -89,6 +89,8 @@ impl BashJob {
|
||||
let Ok(mut guard) = self.process.lock() else {
|
||||
return false;
|
||||
};
|
||||
guard.as_mut().is_some_and(|c| matches!(c.try_wait(), Ok(None)))
|
||||
guard
|
||||
.as_mut()
|
||||
.is_some_and(|c| matches!(c.try_wait(), Ok(None)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,24 +7,22 @@
|
||||
pub fn check_dangerous_pattern(tool_name: &str, args: &serde_json::Value) -> Option<String> {
|
||||
match tool_name {
|
||||
"bash" => {
|
||||
let cmd = args
|
||||
.get("command")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
let cmd = args.get("command").and_then(|v| v.as_str()).unwrap_or("");
|
||||
// Detect git push with --force
|
||||
if cmd.contains("git push") && cmd.contains("--force") {
|
||||
return Some("Force-pushing to git is destructive and may lose history".to_string());
|
||||
return Some(
|
||||
"Force-pushing to git is destructive and may lose history".to_string(),
|
||||
);
|
||||
}
|
||||
// Detect rm -rf /
|
||||
if cmd.contains("rm -rf /") || cmd.contains("rm -rf /*") {
|
||||
return Some("Recursive deletion of the root filesystem is never allowed".to_string());
|
||||
return Some(
|
||||
"Recursive deletion of the root filesystem is never allowed".to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
"delete" => {
|
||||
let path = args
|
||||
.get("path")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
let path = args.get("path").and_then(|v| v.as_str()).unwrap_or("");
|
||||
if path == "/" || path.starts_with("/etc") {
|
||||
return Some(format!("Deleting '{}' is too dangerous", path));
|
||||
}
|
||||
|
||||
@@ -76,10 +76,7 @@ pub struct StatePayload {
|
||||
pub enum DaemonFrame {
|
||||
StateUpdate(Box<StatePayload>),
|
||||
StreamToken(String),
|
||||
SystemNote {
|
||||
kind: String,
|
||||
message: String,
|
||||
},
|
||||
SystemNote { kind: String, message: String },
|
||||
ClipboardCopy(String),
|
||||
Closed,
|
||||
}
|
||||
|
||||
@@ -67,8 +67,7 @@ use std::sync::Arc;
|
||||
pub use tools::{GraduatedCheck, Tool, ToolCtx, ToolCtxBuilder};
|
||||
|
||||
// Re-export commonly needed types at the crate root
|
||||
pub use zesdex_domain::core::{ChatMessage, Role, Store, UsageStats, ToolCallResult};
|
||||
|
||||
pub use zesdex_domain::core::{ChatMessage, Role, Store, ToolCallResult, UsageStats};
|
||||
|
||||
/// A shared, async-writable cache of directory entries, used to avoid
|
||||
/// re-reading a directory every render frame.
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
use rand_core::RngCore;
|
||||
use std::time::Duration;
|
||||
|
||||
use zesdex_application::ports::ProviderService;
|
||||
use zesdex_domain::core::{
|
||||
ChatMessage, ChatRequest, ChatResponse, SseParser, StreamEvent, StreamOptions, ToolDef,
|
||||
};
|
||||
use zesdex_application::ports::ProviderService;
|
||||
|
||||
use zesdex_domain::agent::defaults::{DEFAULT_API_BASE, DEFAULT_MODEL};
|
||||
const DEFAULT_BASE_URL: &str = DEFAULT_API_BASE;
|
||||
@@ -82,10 +82,7 @@ impl LlmClient {
|
||||
retrying without connect timeout",
|
||||
e,
|
||||
);
|
||||
match reqwest::Client::builder()
|
||||
.timeout(REQUEST_TIMEOUT)
|
||||
.build()
|
||||
{
|
||||
match reqwest::Client::builder().timeout(REQUEST_TIMEOUT).build() {
|
||||
Ok(c) => c,
|
||||
Err(e2) => {
|
||||
tracing::warn!("also failed: {e2}. using default client");
|
||||
@@ -115,8 +112,7 @@ impl LlmClient {
|
||||
.post(url)
|
||||
.header("Content-Type", "application/json");
|
||||
if !self.api_key.is_empty() {
|
||||
http_req =
|
||||
http_req.header("Authorization", format!("Bearer {}", self.api_key));
|
||||
http_req = http_req.header("Authorization", format!("Bearer {}", self.api_key));
|
||||
}
|
||||
|
||||
let mut resp = http_req.json(req).send().await.map_err(|e| {
|
||||
@@ -182,7 +178,7 @@ impl LlmClient {
|
||||
}
|
||||
|
||||
let tc = &mut self.tool_calls[index];
|
||||
|
||||
|
||||
if let Some(ref id_val) = id {
|
||||
tc.id = id_val.clone();
|
||||
}
|
||||
@@ -237,8 +233,7 @@ impl LlmClient {
|
||||
n
|
||||
}
|
||||
};
|
||||
let text =
|
||||
String::from_utf8_lossy(&byte_buf[..valid_len]).into_owned();
|
||||
let text = String::from_utf8_lossy(&byte_buf[..valid_len]).into_owned();
|
||||
byte_buf.drain(..valid_len);
|
||||
|
||||
for event in parser.feed(&text) {
|
||||
@@ -306,8 +301,7 @@ impl ProviderService for LlmClient {
|
||||
.header("Content-Type", "application/json");
|
||||
|
||||
if !self.api_key.is_empty() {
|
||||
http_req =
|
||||
http_req.header("Authorization", format!("Bearer {}", self.api_key));
|
||||
http_req = http_req.header("Authorization", format!("Bearer {}", self.api_key));
|
||||
}
|
||||
|
||||
let result = async {
|
||||
@@ -335,9 +329,9 @@ impl ProviderService for LlmClient {
|
||||
}
|
||||
|
||||
let data: ChatResponse = resp.json().await?;
|
||||
let usage = data.usage.map(|u| {
|
||||
(u64::from(u.prompt_tokens), u64::from(u.completion_tokens))
|
||||
});
|
||||
let usage = data
|
||||
.usage
|
||||
.map(|u| (u64::from(u.prompt_tokens), u64::from(u.completion_tokens)));
|
||||
let message = data
|
||||
.choices
|
||||
.into_iter()
|
||||
@@ -345,7 +339,8 @@ impl ProviderService for LlmClient {
|
||||
.and_then(|c| c.message)
|
||||
.ok_or_else(|| anyhow::anyhow!("API response had no choices"))?;
|
||||
Ok((message, usage))
|
||||
}.await;
|
||||
}
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok((msg, usage)) => return Ok((msg, usage)),
|
||||
@@ -393,14 +388,16 @@ impl ProviderService for LlmClient {
|
||||
let mut captured_content = false;
|
||||
let mut wrapped = |event: &StreamEvent| -> bool {
|
||||
match event {
|
||||
StreamEvent::Token(_) | StreamEvent::Reasoning(_) | StreamEvent::ToolCallDelta { .. } => {
|
||||
StreamEvent::Token(_)
|
||||
| StreamEvent::Reasoning(_)
|
||||
| StreamEvent::ToolCallDelta { .. } => {
|
||||
captured_content = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
on_event(event)
|
||||
};
|
||||
|
||||
|
||||
match self.try_stream_once(&req, &url, &mut wrapped).await {
|
||||
Ok(result) => return Ok(result),
|
||||
Err(e) => {
|
||||
@@ -425,11 +422,7 @@ pub fn resolve_api_key(
|
||||
) -> String {
|
||||
let provider = &settings.provider;
|
||||
|
||||
let mut api_key = settings
|
||||
.api_keys
|
||||
.get(provider)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let mut api_key = settings.api_keys.get(provider).cloned().unwrap_or_default();
|
||||
|
||||
if api_key.is_empty() {
|
||||
if let Some(provider_cfg) = app_config.providers.get(provider) {
|
||||
|
||||
@@ -32,8 +32,16 @@ impl LspClient {
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
let stdin = child.stdin.take().ok_or_else(|| anyhow::anyhow!("no stdin on LSP process"))?;
|
||||
let stdout = BufReader::new(child.stdout.take().ok_or_else(|| anyhow::anyhow!("no stdout on LSP process"))?);
|
||||
let stdin = child
|
||||
.stdin
|
||||
.take()
|
||||
.ok_or_else(|| anyhow::anyhow!("no stdin on LSP process"))?;
|
||||
let stdout = BufReader::new(
|
||||
child
|
||||
.stdout
|
||||
.take()
|
||||
.ok_or_else(|| anyhow::anyhow!("no stdout on LSP process"))?,
|
||||
);
|
||||
|
||||
info!("LSP client spawned: {command}");
|
||||
Ok(LspClient {
|
||||
|
||||
@@ -28,7 +28,6 @@ impl Default for LspManager {
|
||||
}
|
||||
|
||||
impl LspManager {
|
||||
|
||||
pub fn start(&mut self, language: &str, command: &str, args: &[String]) -> anyhow::Result<()> {
|
||||
let client = LspClient::start(command, args)?;
|
||||
self.clients.insert(language.to_string(), client);
|
||||
|
||||
@@ -9,8 +9,14 @@ fn known_configs() -> HashMap<&'static str, (&'static str, Vec<&'static str>)> {
|
||||
let mut m = HashMap::new();
|
||||
m.insert("rust", ("rust-analyzer", vec![]));
|
||||
m.insert("python", ("pyright-langserver", vec!["--stdio"]));
|
||||
m.insert("typescript", ("typescript-language-server", vec!["--stdio"]));
|
||||
m.insert("javascript", ("typescript-language-server", vec!["--stdio"]));
|
||||
m.insert(
|
||||
"typescript",
|
||||
("typescript-language-server", vec!["--stdio"]),
|
||||
);
|
||||
m.insert(
|
||||
"javascript",
|
||||
("typescript-language-server", vec!["--stdio"]),
|
||||
);
|
||||
m.insert("go", ("gopls", vec![]));
|
||||
m
|
||||
}
|
||||
|
||||
@@ -14,7 +14,10 @@ pub fn install_language_server(language: &str) -> anyhow::Result<String> {
|
||||
if output.status.success() {
|
||||
Ok("rust-analyzer installed via rustup".to_string())
|
||||
} else {
|
||||
anyhow::bail!("failed to install rust-analyzer: {}", String::from_utf8_lossy(&output.stderr))
|
||||
anyhow::bail!(
|
||||
"failed to install rust-analyzer: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
)
|
||||
}
|
||||
}
|
||||
"python" => {
|
||||
@@ -24,7 +27,10 @@ pub fn install_language_server(language: &str) -> anyhow::Result<String> {
|
||||
if output.status.success() {
|
||||
Ok("pyright installed via npm".to_string())
|
||||
} else {
|
||||
anyhow::bail!("failed to install pyright: {}", String::from_utf8_lossy(&output.stderr))
|
||||
anyhow::bail!(
|
||||
"failed to install pyright: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
)
|
||||
}
|
||||
}
|
||||
lang => anyhow::bail!("no install method known for language '{lang}'"),
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
//! High-level manager that discovers, installs (if needed), and starts
|
||||
//! LSP servers.
|
||||
|
||||
use crate::lsp::manager::LspManager;
|
||||
use super::discovery::discover_installed;
|
||||
use super::install::install_language_server;
|
||||
use crate::lsp::manager::LspManager;
|
||||
|
||||
/// Auto-provision language servers for the given list of languages.
|
||||
///
|
||||
/// Flow: discover already-installed servers → for each requested language
|
||||
/// not yet available, attempt auto-install → start each server.
|
||||
pub fn auto_provision(
|
||||
lsp_manager: &mut LspManager,
|
||||
languages: &[String],
|
||||
) -> Vec<String> {
|
||||
pub fn auto_provision(lsp_manager: &mut LspManager, languages: &[String]) -> Vec<String> {
|
||||
let mut started = Vec::new();
|
||||
let installed = discover_installed();
|
||||
let mut installed_map: std::collections::HashMap<&str, &crate::lsp::provisioner::config::LspProvisionerConfig> = std::collections::HashMap::new();
|
||||
let mut installed_map: std::collections::HashMap<
|
||||
&str,
|
||||
&crate::lsp::provisioner::config::LspProvisionerConfig,
|
||||
> = std::collections::HashMap::new();
|
||||
for cfg in &installed {
|
||||
installed_map.insert(cfg.language.as_str(), cfg);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ impl Default for McpManager {
|
||||
}
|
||||
|
||||
impl McpManager {
|
||||
|
||||
/// Register an MCP server by name and transport string.
|
||||
///
|
||||
/// Returns an error if a server with the same name is already registered.
|
||||
|
||||
@@ -114,7 +114,11 @@ where
|
||||
}
|
||||
|
||||
Box::pin(async move {
|
||||
Ok((StatusCode::UNAUTHORIZED, "missing or invalid X-Session-Id header").into_response())
|
||||
Ok((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"missing or invalid X-Session-Id header",
|
||||
)
|
||||
.into_response())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
//! CORS layer factory for the daemon HTTP server.
|
||||
|
||||
use tower_http::cors::{AllowHeaders, AllowOrigin, CorsLayer};
|
||||
|
||||
/// Return a permissive CorsLayer for local daemon IPC.
|
||||
///
|
||||
/// All method and header names are static strings guaranteed to be valid
|
||||
/// HTTP tokens — `.parse()` is infallible here.
|
||||
pub fn default_cors_layer() -> CorsLayer {
|
||||
CorsLayer::new()
|
||||
.allow_origin(AllowOrigin::any())
|
||||
.allow_methods([
|
||||
"GET".parse().expect("static HTTP method"),
|
||||
"POST".parse().expect("static HTTP method"),
|
||||
"PUT".parse().expect("static HTTP method"),
|
||||
"DELETE".parse().expect("static HTTP method"),
|
||||
"PATCH".parse().expect("static HTTP method"),
|
||||
"OPTIONS".parse().expect("static HTTP method"),
|
||||
])
|
||||
.allow_headers(AllowHeaders::any())
|
||||
.expose_headers([
|
||||
"Content-Type".parse().expect("static HTTP header"),
|
||||
"X-Session-Id".parse().expect("static HTTP header"),
|
||||
"X-Request-Id".parse().expect("static HTTP header"),
|
||||
])
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
//! Axum middleware tower for the HTTP API layer.
|
||||
|
||||
pub mod auth;
|
||||
pub mod cors;
|
||||
pub mod rate_limit;
|
||||
|
||||
@@ -28,11 +28,14 @@ impl RateLimiter {
|
||||
.as_secs() as i64;
|
||||
|
||||
let cutoff = now.saturating_sub(window_secs as i64);
|
||||
let mut windows = self.windows.lock().map_err(|e| {
|
||||
anyhow::anyhow!("rate limiter lock poisoned: {e}")
|
||||
})?;
|
||||
let mut windows = self
|
||||
.windows
|
||||
.lock()
|
||||
.map_err(|e| anyhow::anyhow!("rate limiter lock poisoned: {e}"))?;
|
||||
|
||||
let timestamps = windows.entry(client_id.to_string()).or_insert_with(Vec::new);
|
||||
let timestamps = windows
|
||||
.entry(client_id.to_string())
|
||||
.or_insert_with(Vec::new);
|
||||
timestamps.retain(|&ts| ts >= cutoff);
|
||||
|
||||
if timestamps.len() >= max_requests as usize {
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
use std::path::Path;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zesdex_domain::cms::{AppConfig, AppConfigRepository, ModelRole, ProviderConfig, RepositoryError};
|
||||
use zesdex_domain::cms::{
|
||||
AppConfig, AppConfigRepository, ModelRole, ProviderConfig, RepositoryError,
|
||||
};
|
||||
|
||||
use crate::utils::write_json_atomic;
|
||||
|
||||
@@ -40,12 +42,15 @@ fn claude_settings_from_file() -> Option<ClaudeSettings> {
|
||||
|
||||
fn detect_claude_settings_provider() -> Option<(ProviderConfig, Option<String>)> {
|
||||
let settings = claude_settings_from_file();
|
||||
|
||||
|
||||
let file_creds = settings.as_ref().and_then(|s| {
|
||||
let env = s.env.as_ref()?;
|
||||
Some((env.anthropic_base_url.clone()?, env.anthropic_api_key.clone()?))
|
||||
Some((
|
||||
env.anthropic_base_url.clone()?,
|
||||
env.anthropic_api_key.clone()?,
|
||||
))
|
||||
});
|
||||
|
||||
|
||||
let env_creds = || -> Option<(String, String)> {
|
||||
let base_url = std::env::var("ANTHROPIC_BASE_URL").ok()?;
|
||||
let key = std::env::var("ANTHROPIC_API_KEY").ok()?;
|
||||
@@ -55,7 +60,7 @@ fn detect_claude_settings_provider() -> Option<(ProviderConfig, Option<String>)>
|
||||
let custom_model = settings.and_then(|s| s.custom_model);
|
||||
|
||||
let (base_url, key) = file_creds.or_else(env_creds)?;
|
||||
|
||||
|
||||
Some((
|
||||
ProviderConfig {
|
||||
api_base: base_url,
|
||||
@@ -63,7 +68,7 @@ fn detect_claude_settings_provider() -> Option<(ProviderConfig, Option<String>)>
|
||||
default_model: custom_model.clone(),
|
||||
default_api_key: Some(key),
|
||||
},
|
||||
custom_model
|
||||
custom_model,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -72,9 +77,7 @@ impl AppConfigRepository for JsonAppConfigRepository {
|
||||
let path = base_dir.join("app_config.json");
|
||||
let mut cfg: AppConfig = match std::fs::read_to_string(&path) {
|
||||
Ok(s) => serde_json::from_str(&s)?,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||
AppConfig::default()
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => AppConfig::default(),
|
||||
Err(e) => return Err(RepositoryError::Io(e)),
|
||||
};
|
||||
|
||||
@@ -106,15 +109,13 @@ impl AppConfigRepository for JsonAppConfigRepository {
|
||||
}
|
||||
|
||||
if let Some(custom) = &custom_model {
|
||||
cfg.model_roles
|
||||
.entry(custom.clone())
|
||||
.or_insert(ModelRole {
|
||||
provider: "claude".to_string(),
|
||||
model: custom.clone(),
|
||||
max_tokens: Some(8192),
|
||||
context_window: Some(200_000),
|
||||
temperature: Some(0.7),
|
||||
});
|
||||
cfg.model_roles.entry(custom.clone()).or_insert(ModelRole {
|
||||
provider: "claude".to_string(),
|
||||
model: custom.clone(),
|
||||
max_tokens: Some(8192),
|
||||
context_window: Some(200_000),
|
||||
temperature: Some(0.7),
|
||||
});
|
||||
}
|
||||
|
||||
if cfg.default_provider == defaults.default_provider {
|
||||
|
||||
@@ -77,10 +77,7 @@ impl MarkdownMemoryRepository {
|
||||
.lines()
|
||||
.filter_map(|l| {
|
||||
let mut it = l.splitn(2, ':');
|
||||
Some((
|
||||
it.next()?.trim().to_string(),
|
||||
it.next()?.trim().to_string(),
|
||||
))
|
||||
Some((it.next()?.trim().to_string(), it.next()?.trim().to_string()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -175,7 +172,9 @@ impl MemoryRepository for MarkdownMemoryRepository {
|
||||
|
||||
fn save(&self, memory_dir: &Path, memory: &Memory) -> Result<(), RepositoryError> {
|
||||
let path = Memory::path(memory_dir, &memory.name);
|
||||
let parent = path.parent().expect("memory path always has a parent directory");
|
||||
let parent = path
|
||||
.parent()
|
||||
.expect("memory path always has a parent directory");
|
||||
std::fs::create_dir_all(parent)?;
|
||||
|
||||
let frontmatter = Self::build_frontmatter(memory);
|
||||
|
||||
@@ -21,16 +21,16 @@ impl SettingsRepository for JsonSettingsRepository {
|
||||
fn load(&self, base_dir: &Path) -> Result<Settings, RepositoryError> {
|
||||
let path = base_dir.join("settings.json");
|
||||
match std::fs::read_to_string(&path) {
|
||||
Ok(s) => match serde_json::from_str(&s) {
|
||||
Ok(settings) => Ok(settings),
|
||||
Err(e) => {
|
||||
tracing::warn!("settings.json at '{:?}' failed to parse ({e}); falling back to defaults", path);
|
||||
Ok(Settings::default())
|
||||
Ok(s) => {
|
||||
match serde_json::from_str(&s) {
|
||||
Ok(settings) => Ok(settings),
|
||||
Err(e) => {
|
||||
tracing::warn!("settings.json at '{:?}' failed to parse ({e}); falling back to defaults", path);
|
||||
Ok(Settings::default())
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||
Ok(Settings::default())
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Settings::default()),
|
||||
Err(e) => Err(RepositoryError::Io(e)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,9 +50,7 @@ impl SessionLockRepository for FileSystemSessionLockRepository {
|
||||
.write(true)
|
||||
.open(&tmp)
|
||||
.map_err(|_| {
|
||||
RepositoryError::Other(
|
||||
"another process is replacing the lock".to_string(),
|
||||
)
|
||||
RepositoryError::Other("another process is replacing the lock".to_string())
|
||||
})?;
|
||||
write!(tmp_file, "{pid}")?;
|
||||
tmp_file.sync_all()?;
|
||||
|
||||
@@ -5,16 +5,12 @@ pub mod cms;
|
||||
pub mod iam;
|
||||
pub mod sqlite;
|
||||
|
||||
pub use cms::{
|
||||
app_config_repo::JsonAppConfigRepository, conversation_repo::JsonConversationRepository,
|
||||
edit_log_repo::JsonlEditLogRepository, memory_repo::MarkdownMemoryRepository,
|
||||
rewind_blob_repo::FileRewindBlobRepository, settings_repo::JsonSettingsRepository,
|
||||
};
|
||||
pub use iam::{
|
||||
oauth_repo::FileSystemOAuthRepository,
|
||||
session_lock_repo::FileSystemSessionLockRepository,
|
||||
oauth_repo::FileSystemOAuthRepository, session_lock_repo::FileSystemSessionLockRepository,
|
||||
session_repo::FileSystemSessionRepository,
|
||||
};
|
||||
pub use cms::{
|
||||
app_config_repo::JsonAppConfigRepository,
|
||||
conversation_repo::JsonConversationRepository,
|
||||
edit_log_repo::JsonlEditLogRepository,
|
||||
memory_repo::MarkdownMemoryRepository,
|
||||
rewind_blob_repo::FileRewindBlobRepository,
|
||||
settings_repo::JsonSettingsRepository,
|
||||
};
|
||||
|
||||
@@ -160,7 +160,8 @@ pub fn spawn_background_review(
|
||||
SEVERITY: HIGH|MEDIUM|LOW\n\
|
||||
OLD: <exact text to replace>\n\
|
||||
NEW: <replacement text>\n\
|
||||
---".to_string(),
|
||||
---"
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
let user_msg = ChatMessage::user(format!(
|
||||
@@ -210,7 +211,11 @@ pub fn spawn_background_review(
|
||||
&turn_events,
|
||||
TurnEvent::SystemNote {
|
||||
kind: "review_finding".into(),
|
||||
message: format!("📋 Auto-review complete ({} fix(es) applied).\n{}", fix_count, response_text.trim()),
|
||||
message: format!(
|
||||
"📋 Auto-review complete ({} fix(es) applied).\n{}",
|
||||
fix_count,
|
||||
response_text.trim()
|
||||
),
|
||||
},
|
||||
);
|
||||
info!(fix_count, "auto-review: completed with fixes");
|
||||
@@ -237,11 +242,7 @@ async fn run_llm_review(client: &LlmClient, messages: &[ChatMessage]) -> Result<
|
||||
}
|
||||
|
||||
/// Parse the LLM response for structured fix commands and apply them.
|
||||
fn apply_fixes_from_response(
|
||||
response: &str,
|
||||
tools: &[Box<dyn Tool>],
|
||||
tool_ctx: &ToolCtx,
|
||||
) -> usize {
|
||||
fn apply_fixes_from_response(response: &str, tools: &[Box<dyn Tool>], tool_ctx: &ToolCtx) -> usize {
|
||||
let mut fix_count = 0;
|
||||
|
||||
// Parse structured fix blocks
|
||||
|
||||
@@ -98,10 +98,7 @@ pub async fn run_agent(
|
||||
// If no tool calls, we're done — return content
|
||||
if tool_calls.is_empty() {
|
||||
info!("Subagent completed after {iteration} iterations");
|
||||
report_progress(
|
||||
&tool_ctx,
|
||||
AgentProgress::completed("subagent", directive),
|
||||
);
|
||||
report_progress(&tool_ctx, AgentProgress::completed("subagent", directive));
|
||||
return Ok(content);
|
||||
}
|
||||
|
||||
@@ -121,15 +118,14 @@ pub async fn run_agent(
|
||||
),
|
||||
);
|
||||
|
||||
let result =
|
||||
if let Some(tool) = tools.iter().find(|t| t.name() == tool_name) {
|
||||
match tool.run(&tool_ctx, &args) {
|
||||
Ok(output) => output,
|
||||
Err(e) => format!("Error: {e}"),
|
||||
}
|
||||
} else {
|
||||
format!("Unknown tool: {tool_name}")
|
||||
};
|
||||
let result = if let Some(tool) = tools.iter().find(|t| t.name() == tool_name) {
|
||||
match tool.run(&tool_ctx, &args) {
|
||||
Ok(output) => output,
|
||||
Err(e) => format!("Error: {e}"),
|
||||
}
|
||||
} else {
|
||||
format!("Unknown tool: {tool_name}")
|
||||
};
|
||||
|
||||
messages.push(ChatMessage::tool(tc.id.clone(), result));
|
||||
}
|
||||
@@ -149,5 +145,7 @@ pub async fn run_agent(
|
||||
format!("iteration limit ({MAX_ITERATIONS})"),
|
||||
),
|
||||
);
|
||||
Ok(format!("Subagent reached iteration limit ({MAX_ITERATIONS})"))
|
||||
Ok(format!(
|
||||
"Subagent reached iteration limit ({MAX_ITERATIONS})"
|
||||
))
|
||||
}
|
||||
|
||||
@@ -40,9 +40,7 @@ impl SubagentProvider {
|
||||
messages: &[ChatMessage],
|
||||
) -> Result<(ChatMessage, Option<(u64, u64)>)> {
|
||||
use zesdex_application::ports::ProviderService;
|
||||
self.client
|
||||
.chat(messages, None, Some(4096), None)
|
||||
.await
|
||||
self.client.chat(messages, None, Some(4096), None).await
|
||||
}
|
||||
|
||||
/// Send messages with available tool definitions.
|
||||
|
||||
@@ -131,7 +131,8 @@ impl Tool for BestPractice {
|
||||
}
|
||||
// Suggest a template.
|
||||
if let Some(parsed) = eng.parse_commit(&msg) {
|
||||
let tpl = eng.suggest_commit_template(&parsed.type_, parsed.scope.as_deref());
|
||||
let tpl =
|
||||
eng.suggest_commit_template(&parsed.type_, parsed.scope.as_deref());
|
||||
out.push_str(&format!("\nTemplate: {tpl}\n"));
|
||||
}
|
||||
Ok(out)
|
||||
@@ -309,8 +310,14 @@ mod tests {
|
||||
let tool = BestPractice;
|
||||
let args = json!({"action": "list_skills"});
|
||||
let result = tool.run(&test_ctx(), &args).unwrap();
|
||||
assert!(result.contains("clean-code"), "should list clean-code: {result}");
|
||||
assert!(result.contains("commit-convention"), "should list commit-convention: {result}");
|
||||
assert!(
|
||||
result.contains("clean-code"),
|
||||
"should list clean-code: {result}"
|
||||
);
|
||||
assert!(
|
||||
result.contains("commit-convention"),
|
||||
"should list commit-convention: {result}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -318,7 +325,10 @@ mod tests {
|
||||
let tool = BestPractice;
|
||||
let args = json!({"action": "get_skill", "skill_name": "clean-code"});
|
||||
let result = tool.run(&test_ctx(), &args).unwrap();
|
||||
assert!(result.contains("Clean Code"), "should contain skill content: {result}");
|
||||
assert!(
|
||||
result.contains("Clean Code"),
|
||||
"should contain skill content: {result}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -326,7 +336,10 @@ mod tests {
|
||||
let tool = CommitConvention;
|
||||
let args = json!({"message": "feat(tool): add best practice audit"});
|
||||
let result = tool.run(&test_ctx(), &args).unwrap();
|
||||
assert!(result.contains("✅"), "valid commit should succeed: {result}");
|
||||
assert!(
|
||||
result.contains("✅"),
|
||||
"valid commit should succeed: {result}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -334,6 +347,9 @@ mod tests {
|
||||
let tool = CommitConvention;
|
||||
let args = json!({"message": "Add new feature"});
|
||||
let result = tool.run(&test_ctx(), &args).unwrap();
|
||||
assert!(result.contains("❌"), "invalid commit should fail: {result}");
|
||||
assert!(
|
||||
result.contains("❌"),
|
||||
"invalid commit should fail: {result}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,7 @@ pub struct ToolCtx {
|
||||
pub origin: crate::Origin,
|
||||
pub graduated_checks: Vec<crate::tools::GraduatedCheck>,
|
||||
pub lsp_manager: Arc<Mutex<crate::lsp::manager::LspManager>>,
|
||||
pub turn_events:
|
||||
Option<Arc<Mutex<std::collections::VecDeque<crate::TurnEvent>>>>,
|
||||
pub turn_events: Option<Arc<Mutex<std::collections::VecDeque<crate::TurnEvent>>>>,
|
||||
pub workflow_findings: Option<Arc<Mutex<Vec<String>>>>,
|
||||
pub abort_flag: Option<Arc<AtomicBool>>,
|
||||
}
|
||||
@@ -41,8 +40,7 @@ pub struct ToolCtxBuilder {
|
||||
pub origin: crate::Origin,
|
||||
pub graduated_checks: Vec<crate::tools::GraduatedCheck>,
|
||||
pub lsp_manager: Arc<Mutex<crate::lsp::manager::LspManager>>,
|
||||
pub turn_events:
|
||||
Option<Arc<Mutex<std::collections::VecDeque<crate::TurnEvent>>>>,
|
||||
pub turn_events: Option<Arc<Mutex<std::collections::VecDeque<crate::TurnEvent>>>>,
|
||||
pub workflow_findings: Option<Arc<Mutex<Vec<String>>>>,
|
||||
pub abort_flag: Option<Arc<AtomicBool>>,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::future::Future;
|
||||
use anyhow::Result;
|
||||
use zesdex_application::agent::ToolExecutor;
|
||||
use crate::tools::{all_tools, Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use std::future::Future;
|
||||
use zesdex_application::agent::ToolExecutor;
|
||||
|
||||
pub struct InfrastructureToolExecutor {
|
||||
ctx: ToolCtx,
|
||||
@@ -27,14 +27,10 @@ impl ToolExecutor for InfrastructureToolExecutor {
|
||||
let tool_opt = self.tools.iter().find(|t| t.name() == tool_name);
|
||||
let ctx = self.ctx.clone();
|
||||
let args = args.clone();
|
||||
|
||||
|
||||
async move {
|
||||
match tool_opt {
|
||||
Some(tool) => {
|
||||
tokio::task::block_in_place(move || {
|
||||
tool.run(&ctx, &args)
|
||||
})
|
||||
}
|
||||
Some(tool) => tokio::task::block_in_place(move || tool.run(&ctx, &args)),
|
||||
None => {
|
||||
anyhow::bail!("Unknown tool: {}", tool_name)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::tools::shell_filter::git::check_git_destructive;
|
||||
use crate::tools::{execute_cmd, Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, warn, instrument};
|
||||
use tracing::{info, instrument, warn};
|
||||
|
||||
/// Tool that executes safe git operations (status, log, diff, commit, branch, etc.).
|
||||
///
|
||||
|
||||
@@ -56,15 +56,13 @@ impl Tool for GitWorktree {
|
||||
let branch = crate::tools::arg_str(args, "branch")?;
|
||||
info!(path, branch, "adding worktree");
|
||||
let output = execute_cmd(
|
||||
std::process::Command::new("git")
|
||||
.args(["worktree", "add", &path, &branch]),
|
||||
std::process::Command::new("git").args(["worktree", "add", &path, &branch]),
|
||||
)?;
|
||||
Ok(output)
|
||||
}
|
||||
"list" => {
|
||||
let output = execute_cmd(
|
||||
std::process::Command::new("git").args(["worktree", "list"]),
|
||||
)?;
|
||||
let output =
|
||||
execute_cmd(std::process::Command::new("git").args(["worktree", "list"]))?;
|
||||
Ok(output)
|
||||
}
|
||||
"remove" => {
|
||||
@@ -77,9 +75,8 @@ impl Tool for GitWorktree {
|
||||
}
|
||||
"prune" => {
|
||||
info!("pruning stale worktree metadata");
|
||||
let output = execute_cmd(
|
||||
std::process::Command::new("git").args(["worktree", "prune"]),
|
||||
)?;
|
||||
let output =
|
||||
execute_cmd(std::process::Command::new("git").args(["worktree", "prune"]))?;
|
||||
Ok(output)
|
||||
}
|
||||
_ => anyhow::bail!("unknown action: {}", action),
|
||||
|
||||
@@ -11,11 +11,7 @@ pub struct GraduatedCheck {
|
||||
}
|
||||
|
||||
/// Check which graduated checks apply to a given file path/content pair.
|
||||
pub fn check_graduated_checks(
|
||||
path: &str,
|
||||
content: &str,
|
||||
checks: &[GraduatedCheck],
|
||||
) -> Vec<String> {
|
||||
pub fn check_graduated_checks(path: &str, content: &str, checks: &[GraduatedCheck]) -> Vec<String> {
|
||||
let mut matches = Vec::new();
|
||||
for check in checks {
|
||||
if path.contains(&check.pattern) || content.contains(&check.rule) {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, error, instrument};
|
||||
use tracing::{error, info, instrument};
|
||||
|
||||
/// Tool that requests code completion suggestions from an LSP server.
|
||||
///
|
||||
@@ -65,10 +65,13 @@ impl Tool for LspCompletion {
|
||||
}
|
||||
};
|
||||
if let Some(client) = manager.get_client(&language) {
|
||||
let result = client.send_request("textDocument/completion", &json!({
|
||||
"textDocument": { "uri": format!("file://{}", path) },
|
||||
"position": { "line": line, "character": character }
|
||||
}))?;
|
||||
let result = client.send_request(
|
||||
"textDocument/completion",
|
||||
&json!({
|
||||
"textDocument": { "uri": format!("file://{}", path) },
|
||||
"position": { "line": line, "character": character }
|
||||
}),
|
||||
)?;
|
||||
Ok(serde_json::to_string_pretty(&result)?)
|
||||
} else {
|
||||
anyhow::bail!("no LSP client connected for '{language}'")
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, error, instrument};
|
||||
use tracing::{error, info, instrument};
|
||||
|
||||
/// Tool that connects to an LSP language server for a given language.
|
||||
///
|
||||
@@ -52,7 +52,11 @@ impl Tool for LspConnect {
|
||||
let extra_args: Vec<String> = args
|
||||
.get("args")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str().map(String::from))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
info!(language, command, extra_args = ?extra_args, "LSP connect requested");
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, error, instrument};
|
||||
use tracing::{error, info, instrument};
|
||||
|
||||
/// Tool that resolves a symbol's definition location via LSP.
|
||||
///
|
||||
@@ -66,10 +66,13 @@ impl Tool for LspDefinition {
|
||||
}
|
||||
};
|
||||
if let Some(client) = manager.get_client(&language) {
|
||||
let result = client.send_request("textDocument/definition", &json!({
|
||||
"textDocument": { "uri": format!("file://{}", path) },
|
||||
"position": { "line": line, "character": character }
|
||||
}))?;
|
||||
let result = client.send_request(
|
||||
"textDocument/definition",
|
||||
&json!({
|
||||
"textDocument": { "uri": format!("file://{}", path) },
|
||||
"position": { "line": line, "character": character }
|
||||
}),
|
||||
)?;
|
||||
Ok(serde_json::to_string_pretty(&result)?)
|
||||
} else {
|
||||
anyhow::bail!("no LSP client connected for '{language}'")
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, error, instrument};
|
||||
use tracing::{error, info, instrument};
|
||||
|
||||
/// Tool that retrieves diagnostics (errors, warnings) from the LSP for a file.
|
||||
///
|
||||
@@ -56,9 +56,12 @@ impl Tool for LspDiagnostics {
|
||||
}
|
||||
};
|
||||
if let Some(client) = manager.get_client(&language) {
|
||||
let result = client.send_request("textDocument/diagnostic", &json!({
|
||||
"textDocument": { "uri": format!("file://{}", path) }
|
||||
}))?;
|
||||
let result = client.send_request(
|
||||
"textDocument/diagnostic",
|
||||
&json!({
|
||||
"textDocument": { "uri": format!("file://{}", path) }
|
||||
}),
|
||||
)?;
|
||||
Ok(serde_json::to_string_pretty(&result)?)
|
||||
} else {
|
||||
anyhow::bail!("no LSP client connected for '{language}'")
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, error, instrument};
|
||||
use tracing::{error, info, instrument};
|
||||
|
||||
/// Tool that disconnects an LSP language server for a given language.
|
||||
///
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, error, instrument};
|
||||
use tracing::{error, info, instrument};
|
||||
|
||||
/// Tool that retrieves hover information for a symbol at a position via LSP.
|
||||
///
|
||||
@@ -66,10 +66,13 @@ impl Tool for LspHover {
|
||||
}
|
||||
};
|
||||
if let Some(client) = manager.get_client(&language) {
|
||||
let result = client.send_request("textDocument/hover", &json!({
|
||||
"textDocument": { "uri": format!("file://{}", path) },
|
||||
"position": { "line": line, "character": character }
|
||||
}))?;
|
||||
let result = client.send_request(
|
||||
"textDocument/hover",
|
||||
&json!({
|
||||
"textDocument": { "uri": format!("file://{}", path) },
|
||||
"position": { "line": line, "character": character }
|
||||
}),
|
||||
)?;
|
||||
Ok(serde_json::to_string_pretty(&result)?)
|
||||
} else {
|
||||
anyhow::bail!("no LSP client connected for '{language}'")
|
||||
|
||||
@@ -9,10 +9,10 @@ pub mod disconnect;
|
||||
pub mod hover;
|
||||
pub mod references;
|
||||
|
||||
pub use connect::LspConnect;
|
||||
pub use diagnostics::LspDiagnostics;
|
||||
pub use hover::LspHover;
|
||||
pub use completion::LspCompletion;
|
||||
pub use connect::LspConnect;
|
||||
pub use definition::LspDefinition;
|
||||
pub use references::LspReferences;
|
||||
pub use diagnostics::LspDiagnostics;
|
||||
pub use disconnect::LspDisconnect;
|
||||
pub use hover::LspHover;
|
||||
pub use references::LspReferences;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, error, instrument};
|
||||
use tracing::{error, info, instrument};
|
||||
|
||||
/// Tool that finds all references to a symbol at a position via LSP.
|
||||
///
|
||||
@@ -66,10 +66,13 @@ impl Tool for LspReferences {
|
||||
}
|
||||
};
|
||||
if let Some(client) = manager.get_client(&language) {
|
||||
let result = client.send_request("textDocument/references", &json!({
|
||||
"textDocument": { "uri": format!("file://{}", path) },
|
||||
"position": { "line": line, "character": character }
|
||||
}))?;
|
||||
let result = client.send_request(
|
||||
"textDocument/references",
|
||||
&json!({
|
||||
"textDocument": { "uri": format!("file://{}", path) },
|
||||
"position": { "line": line, "character": character }
|
||||
}),
|
||||
)?;
|
||||
Ok(serde_json::to_string_pretty(&result)?)
|
||||
} else {
|
||||
anyhow::bail!("no LSP client connected for '{language}'")
|
||||
|
||||
@@ -52,8 +52,8 @@ pub mod sequential_think;
|
||||
pub mod shell;
|
||||
pub mod shell_filter;
|
||||
pub mod spawn;
|
||||
pub mod utility;
|
||||
pub mod util;
|
||||
pub mod utility;
|
||||
pub mod web_search;
|
||||
pub mod workflow;
|
||||
|
||||
|
||||
@@ -112,10 +112,7 @@ impl Tool for ParallelDelegate {
|
||||
dirs.iter()
|
||||
.filter_map(|d| {
|
||||
let directive = d.get("directive").and_then(|v| v.as_str())?;
|
||||
let access_str = d
|
||||
.get("access")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("write");
|
||||
let access_str = d.get("access").and_then(|v| v.as_str()).unwrap_or("write");
|
||||
let access = match access_str {
|
||||
"read" => AccessTier::Read,
|
||||
"full" => AccessTier::Full,
|
||||
@@ -162,12 +159,7 @@ impl Tool for ParallelDelegate {
|
||||
);
|
||||
|
||||
debug!(agent_index = i, access = ?access, "spawning parallel agent");
|
||||
let handle = spawn_subagent(
|
||||
subagent_ctx,
|
||||
directive.clone(),
|
||||
*access,
|
||||
ctx.clone(),
|
||||
);
|
||||
let handle = spawn_subagent(subagent_ctx, directive.clone(), *access, ctx.clone());
|
||||
handles.push((i, handle));
|
||||
}
|
||||
|
||||
@@ -181,11 +173,7 @@ impl Tool for ParallelDelegate {
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
warn!(agent_index = i, error = %e, "parallel agent failed");
|
||||
results.push((
|
||||
i,
|
||||
directives[i].0.clone(),
|
||||
format!("[ERROR] {e}"),
|
||||
));
|
||||
results.push((i, directives[i].0.clone(), format!("[ERROR] {e}")));
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(agent_index = i, error = ?e, "parallel agent panicked");
|
||||
@@ -201,7 +189,8 @@ impl Tool for ParallelDelegate {
|
||||
// Consolidate results
|
||||
if synthesize && results.len() > 1 {
|
||||
let rt = tokio::runtime::Runtime::new()?;
|
||||
let consolidated = rt.block_on(consolidate_results(&results, &base_url, &api_key, &model))?;
|
||||
let consolidated =
|
||||
rt.block_on(consolidate_results(&results, &base_url, &api_key, &model))?;
|
||||
Ok(format!(
|
||||
"## Parallel Delegation Complete\n\n**Task:** {task}\n**Parallel agents:** {}\n\n{}",
|
||||
results.len(),
|
||||
@@ -213,7 +202,10 @@ impl Tool for ParallelDelegate {
|
||||
results.len()
|
||||
);
|
||||
for (i, directive, result) in &results {
|
||||
output.push_str(&format!("---\n### Agent {}: {}\n\n{}\n", i, directive, result));
|
||||
output.push_str(&format!(
|
||||
"---\n### Agent {}: {}\n\n{}\n",
|
||||
i, directive, result
|
||||
));
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
@@ -234,9 +226,8 @@ async fn auto_split_task(
|
||||
Some(base_url.to_string()),
|
||||
);
|
||||
|
||||
let sys_msg = zesdex_domain::core::ChatMessage::system(
|
||||
format!(
|
||||
"You are a task decomposition expert. Split the following task into {max_parallel} \
|
||||
let sys_msg = zesdex_domain::core::ChatMessage::system(format!(
|
||||
"You are a task decomposition expert. Split the following task into {max_parallel} \
|
||||
independent sub-tasks that can run in parallel. Each sub-task must be self-contained \
|
||||
and produce useful output independently.\n\n\
|
||||
Output your response as a JSON array of objects, each with:\n\
|
||||
@@ -244,15 +235,16 @@ async fn auto_split_task(
|
||||
- \"access\": one of \"read\", \"write\", or \"full\"\n\n\
|
||||
IMPORTANT: Return ONLY valid JSON, no other text. Example:\n\
|
||||
[{{\"directive\": \"Create the User model with fields...\", \"access\": \"write\"}}]"
|
||||
)
|
||||
);
|
||||
));
|
||||
|
||||
let user_msg =
|
||||
zesdex_domain::core::ChatMessage::user(format!("Task: {task}\n\nSplit into {max_parallel} parallel directives:"));
|
||||
let user_msg = zesdex_domain::core::ChatMessage::user(format!(
|
||||
"Task: {task}\n\nSplit into {max_parallel} parallel directives:"
|
||||
));
|
||||
|
||||
use zesdex_application::ports::ProviderService;
|
||||
match client
|
||||
.chat(&[sys_msg, user_msg], None, Some(2048), Some(0.4)).await
|
||||
.chat(&[sys_msg, user_msg], None, Some(2048), Some(0.4))
|
||||
.await
|
||||
{
|
||||
Ok((response, _)) => {
|
||||
let text = response.content.unwrap_or_default();
|
||||
@@ -320,10 +312,7 @@ fn fallback_split(task: &str, max_parallel: usize) -> Vec<(String, AccessTier)>
|
||||
}
|
||||
|
||||
if task.contains("test") || task.contains("unit") {
|
||||
directives.push((
|
||||
format!("Write unit tests for: {task}"),
|
||||
AccessTier::Read,
|
||||
));
|
||||
directives.push((format!("Write unit tests for: {task}"), AccessTier::Read));
|
||||
}
|
||||
|
||||
if directives.is_empty() {
|
||||
@@ -370,7 +359,10 @@ async fn consolidate_results(
|
||||
));
|
||||
|
||||
use zesdex_application::ports::ProviderService;
|
||||
match client.chat(&[sys_msg, user_msg], None, Some(2048), Some(0.3)).await {
|
||||
match client
|
||||
.chat(&[sys_msg, user_msg], None, Some(2048), Some(0.3))
|
||||
.await
|
||||
{
|
||||
Ok((response, _)) => Ok(response.content.unwrap_or_else(|| summary.clone())),
|
||||
Err(e) => {
|
||||
warn!(error = %e, "consolidation LLM call failed, using raw concatenation");
|
||||
|
||||
@@ -158,11 +158,7 @@ impl Tool for Glob {
|
||||
let p = entry.path();
|
||||
if glob_set.is_match(p) {
|
||||
let rel_path = p.strip_prefix(&root).unwrap_or(p).display().to_string();
|
||||
matches.push(format!(
|
||||
"{}{}",
|
||||
rel_path,
|
||||
if p.is_dir() { "/" } else { "" }
|
||||
));
|
||||
matches.push(format!("{}{}", rel_path, if p.is_dir() { "/" } else { "" }));
|
||||
}
|
||||
}
|
||||
matches.sort();
|
||||
|
||||
@@ -304,10 +304,7 @@ impl SymbolIndex {
|
||||
continue;
|
||||
}
|
||||
|
||||
let ext = file_path
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("");
|
||||
let ext = file_path.extension().and_then(|e| e.to_str()).unwrap_or("");
|
||||
let extractor = match ext_dispatch.get(ext) {
|
||||
Some(f) => f,
|
||||
_ => continue, // unsupported extension
|
||||
@@ -704,8 +701,8 @@ fn extract_typescript(content: &str, rel_path: &str) -> Vec<CodeSymbol> {
|
||||
if let Some(caps) = r.ts_var_export.captures(trimmed) {
|
||||
let name = caps.get(1).unwrap().as_str().to_string();
|
||||
// Only capture top-level (indentation 0) or exported
|
||||
let is_top_level = line.starts_with(|c: char| !c.is_whitespace())
|
||||
|| trimmed.starts_with("export");
|
||||
let is_top_level =
|
||||
line.starts_with(|c: char| !c.is_whitespace()) || trimmed.starts_with("export");
|
||||
if is_top_level {
|
||||
let kind = if trimmed.contains("const ") {
|
||||
SymbolKind::Constant
|
||||
@@ -963,10 +960,7 @@ fn extract_python(content: &str, rel_path: &str) -> Vec<CodeSymbol> {
|
||||
&& !trimmed.contains("==");
|
||||
if is_top_level {
|
||||
let name = trimmed.split('=').next().unwrap_or("").trim().to_string();
|
||||
if !name.is_empty()
|
||||
&& !name.starts_with('_')
|
||||
&& !name.contains(' ')
|
||||
{
|
||||
if !name.is_empty() && !name.starts_with('_') && !name.contains(' ') {
|
||||
let kind = if name.chars().all(|c| c.is_uppercase() || c == '_') {
|
||||
SymbolKind::Constant
|
||||
} else {
|
||||
@@ -1157,10 +1151,7 @@ pub fn format_symbol_listing(index: &SymbolIndex) -> String {
|
||||
std::collections::BTreeMap::new();
|
||||
for sym in syms {
|
||||
let kind_str = sym.kind.to_string();
|
||||
by_kind
|
||||
.entry(kind_str)
|
||||
.or_default()
|
||||
.push(sym.name.as_str());
|
||||
by_kind.entry(kind_str).or_default().push(sym.name.as_str());
|
||||
}
|
||||
for (kind, names) in &by_kind {
|
||||
out.push_str(&format!(" {kind}: {}\n", names.join(", ")));
|
||||
@@ -1312,7 +1303,11 @@ impl Tool for SemanticSearch {
|
||||
}
|
||||
|
||||
let total = index.len();
|
||||
info!(matched = filtered.len(), total_indexed = total, "semantic search completed");
|
||||
info!(
|
||||
matched = filtered.len(),
|
||||
total_indexed = total,
|
||||
"semantic search completed"
|
||||
);
|
||||
|
||||
let mut by_file: std::collections::BTreeMap<String, Vec<&&CodeSymbol>> =
|
||||
std::collections::BTreeMap::new();
|
||||
@@ -1350,7 +1345,11 @@ impl Tool for SemanticSearch {
|
||||
sym.line,
|
||||
sym.context.trim(),
|
||||
doc_str,
|
||||
if sym.context.trim().len() > 80 { "…" } else { "" }
|
||||
if sym.context.trim().len() > 80 {
|
||||
"…"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
));
|
||||
}
|
||||
output.push('\n');
|
||||
@@ -1574,12 +1573,7 @@ impl Tool for ListSymbols {
|
||||
for (file, file_syms) in &by_file {
|
||||
out.push_str(&format!("`{file}`:\n"));
|
||||
for sym in file_syms {
|
||||
out.push_str(&format!(
|
||||
" `{}` {} L{}\n",
|
||||
sym.kind,
|
||||
sym.name,
|
||||
sym.line,
|
||||
));
|
||||
out.push_str(&format!(" `{}` {} L{}\n", sym.kind, sym.name, sym.line,));
|
||||
}
|
||||
}
|
||||
out.push('\n');
|
||||
@@ -1633,51 +1627,82 @@ mod tests {
|
||||
fn test_extract_typescript_function_and_class() {
|
||||
let content = "function hello() {}\nexport class User {}\ninterface Person {}\n";
|
||||
let symbols = extract_typescript(content, "test.ts");
|
||||
assert!(symbols.iter().any(|s| s.name == "hello" && s.kind == SymbolKind::Function));
|
||||
assert!(symbols.iter().any(|s| s.name == "User" && s.kind == SymbolKind::Class));
|
||||
assert!(symbols.iter().any(|s| s.name == "Person" && s.kind == SymbolKind::Interface));
|
||||
assert!(symbols
|
||||
.iter()
|
||||
.any(|s| s.name == "hello" && s.kind == SymbolKind::Function));
|
||||
assert!(symbols
|
||||
.iter()
|
||||
.any(|s| s.name == "User" && s.kind == SymbolKind::Class));
|
||||
assert!(symbols
|
||||
.iter()
|
||||
.any(|s| s.name == "Person" && s.kind == SymbolKind::Interface));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_typescript_const() {
|
||||
let content = "export const API_URL = 'http://example.com';\nconst MAX_RETRIES = 3;\n";
|
||||
let symbols = extract_typescript(content, "test.ts");
|
||||
assert!(symbols.iter().any(|s| s.name == "API_URL" && s.kind == SymbolKind::Constant));
|
||||
assert!(symbols.iter().any(|s| s.name == "MAX_RETRIES" && s.kind == SymbolKind::Constant));
|
||||
assert!(symbols
|
||||
.iter()
|
||||
.any(|s| s.name == "API_URL" && s.kind == SymbolKind::Constant));
|
||||
assert!(symbols
|
||||
.iter()
|
||||
.any(|s| s.name == "MAX_RETRIES" && s.kind == SymbolKind::Constant));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_python_def_and_class() {
|
||||
let content = "class MyClass:\n def method(self):\n pass\n\ndef top_func():\n pass\n";
|
||||
let content =
|
||||
"class MyClass:\n def method(self):\n pass\n\ndef top_func():\n pass\n";
|
||||
let symbols = extract_python(content, "test.py");
|
||||
assert!(symbols.iter().any(|s| s.name == "MyClass" && s.kind == SymbolKind::Class));
|
||||
assert!(symbols.iter().any(|s| s.name == "MyClass.method" && s.kind == SymbolKind::Function));
|
||||
assert!(symbols.iter().any(|s| s.name == "top_func" && s.kind == SymbolKind::Function));
|
||||
assert!(symbols
|
||||
.iter()
|
||||
.any(|s| s.name == "MyClass" && s.kind == SymbolKind::Class));
|
||||
assert!(symbols
|
||||
.iter()
|
||||
.any(|s| s.name == "MyClass.method" && s.kind == SymbolKind::Function));
|
||||
assert!(symbols
|
||||
.iter()
|
||||
.any(|s| s.name == "top_func" && s.kind == SymbolKind::Function));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_python_variable() {
|
||||
let content = "DATABASE_URL = 'postgres://localhost'\nconfig_path = '/etc/app'\n";
|
||||
let symbols = extract_python(content, "test.py");
|
||||
assert!(symbols.iter().any(|s| s.name == "DATABASE_URL" && s.kind == SymbolKind::Constant));
|
||||
assert!(symbols.iter().any(|s| s.name == "config_path" && s.kind == SymbolKind::Variable));
|
||||
assert!(symbols
|
||||
.iter()
|
||||
.any(|s| s.name == "DATABASE_URL" && s.kind == SymbolKind::Constant));
|
||||
assert!(symbols
|
||||
.iter()
|
||||
.any(|s| s.name == "config_path" && s.kind == SymbolKind::Variable));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_go_func_and_struct() {
|
||||
let content = "func main() {}\nfunc (s *Server) Serve() {}\ntype Config struct {\n Name string\n}\n";
|
||||
let symbols = extract_go(content, "test.go");
|
||||
assert!(symbols.iter().any(|s| s.name == "main" && s.kind == SymbolKind::Function));
|
||||
assert!(symbols.iter().any(|s| s.name == "Serve" && s.kind == SymbolKind::Function));
|
||||
assert!(symbols.iter().any(|s| s.name == "Config" && s.kind == SymbolKind::Struct));
|
||||
assert!(symbols
|
||||
.iter()
|
||||
.any(|s| s.name == "main" && s.kind == SymbolKind::Function));
|
||||
assert!(symbols
|
||||
.iter()
|
||||
.any(|s| s.name == "Serve" && s.kind == SymbolKind::Function));
|
||||
assert!(symbols
|
||||
.iter()
|
||||
.any(|s| s.name == "Config" && s.kind == SymbolKind::Struct));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_go_const_and_var() {
|
||||
let content = "const VERSION = \"1.0\"\nvar DefaultPort = 8080\n";
|
||||
let symbols = extract_go(content, "test.go");
|
||||
assert!(symbols.iter().any(|s| s.name == "VERSION" && s.kind == SymbolKind::Constant));
|
||||
assert!(symbols.iter().any(|s| s.name == "DefaultPort" && s.kind == SymbolKind::Variable));
|
||||
assert!(symbols
|
||||
.iter()
|
||||
.any(|s| s.name == "VERSION" && s.kind == SymbolKind::Constant));
|
||||
assert!(symbols
|
||||
.iter()
|
||||
.any(|s| s.name == "DefaultPort" && s.kind == SymbolKind::Variable));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -89,9 +89,13 @@ impl Tool for Bash {
|
||||
let start = std::time::Instant::now();
|
||||
let timeout = Duration::from_millis(timeout_ms);
|
||||
|
||||
let mut child_stdout = child.stdout.take()
|
||||
let mut child_stdout = child
|
||||
.stdout
|
||||
.take()
|
||||
.ok_or_else(|| anyhow::anyhow!("failed to capture stdout"))?;
|
||||
let mut child_stderr = child.stderr.take()
|
||||
let mut child_stderr = child
|
||||
.stderr
|
||||
.take()
|
||||
.ok_or_else(|| anyhow::anyhow!("failed to capture stderr"))?;
|
||||
|
||||
let stdout_handle = std::thread::spawn(move || -> std::io::Result<Vec<u8>> {
|
||||
|
||||
@@ -33,8 +33,10 @@ pub fn is_credential_path(path: &str) -> bool {
|
||||
/// return list of suspected credential reads.
|
||||
#[instrument(skip(cmd))]
|
||||
pub fn check_credential_read(cmd: &str) -> Vec<String> {
|
||||
let re = Regex::new(r#"(?i)(?:cat|head|tail|less|more|vim?|nano|xdg-open|open|type|echo)\s+(~?/[\w/.@-]+)"#)
|
||||
.expect("hardcoded credential-read regex is valid");
|
||||
let re = Regex::new(
|
||||
r#"(?i)(?:cat|head|tail|less|more|vim?|nano|xdg-open|open|type|echo)\s+(~?/[\w/.@-]+)"#,
|
||||
)
|
||||
.expect("hardcoded credential-read regex is valid");
|
||||
let mut findings = Vec::new();
|
||||
for cap in re.captures_iter(cmd) {
|
||||
let path = cap.get(1).map(|m| m.as_str()).unwrap_or("");
|
||||
|
||||
@@ -28,10 +28,7 @@ pub fn check_git_destructive(cmd: &str) -> Result<(), String> {
|
||||
|
||||
for pattern in &destructive_patterns {
|
||||
if cmd_lower.contains(pattern) {
|
||||
return Err(format!(
|
||||
"destructive git operation blocked: '{}'",
|
||||
pattern
|
||||
));
|
||||
return Err(format!("destructive git operation blocked: '{}'", pattern));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user