2026-07-11 13:16:10 +07:00
|
|
|
use std::path::PathBuf;
|
|
|
|
|
use serde_json::Value;
|
|
|
|
|
use anyhow::Result;
|
|
|
|
|
|
2026-07-11 20:21:59 +07:00
|
|
|
pub mod bash_tools;
|
2026-07-11 13:16:10 +07:00
|
|
|
pub mod fs;
|
|
|
|
|
pub mod git_cred;
|
|
|
|
|
pub mod git_operator;
|
|
|
|
|
pub mod git_worktree;
|
|
|
|
|
pub mod internet;
|
2026-07-11 20:44:15 +07:00
|
|
|
pub mod memory;
|
2026-07-11 13:16:10 +07:00
|
|
|
pub mod plan;
|
|
|
|
|
pub mod search;
|
|
|
|
|
pub mod seqthink;
|
|
|
|
|
pub mod shell;
|
|
|
|
|
pub mod shell_filter;
|
2026-07-11 20:44:15 +07:00
|
|
|
pub mod utility;
|
2026-07-11 13:16:10 +07:00
|
|
|
pub mod workflow;
|
|
|
|
|
|
|
|
|
|
pub trait Tool: Send + Sync {
|
|
|
|
|
fn name(&self) -> &'static str;
|
|
|
|
|
fn description(&self) -> &'static str;
|
|
|
|
|
fn parameters(&self) -> Value;
|
|
|
|
|
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String>;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-11 18:23:01 +07:00
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct GraduatedCheck {
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub pattern: String,
|
|
|
|
|
pub rule: String,
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-11 20:21:59 +07:00
|
|
|
#[derive(Clone)]
|
2026-07-11 13:16:10 +07:00
|
|
|
pub struct ToolCtx {
|
|
|
|
|
pub workspaces: Vec<PathBuf>,
|
|
|
|
|
pub session_dir: PathBuf,
|
|
|
|
|
pub memory_dir: PathBuf,
|
|
|
|
|
pub download_dir: PathBuf,
|
|
|
|
|
pub worktrees_dir: PathBuf,
|
|
|
|
|
pub dir_cache: std::sync::Arc<tokio::sync::RwLock<super::app::state::misc::DirCache>>,
|
|
|
|
|
pub internet_mode: super::model::settings::InternetMode,
|
|
|
|
|
pub origin: crate::app::state::types::Origin,
|
2026-07-11 18:23:01 +07:00
|
|
|
pub graduated_checks: Vec<GraduatedCheck>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
matches.push(check.name.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
matches
|
2026-07-11 13:16:10 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ToolCtx {
|
|
|
|
|
pub fn builder() -> ToolCtxBuilder {
|
|
|
|
|
ToolCtxBuilder::default()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct ToolCtxBuilder {
|
|
|
|
|
pub workspaces: Vec<PathBuf>,
|
|
|
|
|
pub session_dir: PathBuf,
|
|
|
|
|
pub memory_dir: PathBuf,
|
|
|
|
|
pub download_dir: PathBuf,
|
|
|
|
|
pub worktrees_dir: PathBuf,
|
|
|
|
|
pub dir_cache: std::sync::Arc<tokio::sync::RwLock<super::app::state::misc::DirCache>>,
|
|
|
|
|
pub internet_mode: super::model::settings::InternetMode,
|
|
|
|
|
pub origin: crate::app::state::types::Origin,
|
2026-07-11 18:23:01 +07:00
|
|
|
pub graduated_checks: Vec<GraduatedCheck>,
|
2026-07-11 13:16:10 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for ToolCtxBuilder {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
ToolCtxBuilder {
|
|
|
|
|
workspaces: Vec::new(),
|
|
|
|
|
session_dir: PathBuf::new(),
|
|
|
|
|
memory_dir: PathBuf::new(),
|
|
|
|
|
download_dir: PathBuf::new(),
|
|
|
|
|
worktrees_dir: PathBuf::new(),
|
|
|
|
|
dir_cache: std::sync::Arc::new(tokio::sync::RwLock::new(super::app::state::misc::DirCache::new())),
|
|
|
|
|
internet_mode: super::model::settings::InternetMode::Off,
|
|
|
|
|
origin: crate::app::state::types::Origin::Main,
|
2026-07-11 18:23:01 +07:00
|
|
|
graduated_checks: Vec::new(),
|
2026-07-11 13:16:10 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ToolCtxBuilder {
|
|
|
|
|
pub fn workspaces(mut self, v: Vec<PathBuf>) -> Self { self.workspaces = v; self }
|
|
|
|
|
pub fn session_dir(mut self, v: PathBuf) -> Self { self.session_dir = v; self }
|
|
|
|
|
pub fn memory_dir(mut self, v: PathBuf) -> Self { self.memory_dir = v; self }
|
|
|
|
|
pub fn download_dir(mut self, v: PathBuf) -> Self { self.download_dir = v; self }
|
|
|
|
|
pub fn worktrees_dir(mut self, v: PathBuf) -> Self { self.worktrees_dir = v; self }
|
|
|
|
|
pub fn internet_mode(mut self, v: super::model::settings::InternetMode) -> Self { self.internet_mode = v; self }
|
|
|
|
|
pub fn origin(mut self, v: crate::app::state::types::Origin) -> Self { self.origin = v; self }
|
2026-07-11 18:23:01 +07:00
|
|
|
pub fn graduated_checks(mut self, v: Vec<GraduatedCheck>) -> Self { self.graduated_checks = v; self }
|
2026-07-11 13:16:10 +07:00
|
|
|
pub fn build(self) -> ToolCtx {
|
|
|
|
|
ToolCtx {
|
|
|
|
|
workspaces: self.workspaces,
|
|
|
|
|
session_dir: self.session_dir,
|
|
|
|
|
memory_dir: self.memory_dir,
|
|
|
|
|
download_dir: self.download_dir,
|
|
|
|
|
worktrees_dir: self.worktrees_dir,
|
|
|
|
|
dir_cache: self.dir_cache,
|
|
|
|
|
internet_mode: self.internet_mode,
|
|
|
|
|
origin: self.origin,
|
2026-07-11 18:23:01 +07:00
|
|
|
graduated_checks: self.graduated_checks,
|
2026-07-11 13:16:10 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn all_tools() -> Vec<Box<dyn Tool>> {
|
|
|
|
|
vec![
|
|
|
|
|
Box::new(super::tool::fs::read::Read),
|
|
|
|
|
Box::new(super::tool::fs::write::Write),
|
|
|
|
|
Box::new(super::tool::fs::edit::Edit),
|
2026-07-11 20:44:15 +07:00
|
|
|
Box::new(super::tool::fs::delete::Delete),
|
2026-07-11 13:16:10 +07:00
|
|
|
Box::new(super::tool::search::Grep),
|
|
|
|
|
Box::new(super::tool::search::Glob),
|
2026-07-11 20:21:59 +07:00
|
|
|
Box::new(super::tool::bash_tools::BashOutput),
|
|
|
|
|
Box::new(super::tool::bash_tools::BashKill),
|
2026-07-11 13:16:10 +07:00
|
|
|
Box::new(super::tool::shell::Bash),
|
|
|
|
|
Box::new(super::tool::git_operator::GitOperator),
|
|
|
|
|
Box::new(super::tool::git_worktree::GitWorktree),
|
|
|
|
|
Box::new(super::tool::git_cred::GitCred),
|
|
|
|
|
Box::new(super::tool::seqthink::SeqThink),
|
|
|
|
|
Box::new(super::tool::plan::PlanEnter),
|
|
|
|
|
Box::new(super::tool::plan::PlanReady),
|
|
|
|
|
Box::new(super::tool::workflow::WorkflowRun),
|
2026-07-11 20:21:59 +07:00
|
|
|
Box::new(super::tool::internet::fetch::Fetch),
|
|
|
|
|
Box::new(super::tool::internet::download::Download),
|
|
|
|
|
Box::new(super::tool::internet::search::Search),
|
2026-07-11 20:44:15 +07:00
|
|
|
Box::new(super::tool::memory::remember::Remember),
|
|
|
|
|
Box::new(super::tool::memory::forget::Forget),
|
|
|
|
|
Box::new(super::tool::memory::recall::Recall),
|
|
|
|
|
Box::new(super::tool::utility::cd::Cd),
|
|
|
|
|
Box::new(super::tool::utility::dir_list::DirList),
|
|
|
|
|
Box::new(super::tool::utility::dir_cache_update::DirCacheUpdate),
|
|
|
|
|
Box::new(super::tool::utility::pong::Pong),
|
|
|
|
|
Box::new(super::tool::utility::todowrite::Todowrite),
|
2026-07-11 13:16:10 +07:00
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn tool_is_risky(name: &str) -> bool {
|
|
|
|
|
matches!(name, "write" | "delete" | "edit" | "bash" | "git_operator")
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-11 20:21:59 +07:00
|
|
|
pub fn tool_defs(tools: &[Box<dyn Tool>]) -> Vec<crate::dto::openrouter::request::ToolDef> {
|
|
|
|
|
tools
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|t| crate::dto::openrouter::request::ToolDef {
|
|
|
|
|
type_: "function".to_string(),
|
|
|
|
|
function: crate::dto::openrouter::request::ToolFunctionDef {
|
|
|
|
|
name: t.name().to_string(),
|
|
|
|
|
description: t.description().to_string(),
|
|
|
|
|
parameters: t.parameters(),
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-11 13:16:10 +07:00
|
|
|
pub const DEFERRED_TOOLS: &[&str] = &[
|
|
|
|
|
"read", "write", "edit", "bash", "grep", "glob",
|
|
|
|
|
"git_operator", "git_worktree", "git_cred",
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
pub fn resolve_path(workspaces: &[PathBuf], rel: &str) -> Result<PathBuf> {
|
|
|
|
|
let _parts: Vec<&str> = rel.splitn(2, '/').collect();
|
|
|
|
|
let (ws_idx, path) = if rel.starts_with('[') {
|
|
|
|
|
let close = rel.find(']').ok_or_else(|| anyhow::anyhow!("invalid workspace prefix"))?;
|
|
|
|
|
let idx: usize = rel[1..close].parse().map_err(|_| anyhow::anyhow!("invalid workspace index"))?;
|
|
|
|
|
(idx, &rel[close + 1..])
|
|
|
|
|
} else {
|
|
|
|
|
(0, rel)
|
|
|
|
|
};
|
|
|
|
|
let base = workspaces.get(ws_idx).ok_or_else(|| anyhow::anyhow!("workspace index {} out of range", ws_idx))?;
|
|
|
|
|
let abs = if path.is_empty() {
|
|
|
|
|
base.clone()
|
|
|
|
|
} else {
|
|
|
|
|
base.join(path)
|
|
|
|
|
};
|
|
|
|
|
let canon = abs.canonicalize().unwrap_or(abs);
|
|
|
|
|
if workspaces.iter().any(|w| canon.starts_with(w)) {
|
|
|
|
|
Ok(canon)
|
|
|
|
|
} else {
|
|
|
|
|
anyhow::bail!("path '{}' is outside all workspace roots", rel)
|
|
|
|
|
}
|
|
|
|
|
}
|