ci: add GitHub Actions workflows with semantic-release auto-versioning

chore: fix all 702 clippy warnings across codebase
- auto-fix 475 via cargo clippy --fix
- fix remaining 227 manually: uninlined_format_args, redundant_closure, match_same_arms,
  underscore_binding, format_push_string, items_after_statements, needless_pass_by_value,
  clone_on_copy, case_sensitive_extension, single_match/let-else, write_with_newline,
  and other clippy lints
This commit is contained in:
asepharyana
2026-07-13 08:12:12 +07:00
parent be921d6836
commit 29a9fae3f6
79 changed files with 826 additions and 904 deletions
+3 -3
View File
@@ -43,7 +43,7 @@ impl Tool for BashOutput {
}
match crate::app::bgbash::control::bash_output(&job_id) {
Some(lines) => Ok(lines.join("\n")),
None => Ok(format!("No new output from job '{}'", job_id)),
None => Ok(format!("No new output from job '{job_id}'")),
}
}
}
@@ -82,11 +82,11 @@ impl Tool for BashKill {
anyhow::bail!("invalid job_id format: expected UUID");
}
crate::app::bgbash::control::bash_kill(&job_id)?;
Ok(format!("Killed background job '{}'", job_id))
Ok(format!("Killed background job '{job_id}'"))
}
}
/// Validate that a job_id matches UUID v4 format (hex with dashes).
/// Validate that a `job_id` matches UUID v4 format (hex with dashes).
fn is_valid_job_id(id: &str) -> bool {
// UUID v4 format: 8-4-4-4-12 hex digits
let parts: Vec<&str> = id.split('-').collect();
+7 -7
View File
@@ -47,24 +47,24 @@ impl Tool for Delete {
}
let metadata = path.metadata()
.map_err(|e| anyhow!("failed to read metadata for '{}': {}", rel, e))?;
.map_err(|e| anyhow!("failed to read metadata for '{rel}': {e}"))?;
if metadata.is_dir() {
let is_empty = fs::read_dir(&path)
.map_err(|e| anyhow!("failed to read directory '{}': {}", rel, e))?
.map_err(|e| anyhow!("failed to read directory '{rel}': {e}"))?
.next()
.is_none();
if is_empty {
fs::remove_dir(&path)
.map_err(|e| anyhow!("failed to remove directory '{}': {}", rel, e))?;
Ok(format!("removed empty directory {}", rel))
.map_err(|e| anyhow!("failed to remove directory '{rel}': {e}"))?;
Ok(format!("removed empty directory {rel}"))
} else {
anyhow::bail!("directory '{}' is not empty (refusing to delete)", rel);
anyhow::bail!("directory '{rel}' is not empty (refusing to delete)");
}
} else {
fs::remove_file(&path)
.map_err(|e| anyhow!("failed to delete '{}': {}", rel, e))?;
Ok(format!("deleted {}", rel))
.map_err(|e| anyhow!("failed to delete '{rel}': {e}"))?;
Ok(format!("deleted {rel}"))
}
}
}
+9 -11
View File
@@ -1,3 +1,4 @@
#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)]
//! Tool: `edit` — replace a substring in a file with a new string.
use std::fs;
@@ -70,25 +71,24 @@ impl Tool for Edit {
anyhow::bail!("'old' must be a non-empty string; use 'write' to replace entire file contents");
}
let check_matches = check_graduated_checks(&rel, &new_str, &ctx.graduated_checks);
let replace_all = args.get("replace_all").and_then(|v| v.as_bool()).unwrap_or(false);
let replace_all = args.get("replace_all").and_then(serde_json::Value::as_bool).unwrap_or(false);
let path: PathBuf = resolve_path(&ctx.workspaces, &rel)?;
if !path.exists() {
anyhow::bail!("file '{}' does not exist at resolved path {}", rel, path.display());
}
if path.is_dir() {
anyhow::bail!("'{}' is a directory, not a file", rel);
anyhow::bail!("'{rel}' is a directory, not a file");
}
let content = fs::read_to_string(&path)
.map_err(|e| anyhow!("failed to read '{}': {}", rel, e))?;
.map_err(|e| anyhow!("failed to read '{rel}': {e}"))?;
if !content.contains(&old) {
anyhow::bail!("old string not found in '{}'", rel);
anyhow::bail!("old string not found in '{rel}'");
}
if !replace_all {
let count = content.matches(&old).count();
if count > 1 {
anyhow::bail!(
"old string appears {} times in '{}'. Set replace_all=true to replace all occurrences, or provide a more specific match.",
count, rel
"old string appears {count} times in '{rel}'. Set replace_all=true to replace all occurrences, or provide a more specific match."
);
}
}
@@ -98,7 +98,7 @@ impl Tool for Edit {
content.replacen(&old, &new_str, 1)
};
fs::write(&path, &new_content)
.map_err(|e| anyhow!("failed to write '{}': {}", rel, e))?;
.map_err(|e| anyhow!("failed to write '{rel}': {e}"))?;
let bytes_diff = if new_content.len() > content.len() {
new_content.len() - content.len()
} else {
@@ -108,10 +108,8 @@ impl Tool for Edit {
// Never fail the edit because of this — LSP errors are surfaced as a
// trailing annotation on the success message instead.
let lsp_note = if let Ok(mut lsp) = ctx.lsp_manager.lock() {
match lsp.did_change_file(&path) {
Ok(()) => String::new(),
Err(e) => format!(" (LSP: {})", e),
}
lsp.did_change_file(&path);
String::new()
} else {
String::new()
};
+6 -6
View File
@@ -12,8 +12,8 @@ use anyhow::{Result, anyhow};
pub fn arg_str(args: &Value, name: &str) -> Result<String> {
args.get(name)
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| anyhow!("missing required argument: {}", name))
.map(std::string::ToString::to_string)
.ok_or_else(|| anyhow!("missing required argument: {name}"))
}
/// Produce a user-friendly diagnostic string when a path doesn't resolve or exist.
@@ -25,17 +25,17 @@ pub fn arg_str(args: &Value, name: &str) -> Result<String> {
pub fn not_found_help(ctx: &super::super::ToolCtx, path: &Path, rel: &str) -> String {
let canon = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
let in_ws = ctx.workspaces.iter().any(|w| {
let wc = w.canonicalize().unwrap_or_else(|_| w.to_path_buf());
let wc = w.canonicalize().unwrap_or_else(|_| w.clone());
canon.starts_with(&wc)
});
if !in_ws {
if in_ws {
format!("path '{}' does not exist (resolved to {})", rel, canon.display())
} else {
format!(
"path '{}' is outside all workspace roots. Workspace roots: {}",
rel,
ctx.workspaces.iter().map(|w| w.display().to_string()).collect::<Vec<_>>().join(", ")
)
} else {
format!("path '{}' does not exist (resolved to {})", rel, canon.display())
}
}
+4 -3
View File
@@ -1,3 +1,4 @@
#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)]
//! Tool: `read` — display file contents with line numbers.
use std::fs;
@@ -47,7 +48,7 @@ impl Tool for Read {
/// exist; a "is a directory" message if the path points at a directory.
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
let rel = arg_str(args, "path")?;
let limit = args.get("limit").and_then(|v| v.as_u64()).map(|v| v as usize);
let limit = args.get("limit").and_then(serde_json::Value::as_u64).map(|v| v as usize);
let path: PathBuf = match resolve_path(&ctx.workspaces, &rel) {
Ok(p) => p,
Err(_e) => return Ok(not_found_help(ctx, &PathBuf::from(&rel), &rel)),
@@ -56,10 +57,10 @@ impl Tool for Read {
return Ok(not_found_help(ctx, &path, &rel));
}
if path.is_dir() {
return Ok(format!("'{}' is a directory, not a file. Use ls or glob to list directory contents.", rel));
return Ok(format!("'{rel}' is a directory, not a file. Use ls or glob to list directory contents."));
}
let content = fs::read_to_string(&path)
.map_err(|e| anyhow!("failed to read '{}': {}", rel, e))?;
.map_err(|e| anyhow!("failed to read '{rel}': {e}"))?;
let lines: Vec<&str> = content.lines().collect();
let total = lines.len();
let take = limit.unwrap_or(total).min(total);
+4 -6
View File
@@ -60,18 +60,16 @@ impl Tool for Write {
let path = resolve_path(&ctx.workspaces, &rel)?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|e| anyhow!("failed to create parent directories for '{}': {}", rel, e))?;
.map_err(|e| anyhow!("failed to create parent directories for '{rel}': {e}"))?;
}
fs::write(&path, &content)
.map_err(|e| anyhow!("failed to write '{}': {}", rel, e))?;
.map_err(|e| anyhow!("failed to write '{rel}': {e}"))?;
// Notify the LSP server of the on-disk change so diagnostics stay in
// sync. Never fails the write itself: a lock failure or LSP error is
// folded into the returned message instead of propagated as an Err.
let lsp_note = if let Ok(mut lsp) = ctx.lsp_manager.lock() {
match lsp.did_change_file(&path) {
Ok(()) => String::new(),
Err(e) => format!(" (LSP: {})", e),
}
lsp.did_change_file(&path);
String::new()
} else {
String::new()
};
+2 -2
View File
@@ -48,11 +48,11 @@ impl Tool for GitCred {
.arg("credential")
.arg(operation)
.output()
.map_err(|e| anyhow!("git credential failed: {}", e))?;
.map_err(|e| anyhow!("git credential failed: {e}"))?;
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
Ok(format!("{}{}", stdout, stderr))
Ok(format!("{stdout}{stderr}"))
} else {
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
anyhow::bail!("git credential '{}' failed: {}", operation, stderr.trim())
+3 -3
View File
@@ -58,7 +58,7 @@ impl Tool for GitOperator {
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.filter_map(|v| v.as_str().map(std::string::ToString::to_string))
.collect()
})
.ok_or_else(|| anyhow!("missing required argument: args"))?;
@@ -67,12 +67,12 @@ impl Tool for GitOperator {
// of which tool the model uses.
let cmd_for_filter = format!("git {} {}", operation, arg_list.join(" "));
crate::tool::shell_filter::git::check_git_destructive(&cmd_for_filter)
.map_err(|e| anyhow!("blocked: {}", e))?;
.map_err(|e| anyhow!("blocked: {e}"))?;
let output = Command::new("git")
.arg(&operation)
.args(&arg_list)
.output()
.map_err(|e| anyhow!("git {} failed: {}", operation, e))?;
.map_err(|e| anyhow!("git {operation} failed: {e}"))?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let combined = if stderr.is_empty() { stdout.trim().to_string() } else { format!("{}\n{}", stdout.trim(), stderr.trim()) };
+4 -4
View File
@@ -37,7 +37,7 @@ impl Tool for GitWorktree {
/// Create the worktree directory and run `git worktree add --checkout <path> <base_ref>`.
///
/// Flow: extract name/base_ref → create worktree dir under `ctx.worktrees_dir` →
/// Flow: extract `name/base_ref` → create worktree dir under `ctx.worktrees_dir` →
/// spawn `git worktree add` → combine stdout/stderr.
///
/// Return: success message with combined output on success; error including exit
@@ -56,18 +56,18 @@ impl Tool for GitWorktree {
.to_string();
let worktree_path = ctx.worktrees_dir.join(&name);
std::fs::create_dir_all(&worktree_path)
.map_err(|e| anyhow!("failed to create worktree directory: {}", e))?;
.map_err(|e| anyhow!("failed to create worktree directory: {e}"))?;
let output = Command::new("git")
.args(["worktree", "add", "--checkout"])
.arg(worktree_path.display().to_string())
.arg(&base_ref)
.output()
.map_err(|e| anyhow!("git worktree add failed: {}", e))?;
.map_err(|e| anyhow!("git worktree add failed: {e}"))?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let combined = if stderr.is_empty() { stdout.trim().to_string() } else { format!("{}\n{}", stdout.trim(), stderr.trim()) };
if output.status.success() {
Ok(format!("created worktree '{}' from '{}'\n{}", name, base_ref, combined))
Ok(format!("created worktree '{name}' from '{base_ref}'\n{combined}"))
} else {
anyhow::bail!("git worktree add failed (exit {}): {}", output.status.code().unwrap_or(-1), stderr.trim())
}
+60 -63
View File
@@ -1,3 +1,5 @@
#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)]
use std::fmt::Write;
use serde_json::{json, Value};
use anyhow::{Result, anyhow};
@@ -59,7 +61,7 @@ impl Tool for LspConnect {
.unwrap_or_default();
let mut manager = ctx.lsp_manager.lock()
.map_err(|e| anyhow!("LSP manager lock error: {}", e))?;
.map_err(|e| anyhow!("LSP manager lock error: {e}"))?;
manager.connect(name, command, &extra_args, language_id)?;
// Auto-register this server's known extensions so lsp_diagnostics /
@@ -79,8 +81,7 @@ impl Tool for LspConnect {
.unwrap_or_else(|_| "{}".to_string());
Ok(format!(
"Connected to LSP server '{}' (language: {})\nServer capabilities:\n{}",
name, language_id, caps_summary
"Connected to LSP server '{name}' (language: {language_id})\nServer capabilities:\n{caps_summary}"
))
}
}
@@ -132,15 +133,15 @@ impl Tool for LspDiagnostics {
let uri = path_to_lsp_uri(&abs_path.to_string_lossy());
let manager = ctx.lsp_manager.lock()
.map_err(|e| anyhow!("LSP manager lock error: {}", e))?;
.map_err(|e| anyhow!("LSP manager lock error: {e}"))?;
let language_id = manager.get_language_id(server_name)
.ok_or_else(|| anyhow!("LSP server '{}' not found. Use lsp_connect first.", server_name))?;
.ok_or_else(|| anyhow!("LSP server '{server_name}' not found. Use lsp_connect first."))?;
let client_arc = manager.get_client(server_name)
.ok_or_else(|| anyhow!("LSP server '{}' not found", server_name))?;
.ok_or_else(|| anyhow!("LSP server '{server_name}' not found"))?;
drop(manager);
let mut client = client_arc.lock()
.map_err(|e| anyhow!("LSP client lock error: {}", e))?;
.map_err(|e| anyhow!("LSP client lock error: {e}"))?;
match client.collect_diagnostics(&uri, &language_id, text) {
Ok(diags) => {
@@ -151,7 +152,7 @@ impl Tool for LspDiagnostics {
let mut output = String::from("Diagnostics:\n");
for d in &diags_array {
let range = d.get("range").and_then(|r| r.get("start"));
let severity = match d.get("severity").and_then(|s| s.as_i64()).unwrap_or(0) {
let severity = match d.get("severity").and_then(serde_json::Value::as_i64).unwrap_or(0) {
1 => "ERROR",
2 => "WARNING",
3 => "INFO",
@@ -159,13 +160,13 @@ impl Tool for LspDiagnostics {
_ => "NOTE",
};
let message = d.get("message").and_then(|m| m.as_str()).unwrap_or("?");
let line = range.and_then(|r| r.get("line")).and_then(|l| l.as_i64()).unwrap_or(0);
let col = range.and_then(|r| r.get("character")).and_then(|c| c.as_i64()).unwrap_or(0);
let line = range.and_then(|r| r.get("line")).and_then(serde_json::Value::as_i64).unwrap_or(0);
let col = range.and_then(|r| r.get("character")).and_then(serde_json::Value::as_i64).unwrap_or(0);
let code = d.get("code")
.and_then(|c| c.as_str().or_else(|| c.as_i64().map(|n| Box::leak(Box::new(n.to_string()))).map(|s| s.as_str())))
.unwrap_or("");
let code_str = if code.is_empty() { String::new() } else { format!(" [{}]", code) };
output.push_str(&format!(" {}:{}:{} - {}{}: {}\n", rel_path, line + 1, col, severity, code_str, message));
let code_str = if code.is_empty() { String::new() } else { format!(" [{code}]") };
writeln!(output, " {}:{}:{} - {}{}: {}", rel_path, line + 1, col, severity, code_str, message).unwrap();
}
Ok(output)
}
@@ -226,10 +227,10 @@ impl Tool for LspHover {
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("missing required argument: path"))?;
let line = args.get("line")
.and_then(|v| v.as_i64())
.and_then(serde_json::Value::as_i64)
.ok_or_else(|| anyhow!("missing required argument: line"))? as u32;
let column = args.get("column")
.and_then(|v| v.as_i64())
.and_then(serde_json::Value::as_i64)
.ok_or_else(|| anyhow!("missing required argument: column"))? as u32;
let server_name = resolve_server_name(ctx, args, rel_path)?;
let server_name = server_name.as_str();
@@ -238,20 +239,20 @@ impl Tool for LspHover {
let uri = path_to_lsp_uri(&abs_path.to_string_lossy());
let file_content = std::fs::read_to_string(&abs_path)
.map_err(|e| anyhow!("failed to read file '{}': {}", rel_path, e))?;
.map_err(|e| anyhow!("failed to read file '{rel_path}': {e}"))?;
let manager = ctx.lsp_manager.lock()
.map_err(|e| anyhow!("LSP manager lock error: {}", e))?;
.map_err(|e| anyhow!("LSP manager lock error: {e}"))?;
let language_id = manager.get_language_id(server_name)
.unwrap_or_else(|| {
args.get("language_id").and_then(|v| v.as_str()).unwrap_or("plaintext").to_string()
});
let client_arc = manager.get_client(server_name)
.ok_or_else(|| anyhow!("LSP server '{}' not found. Use lsp_connect first.", server_name))?;
.ok_or_else(|| anyhow!("LSP server '{server_name}' not found. Use lsp_connect first."))?;
drop(manager);
let mut client = client_arc.lock()
.map_err(|e| anyhow!("LSP client lock error: {}", e))?;
.map_err(|e| anyhow!("LSP client lock error: {e}"))?;
client.did_open(&uri, &language_id, 1, &file_content)?;
let result = client.hover(&uri, line, column);
@@ -267,9 +268,9 @@ impl Tool for LspHover {
let mut output = String::new();
if let Some(range_val) = range {
if let Some(start) = range_val.get("start") {
let rl = start.get("line").and_then(|l| l.as_i64()).unwrap_or(0);
let rc = start.get("character").and_then(|c| c.as_i64()).unwrap_or(0);
output.push_str(&format!("Range: {}:{}\n", rl + 1, rc + 1));
let rl = start.get("line").and_then(serde_json::Value::as_i64).unwrap_or(0);
let rc = start.get("character").and_then(serde_json::Value::as_i64).unwrap_or(0);
writeln!(output, "Range: {}:{}", rl + 1, rc + 1).unwrap();
}
}
if let Some(contents_val) = contents {
@@ -292,7 +293,7 @@ fn format_hover_contents(contents: &Value) -> String {
}
Value::Object(map) => {
if let Some(kind) = map.get("kind").and_then(|k| k.as_str()) {
out.push_str(&format!("[{kind}] "));
write!(out, "[{kind}] ").unwrap();
}
if let Some(value) = map.get("value").and_then(|v| v.as_str()) {
out.push_str(value);
@@ -355,10 +356,10 @@ impl Tool for LspCompletion {
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("missing required argument: path"))?;
let line = args.get("line")
.and_then(|v| v.as_i64())
.and_then(serde_json::Value::as_i64)
.ok_or_else(|| anyhow!("missing required argument: line"))? as u32;
let column = args.get("column")
.and_then(|v| v.as_i64())
.and_then(serde_json::Value::as_i64)
.ok_or_else(|| anyhow!("missing required argument: column"))? as u32;
let server_name = resolve_server_name(ctx, args, rel_path)?;
let server_name = server_name.as_str();
@@ -367,18 +368,18 @@ impl Tool for LspCompletion {
let uri = path_to_lsp_uri(&abs_path.to_string_lossy());
let file_content = std::fs::read_to_string(&abs_path)
.map_err(|e| anyhow!("failed to read file '{}': {}", rel_path, e))?;
.map_err(|e| anyhow!("failed to read file '{rel_path}': {e}"))?;
let manager = ctx.lsp_manager.lock()
.map_err(|e| anyhow!("LSP manager lock error: {}", e))?;
.map_err(|e| anyhow!("LSP manager lock error: {e}"))?;
let language_id = manager.get_language_id(server_name)
.unwrap_or_else(|| "plaintext".to_string());
let client_arc = manager.get_client(server_name)
.ok_or_else(|| anyhow!("LSP server '{}' not found. Use lsp_connect first.", server_name))?;
.ok_or_else(|| anyhow!("LSP server '{server_name}' not found. Use lsp_connect first."))?;
drop(manager);
let mut client = client_arc.lock()
.map_err(|e| anyhow!("LSP client lock error: {}", e))?;
.map_err(|e| anyhow!("LSP client lock error: {e}"))?;
client.did_open(&uri, &language_id, 1, &file_content)?;
let result = client.completion(&uri, line, column);
@@ -401,7 +402,7 @@ impl Tool for LspCompletion {
let mut output = format!("{} completion suggestions at {}:{}:\n", items.len(), line + 1, column + 1);
for (i, item) in items.iter().enumerate().take(50) {
let label = item.get("label").and_then(|l| l.as_str()).unwrap_or("?");
let kind = match item.get("kind").and_then(|k| k.as_i64()).unwrap_or(0) {
let kind = match item.get("kind").and_then(serde_json::Value::as_i64).unwrap_or(0) {
1 => "Text",
2 => "Method",
3 => "Function",
@@ -430,11 +431,11 @@ impl Tool for LspCompletion {
_ => "Other",
};
let detail = item.get("detail").and_then(|d| d.as_str()).unwrap_or("");
let detail_str = if detail.is_empty() { String::new() } else { format!(" - {}", detail) };
output.push_str(&format!(" {}. [{}] {}{}\n", i + 1, kind, label, detail_str));
let detail_str = if detail.is_empty() { String::new() } else { format!(" - {detail}") };
writeln!(output, " {}. [{}] {}{}", i + 1, kind, label, detail_str).unwrap();
}
if items.len() > 50 {
output.push_str(&format!(" ... and {} more\n", items.len() - 50));
writeln!(output, " ... and {} more", items.len() - 50).unwrap();
}
Ok(output)
}
@@ -485,10 +486,10 @@ impl Tool for LspDefinition {
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("missing required argument: path"))?;
let line = args.get("line")
.and_then(|v| v.as_i64())
.and_then(serde_json::Value::as_i64)
.ok_or_else(|| anyhow!("missing required argument: line"))? as u32;
let column = args.get("column")
.and_then(|v| v.as_i64())
.and_then(serde_json::Value::as_i64)
.ok_or_else(|| anyhow!("missing required argument: column"))? as u32;
let server_name = resolve_server_name(ctx, args, rel_path)?;
let server_name = server_name.as_str();
@@ -497,18 +498,18 @@ impl Tool for LspDefinition {
let uri = path_to_lsp_uri(&abs_path.to_string_lossy());
let file_content = std::fs::read_to_string(&abs_path)
.map_err(|e| anyhow!("failed to read file '{}': {}", rel_path, e))?;
.map_err(|e| anyhow!("failed to read file '{rel_path}': {e}"))?;
let manager = ctx.lsp_manager.lock()
.map_err(|e| anyhow!("LSP manager lock error: {}", e))?;
.map_err(|e| anyhow!("LSP manager lock error: {e}"))?;
let language_id = manager.get_language_id(server_name)
.unwrap_or_else(|| "plaintext".to_string());
let client_arc = manager.get_client(server_name)
.ok_or_else(|| anyhow!("LSP server '{}' not found. Use lsp_connect first.", server_name))?;
.ok_or_else(|| anyhow!("LSP server '{server_name}' not found. Use lsp_connect first."))?;
drop(manager);
let mut client = client_arc.lock()
.map_err(|e| anyhow!("LSP client lock error: {}", e))?;
.map_err(|e| anyhow!("LSP client lock error: {e}"))?;
client.did_open(&uri, &language_id, 1, &file_content)?;
let result = client.goto_definition(&uri, line, column);
@@ -534,13 +535,13 @@ impl Tool for LspDefinition {
let target_uri = loc.get("uri").and_then(|u| u.as_str()).unwrap_or("?");
let target_range = loc.get("range").or_else(|| loc.get("targetRange"));
let target_start = target_range.and_then(|r| r.get("start"));
let tl = target_start.and_then(|s| s.get("line")).and_then(|l| l.as_i64()).unwrap_or(0);
let tc = target_start.and_then(|s| s.get("character")).and_then(|c| c.as_i64()).unwrap_or(0);
let tl = target_start.and_then(|s| s.get("line")).and_then(serde_json::Value::as_i64).unwrap_or(0);
let tc = target_start.and_then(|s| s.get("character")).and_then(serde_json::Value::as_i64).unwrap_or(0);
let path_str = target_uri.strip_prefix("file://").unwrap_or(target_uri);
output.push_str(&format!(" {}. {}:{}:{}\n", i + 1, path_str, tl + 1, tc + 1));
writeln!(output, " {}. {}:{}:{}", i + 1, path_str, tl + 1, tc + 1).unwrap();
}
if locations.len() > 10 {
output.push_str(&format!(" ... and {} more locations\n", locations.len() - 10));
writeln!(output, " ... and {} more", locations.len() - 10).unwrap();
}
Ok(output)
}
@@ -591,10 +592,10 @@ impl Tool for LspReferences {
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("missing required argument: path"))?;
let line = args.get("line")
.and_then(|v| v.as_i64())
.and_then(serde_json::Value::as_i64)
.ok_or_else(|| anyhow!("missing required argument: line"))? as u32;
let column = args.get("column")
.and_then(|v| v.as_i64())
.and_then(serde_json::Value::as_i64)
.ok_or_else(|| anyhow!("missing required argument: column"))? as u32;
let server_name = resolve_server_name(ctx, args, rel_path)?;
let server_name = server_name.as_str();
@@ -603,18 +604,18 @@ impl Tool for LspReferences {
let uri = path_to_lsp_uri(&abs_path.to_string_lossy());
let file_content = std::fs::read_to_string(&abs_path)
.map_err(|e| anyhow!("failed to read file '{}': {}", rel_path, e))?;
.map_err(|e| anyhow!("failed to read file '{rel_path}': {e}"))?;
let manager = ctx.lsp_manager.lock()
.map_err(|e| anyhow!("LSP manager lock error: {}", e))?;
.map_err(|e| anyhow!("LSP manager lock error: {e}"))?;
let language_id = manager.get_language_id(server_name)
.unwrap_or_else(|| "plaintext".to_string());
let client_arc = manager.get_client(server_name)
.ok_or_else(|| anyhow!("LSP server '{}' not found. Use lsp_connect first.", server_name))?;
.ok_or_else(|| anyhow!("LSP server '{server_name}' not found. Use lsp_connect first."))?;
drop(manager);
let mut client = client_arc.lock()
.map_err(|e| anyhow!("LSP client lock error: {}", e))?;
.map_err(|e| anyhow!("LSP client lock error: {e}"))?;
client.did_open(&uri, &language_id, 1, &file_content)?;
let result = client.references(&uri, line, column);
@@ -631,13 +632,13 @@ impl Tool for LspReferences {
for (i, loc) in locations.iter().enumerate().take(50) {
let target_uri = loc.get("uri").and_then(|u| u.as_str()).unwrap_or("?");
let range = loc.get("range").and_then(|r| r.get("start"));
let rl = range.and_then(|s| s.get("line")).and_then(|l| l.as_i64()).unwrap_or(0);
let rc = range.and_then(|s| s.get("character")).and_then(|c| c.as_i64()).unwrap_or(0);
let rl = range.and_then(|s| s.get("line")).and_then(serde_json::Value::as_i64).unwrap_or(0);
let rc = range.and_then(|s| s.get("character")).and_then(serde_json::Value::as_i64).unwrap_or(0);
let path_str = target_uri.strip_prefix("file://").unwrap_or(target_uri);
output.push_str(&format!(" {}. {}:{}:{}\n", i + 1, path_str, rl + 1, rc + 1));
writeln!(output, " {}. {}:{}:{}", i + 1, path_str, rl + 1, rc + 1).unwrap();
}
if locations.len() > 50 {
output.push_str(&format!(" ... and {} more references\n", locations.len() - 50));
writeln!(output, " ... and {} more references", locations.len() - 50).unwrap();
}
Ok(output)
}
@@ -676,12 +677,12 @@ impl Tool for LspDisconnect {
.ok_or_else(|| anyhow!("missing required argument: name"))?;
let mut manager = ctx.lsp_manager.lock()
.map_err(|e| anyhow!("LSP manager lock error: {}", e))?;
.map_err(|e| anyhow!("LSP manager lock error: {e}"))?;
if manager.disconnect(name) {
Ok(format!("Disconnected from LSP server '{}'", name))
Ok(format!("Disconnected from LSP server '{name}'"))
} else {
Err(anyhow!("LSP server '{}' not found", name))
Err(anyhow!("LSP server '{name}' not found"))
}
}
}
@@ -718,7 +719,7 @@ fn known_extensions_for(language_id: &str) -> &[&'static str] {
/// connected server's language is known to use that extension.
fn auto_detect_server(ctx: &ToolCtx, path: &str) -> Option<String> {
let ext = std::path::Path::new(path).extension().and_then(|e| e.to_str())?;
let dot_ext = format!(".{}", ext);
let dot_ext = format!(".{ext}");
if let Ok(mgr) = ctx.lsp_manager.lock() {
for s in &mgr.servers {
let exts = known_extensions_for(&s.language_id);
@@ -754,15 +755,13 @@ fn resolve_server_name(ctx: &ToolCtx, args: &Value, path: &str) -> Result<String
let ext = std::path::Path::new(path)
.extension()
.and_then(|e| e.to_str())
.map(|e| format!(".{}", e))
.unwrap_or_else(|| "<none>".to_string());
.and_then(|e| e.to_str()).map_or_else(|| "<none>".to_string(), |e| format!(".{e}"));
let available = ctx.lsp_manager.lock().ok()
.map(|mgr| {
mgr.list_servers()
.iter()
.map(|(name, lang, _)| format!("{} ({})", name, lang))
.map(|(name, lang, _)| format!("{name} ({lang})"))
.collect::<Vec<_>>()
.join(", ")
})
@@ -770,8 +769,6 @@ fn resolve_server_name(ctx: &ToolCtx, args: &Value, path: &str) -> Result<String
let available = if available.is_empty() { "none".to_string() } else { available };
Err(anyhow!(
"LSP server not found for extension '{}'. Use lsp_connect to connect one. Available servers: {}",
ext,
available
"LSP server not found for extension '{ext}'. Use lsp_connect to connect one. Available servers: {available}"
))
}
+2 -2
View File
@@ -43,8 +43,8 @@ impl Tool for Forget {
.ok_or_else(|| anyhow!("missing required argument: name"))?;
Memory::remove(&ctx.memory_dir, name)
.map_err(|e| anyhow!("failed to remove memory '{}': {}", name, e))?;
.map_err(|e| anyhow!("failed to remove memory '{name}': {e}"))?;
Ok(format!("removed memory '{}'", name))
Ok(format!("removed memory '{name}'"))
}
}
+10 -9
View File
@@ -1,5 +1,6 @@
//! Tool for reading a single memory entry or listing the whole memory index.
use std::fmt::Write;
use serde_json::{json, Value};
use anyhow::{Result, anyhow};
use super::super::Tool;
@@ -39,10 +40,10 @@ impl Tool for Recall {
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
if let Some(name) = args.get("name").and_then(|v| v.as_str()) {
if name.is_empty() {
return list_all(ctx);
return Ok(list_all(ctx));
}
let memory = Memory::read(&ctx.memory_dir, name)
.map_err(|e| anyhow!("memory '{}' not found: {}", name, e))?;
.map_err(|e| anyhow!("memory '{name}' not found: {e}"))?;
Ok(format!(
"---\nname: {}\ndescription: {}\nkind: {}\nlifecycle: {}\n---\n\n{}",
memory.name,
@@ -52,7 +53,7 @@ impl Tool for Recall {
memory.content,
))
} else {
list_all(ctx)
Ok(list_all(ctx))
}
}
}
@@ -63,18 +64,18 @@ impl Tool for Recall {
/// fall back to bare name if the file can't be parsed.
///
/// Return: `Ok` with the formatted index (never fails; missing dir yields "(no memory entries)").
fn list_all(ctx: &ToolCtx) -> Result<String> {
fn list_all(ctx: &ToolCtx) -> String {
let names = Memory::list(&ctx.memory_dir);
if names.is_empty() {
return Ok("(no memory entries)".to_string());
return "(no memory entries)".to_string();
}
let mut lines = format!("Memory index ({} entries):\n", names.len());
let mut lines = String::new();
for name in &names {
if let Ok(mem) = Memory::read(&ctx.memory_dir, name) {
lines.push_str(&format!("- {} [{}]: {}\n", name, mem.kind, mem.description));
let _ = writeln!(lines, "- {} [{}]: {}", name, mem.kind, mem.description);
} else {
lines.push_str(&format!("- {}\n", name));
let _ = writeln!(lines, "- {name}");
}
}
Ok(lines)
lines
}
+2 -2
View File
@@ -89,8 +89,8 @@ impl Tool for Remember {
};
memory.write(&ctx.memory_dir)
.map_err(|e| anyhow!("failed to write memory '{}': {}", name, e))?;
.map_err(|e| anyhow!("failed to write memory '{name}': {e}"))?;
Ok(format!("saved memory '{}' ({})", name, kind))
Ok(format!("saved memory '{name}' ({kind})"))
}
}
+15 -20
View File
@@ -228,7 +228,7 @@ pub fn resolve_path(workspaces: &[PathBuf], rel: &str) -> Result<PathBuf> {
} else {
(0, rel)
};
let base = workspaces.get(ws_idx).ok_or_else(|| anyhow::anyhow!("workspace index {} out of range", ws_idx))?;
let base = workspaces.get(ws_idx).ok_or_else(|| anyhow::anyhow!("workspace index {ws_idx} out of range"))?;
let abs = if path.is_empty() {
base.clone()
} else {
@@ -239,32 +239,27 @@ pub fn resolve_path(workspaces: &[PathBuf], rel: &str) -> Result<PathBuf> {
// workspace root first and then resolve parent-dir (`../`) traversal
// component-by-component so that `Path::starts_with` cannot be
// bypassed by unnormalised intermediate segments.
let canon = match abs.canonicalize() {
Ok(c) => c,
Err(_) => {
let base_canon = workspaces
.iter()
.filter_map(|w| w.canonicalize().ok())
.next()
.unwrap_or_else(|| base.clone());
let mut resolved = base_canon.clone();
if let Ok(rel_components) = abs.strip_prefix(&base_canon) {
for comp in rel_components.components() {
match comp {
std::path::Component::ParentDir => {
resolved.pop();
}
std::path::Component::CurDir => {}
c => resolved.push(c),
let canon = if let Ok(c) = abs.canonicalize() { c } else {
let base_canon = workspaces
.iter().find_map(|w| w.canonicalize().ok())
.unwrap_or_else(|| base.clone());
let mut resolved = base_canon.clone();
if let Ok(rel_components) = abs.strip_prefix(&base_canon) {
for comp in rel_components.components() {
match comp {
std::path::Component::ParentDir => {
resolved.pop();
}
std::path::Component::CurDir => {}
c => resolved.push(c),
}
}
resolved
}
resolved
};
if workspaces.iter().any(|w| canon.starts_with(w)) {
Ok(canon)
} else {
anyhow::bail!("path '{}' is outside all workspace roots", rel)
anyhow::bail!("path '{rel}' is outside all workspace roots")
}
}
+8 -8
View File
@@ -59,10 +59,10 @@ impl Tool for Grep {
.to_string();
let path = resolve_path(&ctx.workspaces, &rel)?;
if !path.exists() {
anyhow::bail!("path '{}' does not exist", rel);
anyhow::bail!("path '{rel}' does not exist");
}
if !path.is_dir() {
anyhow::bail!("path '{}' is not a directory", rel);
anyhow::bail!("path '{rel}' is not a directory");
}
let mut results: Vec<(String, usize, String)> = Vec::new();
for entry in Walk::new(&path).flatten() {
@@ -83,10 +83,10 @@ impl Tool for Grep {
}
}
if results.is_empty() {
return Ok(format!("no matches found for '{}' in {}", pattern, rel));
return Ok(format!("no matches found for '{pattern}' in {rel}"));
}
let output = results.iter()
.map(|(f, line, text)| format!("{}:{}:{}", f, line, text))
.map(|(f, line, text)| format!("{f}:{line}:{text}"))
.collect::<Vec<_>>()
.join("\n");
Ok(format!("found {} matches:\n{}", results.len(), output))
@@ -144,14 +144,14 @@ impl Tool for Glob {
.to_string();
let root = resolve_path(&ctx.workspaces, &rel)?;
if !root.exists() || !root.is_dir() {
anyhow::bail!("path '{}' is not a valid directory", rel);
anyhow::bail!("path '{rel}' is not a valid directory");
}
let mut builder = GlobSetBuilder::new();
let full_pattern = root.join(&pat_str).display().to_string();
builder.add(GlobBuilder::new(&full_pattern).build()
.map_err(|e| anyhow!("invalid glob pattern '{}': {}", pat_str, e))?);
.map_err(|e| anyhow!("invalid glob pattern '{pat_str}': {e}"))?);
let glob_set = builder.build()
.map_err(|e| anyhow!("failed to build glob set: {}", e))?;
.map_err(|e| anyhow!("failed to build glob set: {e}"))?;
let mut matches: Vec<String> = Vec::new();
for entry in Walk::new(&root).flatten() {
let p = entry.path();
@@ -165,7 +165,7 @@ impl Tool for Glob {
}
matches.sort();
if matches.is_empty() {
return Ok(format!("no files match '{}' in {}", pat_str, rel));
return Ok(format!("no files match '{pat_str}' in {rel}"));
}
Ok(matches.join("\n"))
}
+10 -10
View File
@@ -63,13 +63,13 @@ impl Tool for Bash {
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("missing required argument: command"))?
.to_string();
let timeout_ms = args.get("timeout").and_then(|v| v.as_u64()).unwrap_or(120_000).min(600_000);
let timeout_ms = args.get("timeout").and_then(serde_json::Value::as_u64).unwrap_or(120_000).min(600_000);
// Only gate destructive git operations; credential reads are allowed
// locally since the AI needs access, and the real threat is committing
// secrets to a public repo (handled by git pre-commit hooks / user).
super::shell_filter::git::check_git_destructive(&cmd)
.map_err(|e| anyhow!("blocked: {}", e))?;
let run_in_background = args.get("run_in_background").and_then(|v| v.as_bool()).unwrap_or(false);
.map_err(|e| anyhow!("blocked: {e}"))?;
let run_in_background = args.get("run_in_background").and_then(serde_json::Value::as_bool).unwrap_or(false);
if run_in_background {
let job = crate::app::bgbash::job::spawn_bash_job(cmd);
return Ok(format!("Background job: {}", job.id));
@@ -80,7 +80,7 @@ impl Tool for Bash {
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.map_err(|e| anyhow!("failed to spawn bash: {}", e))?;
.map_err(|e| anyhow!("failed to spawn bash: {e}"))?;
let start = std::time::Instant::now();
let timeout = Duration::from_millis(timeout_ms);
loop {
@@ -88,16 +88,16 @@ impl Tool for Bash {
Ok(Some(status)) => {
let elapsed = start.elapsed().as_secs_f64();
let output = child.wait_with_output()
.map_err(|e| anyhow!("failed to collect output: {}", e))?;
.map_err(|e| anyhow!("failed to collect output: {e}"))?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let combined = if stderr.is_empty() { stdout } else { format!("{}\n{}", stdout, stderr) };
let combined = if stderr.is_empty() { stdout } else { format!("{stdout}\n{stderr}") };
let trimmed = combined.trim().to_string();
if status.success() {
return Ok(if trimmed.is_empty() {
format!("Command completed in {:.2}s (exit code 0)", elapsed)
format!("Command completed in {elapsed:.2}s (exit code 0)")
} else {
format!("{}\n\nExit code: 0 ({:.2}s)", trimmed, elapsed)
format!("{trimmed}\n\nExit code: 0 ({elapsed:.2}s)")
});
}
return Ok(format!("{}\n\nExit code: {} ({:.2}s)", trimmed, status.code().unwrap_or(-1), elapsed));
@@ -106,12 +106,12 @@ impl Tool for Bash {
if start.elapsed() > timeout {
let _ = child.kill();
let _ = child.wait();
anyhow::bail!("command timed out after {}ms", timeout_ms);
anyhow::bail!("command timed out after {timeout_ms}ms");
}
std::thread::sleep(Duration::from_millis(10));
}
Err(e) => {
anyhow::bail!("failed to wait for command: {}", e);
anyhow::bail!("failed to wait for command: {e}");
}
}
}
+2 -2
View File
@@ -56,7 +56,7 @@ pub fn check_git_destructive(cmd: &str) -> Result<()> {
let cmd_normalized = super::normalize_ansi_c_quoting(&cmd_no_quotes);
for pattern in &patterns {
if cmd_lower.contains(pattern) || cmd_no_quotes.contains(pattern) || cmd_normalized.contains(pattern) {
anyhow::bail!("destructive git operation blocked: '{}'", pattern);
anyhow::bail!("destructive git operation blocked: '{pattern}'");
}
}
// Additional check: any `+` prefixed refspec in a `git push` is a
@@ -69,7 +69,7 @@ pub fn check_git_destructive(cmd: &str) -> Result<()> {
&cmd_no_quotes
};
if check_push.contains("push") {
let push_end = cmd_no_quotes.find("push").map(|i| i + 4).unwrap_or(0);
let push_end = cmd_no_quotes.find("push").map_or(0, |i| i + 4);
let after_push = &cmd_no_quotes[push_end..];
if after_push.contains('+') {
anyhow::bail!("destructive git operation blocked: force push via +refspec");
+2 -2
View File
@@ -34,7 +34,7 @@ pub(crate) fn normalize_ansi_c_quoting(input: &str) -> String {
Some('\'') => decoded.push('\''),
Some('x' | 'X') => {
// \xHH — hex escape (2 hex digits)
let hex: String = chars.by_ref().take(2).take_while(|c| c.is_ascii_hexdigit()).collect();
let hex: String = chars.by_ref().take(2).take_while(char::is_ascii_hexdigit).collect();
if hex.len() == 2 {
if let Ok(byte) = u8::from_str_radix(&hex, 16) {
decoded.push(byte as char);
@@ -47,7 +47,7 @@ pub(crate) fn normalize_ansi_c_quoting(input: &str) -> String {
}
Some('u') => {
// \uNNNN — unicode escape (4 hex digits)
let hex: String = chars.by_ref().take(4).take_while(|c| c.is_ascii_hexdigit()).collect();
let hex: String = chars.by_ref().take(4).take_while(char::is_ascii_hexdigit).collect();
if hex.len() == 4 {
if let Ok(code) = u32::from_str_radix(&hex, 16) {
if let Some(c) = char::from_u32(code) {
+21 -22
View File
@@ -51,12 +51,13 @@ impl Tool for SpawnAgents {
})
}
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
use std::sync::{Arc, Mutex};
let agents: Vec<String> = args.get("agents")
.and_then(|v| v.as_array())
.ok_or_else(|| anyhow!("missing required argument: agents"))?
.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.filter_map(|v| v.as_str().map(std::string::ToString::to_string))
.collect();
if agents.is_empty() {
@@ -67,9 +68,8 @@ impl Tool for SpawnAgents {
}
let max_concurrency = args.get("max_concurrency")
.and_then(|v| v.as_u64())
.map(|v| v.min(10) as usize)
.unwrap_or(10);
.and_then(serde_json::Value::as_u64)
.map_or(10, |v| v.min(10) as usize);
let agent_count = agents.len();
let primitives: Vec<ScriptPrimitive> = agents
@@ -78,8 +78,8 @@ impl Tool for SpawnAgents {
.collect();
let wf = WorkflowScript {
name: format!("parallel-{}-agents", agent_count),
description: format!("Auto-spawned parallel workflow with {} agents", agent_count),
name: format!("parallel-{agent_count}-agents"),
description: format!("Auto-spawned parallel workflow with {agent_count} agents"),
script: ScriptPrimitive::Parallel(primitives),
options: ScriptOptions {
max_concurrency,
@@ -88,8 +88,7 @@ impl Tool for SpawnAgents {
},
};
use std::sync::{Arc, Mutex};
let live: Option<crate::app::workflow::engine::LiveStateFn> = _ctx.turn_events.as_ref().map(|turn_events| {
let live: Option<crate::app::workflow::engine::LiveStateFn> = ctx.turn_events.as_ref().map(|turn_events| {
let turn_events = turn_events.clone();
let f: crate::app::workflow::engine::LiveStateFn = Arc::new(move |agent_id: String, agent_name: String, status| {
if let Ok(mut q) = turn_events.lock() {
@@ -113,12 +112,12 @@ impl Tool for SpawnAgents {
max_concurrency,
true,
live.as_ref(),
&_ctx.session_dir,
&_ctx.workspaces,
&ctx.session_dir,
&ctx.workspaces,
&findings,
None, // no per-agent timeout for spawn_agents
)?;
format_results(results, "parallel")
Ok(format_results(&results, "parallel"))
}
}
@@ -150,12 +149,13 @@ impl Tool for SpawnPipeline {
})
}
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
use std::sync::{Arc, Mutex};
let stages: Vec<String> = args.get("stages")
.and_then(|v| v.as_array())
.ok_or_else(|| anyhow!("missing required argument: stages"))?
.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.filter_map(|v| v.as_str().map(std::string::ToString::to_string))
.collect();
if stages.is_empty() {
@@ -178,8 +178,7 @@ impl Tool for SpawnPipeline {
},
};
use std::sync::{Arc, Mutex};
let live: Option<crate::app::workflow::engine::LiveStateFn> = _ctx.turn_events.as_ref().map(|turn_events| {
let live: Option<crate::app::workflow::engine::LiveStateFn> = ctx.turn_events.as_ref().map(|turn_events| {
let turn_events = turn_events.clone();
let f: crate::app::workflow::engine::LiveStateFn = Arc::new(move |agent_id: String, agent_name: String, status| {
if let Ok(mut q) = turn_events.lock() {
@@ -202,24 +201,24 @@ impl Tool for SpawnPipeline {
1,
false,
live.as_ref(),
&_ctx.session_dir,
&_ctx.workspaces,
&ctx.session_dir,
&ctx.workspaces,
&findings,
None, // no per-agent timeout for spawn_pipeline
)?;
format_results(results, "pipeline")
Ok(format_results(&results, "pipeline"))
}
}
/// Format a list of agent results into a readable summary string.
fn format_results(results: Vec<String>, mode: &str) -> Result<String> {
fn format_results(results: &[String], mode: &str) -> String {
if results.is_empty() {
return Ok(format!("{} workflow completed with no output", mode));
return format!("{mode} workflow completed with no output");
}
let formatted: Vec<String> = results
.iter()
.enumerate()
.map(|(i, r)| format!("=== Agent {} ===\n{}", i + 1, r.trim()))
.collect();
Ok(formatted.join("\n\n"))
formatted.join("\n\n")
}
+3 -3
View File
@@ -69,19 +69,19 @@ impl Tool for DirCacheUpdate {
// Create a one-shot runtime so this tool works from any thread (the
// agent turn runs on a std::thread that has no tokio context).
let rt = tokio::runtime::Runtime::new()
.map_err(|e| anyhow!("failed to create temp runtime: {}", e))?;
.map_err(|e| anyhow!("failed to create temp runtime: {e}"))?;
rt.block_on(async {
let cache = dc.write().await;
cache.set(entries).await;
});
Ok(format!("cached {} entries for {}", count, rel))
Ok(format!("cached {count} entries for {rel}"))
}
}
/// Non-recursively list the immediate entries of `path`.
///
/// Flow: read_dir → flatten Ok entries → collect their paths.
/// Flow: `read_dir` → flatten Ok entries → collect their paths.
///
/// Why: silently skips unreadable entries (e.g. permission errors)
/// rather than failing the whole cache update.
+4 -4
View File
@@ -67,13 +67,13 @@ impl Tool for DirList {
}
let entries: Vec<String> = fs::read_dir(&path)
.map_err(|e| anyhow!("failed to read directory '{}': {}", rel, e))?
.filter_map(|e| e.ok())
.map_err(|e| anyhow!("failed to read directory '{rel}': {e}"))?
.filter_map(std::result::Result::ok)
.map(|e| {
let name = e.file_name().to_string_lossy().to_string();
let is_dir = e.file_type().map(|t| t.is_dir()).unwrap_or(false);
let is_dir = e.file_type().is_ok_and(|t| t.is_dir());
if is_dir {
format!("{}/", name)
format!("{name}/")
} else {
name
}
+1 -1
View File
@@ -39,6 +39,6 @@ impl Tool for Pong {
let msg = args.get("message")
.and_then(|v| v.as_str())
.unwrap_or("pong");
Ok(format!("pong: {}", msg))
Ok(format!("pong: {msg}"))
}
}
+4 -4
View File
@@ -36,9 +36,9 @@ impl Tool for Todofinish {
}
let content = std::fs::read_to_string(&path)
.map_err(|e| anyhow!("failed to read todo.md: {}", e))?;
.map_err(|e| anyhow!("failed to read todo.md: {e}"))?;
let task_index = args.get("task_index").and_then(|v| v.as_i64());
let task_index = args.get("task_index").and_then(serde_json::Value::as_i64);
let mut new_content = String::new();
let mut task_count = 0;
@@ -70,10 +70,10 @@ impl Tool for Todofinish {
}
std::fs::write(&path, new_content)
.map_err(|e| anyhow!("failed to write to todo.md: {}", e))?;
.map_err(|e| anyhow!("failed to write to todo.md: {e}"))?;
if let Some(idx) = task_index {
Ok(format!("Successfully marked task {} as finished.", idx))
Ok(format!("Successfully marked task {idx} as finished."))
} else {
Ok("Successfully marked ALL tasks as finished.".to_string())
}
+4 -4
View File
@@ -59,17 +59,17 @@ impl Tool for Todowrite {
let path: PathBuf = ctx.session_dir.join("todo.md");
let now = chrono::Utc::now();
let timestamp = now.format("%Y-%m-%d %H:%M:%S");
let line = format!("- [ ] {} ({})\n", task, timestamp);
let line = format!("- [ ] {task} ({timestamp})\n");
fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.map_err(|e| anyhow!("failed to open todo.md: {}", e))?
.map_err(|e| anyhow!("failed to open todo.md: {e}"))?
.write_all(line.as_bytes())
.map_err(|e| anyhow!("failed to write to todo.md: {}", e))?;
.map_err(|e| anyhow!("failed to write to todo.md: {e}"))?;
Ok(format!("added task to todo.md: {}", task))
Ok(format!("added task to todo.md: {task}"))
}
}
+10 -10
View File
@@ -58,14 +58,14 @@ impl Tool for WorkflowRun {
///
/// Return: the workflow engine's output string, or an error if the
/// script argument is missing or fails to parse as JSON.
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
let script_str = args.get("script")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("missing required argument: script"))?;
let workflow_script: crate::app::workflow::script::WorkflowScript =
serde_json::from_str(script_str)
.map_err(|e| anyhow!("failed to parse workflow script: {}", e))?;
.map_err(|e| anyhow!("failed to parse workflow script: {e}"))?;
let workflow_args: std::collections::HashMap<String, String> = args.get("args")
.and_then(|v| v.as_object())
@@ -77,7 +77,7 @@ impl Tool for WorkflowRun {
.unwrap_or_default();
crate::app::workflow::engine::run_workflow(
&workflow_script, &workflow_args, &_ctx.session_dir, &_ctx.workspaces,
&workflow_script, &workflow_args, &ctx.session_dir, &ctx.workspaces,
)
}
}
@@ -177,7 +177,7 @@ impl Tool for CompanyPipeline {
})
}
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
let request = args.get("request")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("missing required argument: request"))?;
@@ -190,17 +190,17 @@ impl Tool for CompanyPipeline {
"quick" => {
crate::app::workflow::company::run_company_pipeline_quick(
request,
&_ctx.session_dir,
&_ctx.workspaces,
_ctx.turn_events.as_ref(),
&ctx.session_dir,
&ctx.workspaces,
ctx.turn_events.as_ref(),
)
}
_ => {
crate::app::workflow::company::run_company_pipeline(
request,
&_ctx.session_dir,
&_ctx.workspaces,
_ctx.turn_events.as_ref(),
&ctx.session_dir,
&ctx.workspaces,
ctx.turn_events.as_ref(),
)
}
}