mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-05 23:54:50 +08:00
Centralize slash command parsing in the commands crate so the REPL can share help metadata and grow toward Claw Code parity without duplicating handlers. This adds shared /help and /model parsing, routes REPL dispatch through the shared parser, and upgrades /status to report model and token totals. To satisfy the required verification gate, this also fixes existing workspace clippy and test blockers in runtime, tools, api, and compat-harness that were unrelated to the new command behavior but prevented fmt/clippy/test from passing cleanly. Constraint: Preserve existing prompt-mode and REPL behavior while adding real slash commands Constraint: cargo fmt, clippy, and workspace tests must pass before shipping command-surface work Rejected: Keep command handling only in main.rs | would deepen duplication with commands crate and resume path Confidence: high Scope-risk: moderate Reversibility: clean Directive: Extend new slash commands through the shared commands crate first so REPL and resume entrypoints stay consistent Tested: cargo fmt; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace Not-tested: live Anthropic network execution beyond existing mocked/integration coverage
261 lines
8.1 KiB
Rust
261 lines
8.1 KiB
Rust
use runtime::{
|
|
edit_file, execute_bash, glob_search, grep_search, read_file, write_file, BashCommandInput,
|
|
GrepSearchInput,
|
|
};
|
|
use serde::Deserialize;
|
|
use serde_json::{json, Value};
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct ToolManifestEntry {
|
|
pub name: String,
|
|
pub source: ToolSource,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum ToolSource {
|
|
Base,
|
|
Conditional,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
|
pub struct ToolRegistry {
|
|
entries: Vec<ToolManifestEntry>,
|
|
}
|
|
|
|
impl ToolRegistry {
|
|
#[must_use]
|
|
pub fn new(entries: Vec<ToolManifestEntry>) -> Self {
|
|
Self { entries }
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn entries(&self) -> &[ToolManifestEntry] {
|
|
&self.entries
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct ToolSpec {
|
|
pub name: &'static str,
|
|
pub description: &'static str,
|
|
pub input_schema: Value,
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn mvp_tool_specs() -> Vec<ToolSpec> {
|
|
vec![
|
|
ToolSpec {
|
|
name: "bash",
|
|
description: "Execute a shell command in the current workspace.",
|
|
input_schema: json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"command": { "type": "string" },
|
|
"timeout": { "type": "integer", "minimum": 1 },
|
|
"description": { "type": "string" },
|
|
"run_in_background": { "type": "boolean" },
|
|
"dangerouslyDisableSandbox": { "type": "boolean" }
|
|
},
|
|
"required": ["command"],
|
|
"additionalProperties": false
|
|
}),
|
|
},
|
|
ToolSpec {
|
|
name: "read_file",
|
|
description: "Read a text file from the workspace.",
|
|
input_schema: json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"path": { "type": "string" },
|
|
"offset": { "type": "integer", "minimum": 0 },
|
|
"limit": { "type": "integer", "minimum": 1 }
|
|
},
|
|
"required": ["path"],
|
|
"additionalProperties": false
|
|
}),
|
|
},
|
|
ToolSpec {
|
|
name: "write_file",
|
|
description: "Write a text file in the workspace.",
|
|
input_schema: json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"path": { "type": "string" },
|
|
"content": { "type": "string" }
|
|
},
|
|
"required": ["path", "content"],
|
|
"additionalProperties": false
|
|
}),
|
|
},
|
|
ToolSpec {
|
|
name: "edit_file",
|
|
description: "Replace text in a workspace file.",
|
|
input_schema: json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"path": { "type": "string" },
|
|
"old_string": { "type": "string" },
|
|
"new_string": { "type": "string" },
|
|
"replace_all": { "type": "boolean" }
|
|
},
|
|
"required": ["path", "old_string", "new_string"],
|
|
"additionalProperties": false
|
|
}),
|
|
},
|
|
ToolSpec {
|
|
name: "glob_search",
|
|
description: "Find files by glob pattern.",
|
|
input_schema: json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"pattern": { "type": "string" },
|
|
"path": { "type": "string" }
|
|
},
|
|
"required": ["pattern"],
|
|
"additionalProperties": false
|
|
}),
|
|
},
|
|
ToolSpec {
|
|
name: "grep_search",
|
|
description: "Search file contents with a regex pattern.",
|
|
input_schema: json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"pattern": { "type": "string" },
|
|
"path": { "type": "string" },
|
|
"glob": { "type": "string" },
|
|
"output_mode": { "type": "string" },
|
|
"-B": { "type": "integer", "minimum": 0 },
|
|
"-A": { "type": "integer", "minimum": 0 },
|
|
"-C": { "type": "integer", "minimum": 0 },
|
|
"context": { "type": "integer", "minimum": 0 },
|
|
"-n": { "type": "boolean" },
|
|
"-i": { "type": "boolean" },
|
|
"type": { "type": "string" },
|
|
"head_limit": { "type": "integer", "minimum": 1 },
|
|
"offset": { "type": "integer", "minimum": 0 },
|
|
"multiline": { "type": "boolean" }
|
|
},
|
|
"required": ["pattern"],
|
|
"additionalProperties": false
|
|
}),
|
|
},
|
|
]
|
|
}
|
|
|
|
pub fn execute_tool(name: &str, input: &Value) -> Result<String, String> {
|
|
match name {
|
|
"bash" => from_value::<BashCommandInput>(input).and_then(run_bash),
|
|
"read_file" => from_value::<ReadFileInput>(input).and_then(|input| run_read_file(&input)),
|
|
"write_file" => {
|
|
from_value::<WriteFileInput>(input).and_then(|input| run_write_file(&input))
|
|
}
|
|
"edit_file" => from_value::<EditFileInput>(input).and_then(|input| run_edit_file(&input)),
|
|
"glob_search" => {
|
|
from_value::<GlobSearchInputValue>(input).and_then(|input| run_glob_search(&input))
|
|
}
|
|
"grep_search" => {
|
|
from_value::<GrepSearchInput>(input).and_then(|input| run_grep_search(&input))
|
|
}
|
|
_ => Err(format!("unsupported tool: {name}")),
|
|
}
|
|
}
|
|
|
|
fn from_value<T: for<'de> Deserialize<'de>>(input: &Value) -> Result<T, String> {
|
|
serde_json::from_value(input.clone()).map_err(|error| error.to_string())
|
|
}
|
|
|
|
fn run_bash(input: BashCommandInput) -> Result<String, String> {
|
|
serde_json::to_string_pretty(&execute_bash(input).map_err(|error| error.to_string())?)
|
|
.map_err(|error| error.to_string())
|
|
}
|
|
|
|
fn run_read_file(input: &ReadFileInput) -> Result<String, String> {
|
|
to_pretty_json(
|
|
read_file(&input.path, input.offset, input.limit).map_err(|error| io_to_string(&error))?,
|
|
)
|
|
}
|
|
|
|
fn run_write_file(input: &WriteFileInput) -> Result<String, String> {
|
|
to_pretty_json(write_file(&input.path, &input.content).map_err(|error| io_to_string(&error))?)
|
|
}
|
|
|
|
fn run_edit_file(input: &EditFileInput) -> Result<String, String> {
|
|
to_pretty_json(
|
|
edit_file(
|
|
&input.path,
|
|
&input.old_string,
|
|
&input.new_string,
|
|
input.replace_all.unwrap_or(false),
|
|
)
|
|
.map_err(|error| io_to_string(&error))?,
|
|
)
|
|
}
|
|
|
|
fn run_glob_search(input: &GlobSearchInputValue) -> Result<String, String> {
|
|
to_pretty_json(
|
|
glob_search(&input.pattern, input.path.as_deref()).map_err(|error| io_to_string(&error))?,
|
|
)
|
|
}
|
|
|
|
fn run_grep_search(input: &GrepSearchInput) -> Result<String, String> {
|
|
to_pretty_json(grep_search(input).map_err(|error| io_to_string(&error))?)
|
|
}
|
|
|
|
fn to_pretty_json<T: serde::Serialize>(value: T) -> Result<String, String> {
|
|
serde_json::to_string_pretty(&value).map_err(|error| error.to_string())
|
|
}
|
|
|
|
fn io_to_string(error: &std::io::Error) -> String {
|
|
error.to_string()
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct ReadFileInput {
|
|
path: String,
|
|
offset: Option<usize>,
|
|
limit: Option<usize>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct WriteFileInput {
|
|
path: String,
|
|
content: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct EditFileInput {
|
|
path: String,
|
|
old_string: String,
|
|
new_string: String,
|
|
replace_all: Option<bool>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct GlobSearchInputValue {
|
|
pattern: String,
|
|
path: Option<String>,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{execute_tool, mvp_tool_specs};
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn exposes_mvp_tools() {
|
|
let names = mvp_tool_specs()
|
|
.into_iter()
|
|
.map(|spec| spec.name)
|
|
.collect::<Vec<_>>();
|
|
assert!(names.contains(&"bash"));
|
|
assert!(names.contains(&"read_file"));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_unknown_tool_names() {
|
|
let error = execute_tool("nope", &json!({})).expect_err("tool should be rejected");
|
|
assert!(error.contains("unsupported tool"));
|
|
}
|
|
}
|