mirror of
https://github.com/lWolvesl/claw-code.git
synced 2026-04-02 20:21:51 +08:00
feat: make rusty-claude-cli usable end-to-end
Wire the CLI to the Anthropic client, runtime conversation loop, and MVP in-tree tool executor so prompt mode and the default REPL both execute real turns instead of scaffold-only commands. Constraint: Proxy auth uses ANTHROPIC_AUTH_TOKEN as the primary x-api-key source and may stream extra usage fields Constraint: Must preserve existing scaffold commands while enabling real prompt and REPL flows Rejected: Keep prompt mode on the old scaffold path | does not satisfy end-to-end CLI requirement Rejected: Depend solely on raw SSE message_stop from proxy | proxy/event differences required tolerant parsing plus fallback handling Confidence: medium Scope-risk: moderate Reversibility: clean Directive: Keep prompt mode tool-free unless the one-shot path is explicitly expanded and reverified against the proxy Tested: cargo test -p api; cargo test -p tools; cargo test -p runtime; cargo test -p rusty-claude-cli; cargo build; cargo run -p rusty-claude-cli -- prompt "say hello"; printf '/quit\n' | cargo run -p rusty-claude-cli -- Not-tested: Full interactive tool_use roundtrip against the proxy in REPL mode
This commit is contained in:
@@ -1,3 +1,10 @@
|
||||
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,
|
||||
@@ -26,3 +33,218 @@ impl ToolRegistry {
|
||||
&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(run_read_file),
|
||||
"write_file" => from_value::<WriteFileInput>(input).and_then(run_write_file),
|
||||
"edit_file" => from_value::<EditFileInput>(input).and_then(run_edit_file),
|
||||
"glob_search" => from_value::<GlobSearchInputValue>(input).and_then(run_glob_search),
|
||||
"grep_search" => from_value::<GrepSearchInput>(input).and_then(run_grep_search),
|
||||
_ => 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(io_to_string)?)
|
||||
}
|
||||
|
||||
fn run_write_file(input: WriteFileInput) -> Result<String, String> {
|
||||
to_pretty_json(write_file(&input.path, &input.content).map_err(io_to_string)?)
|
||||
}
|
||||
|
||||
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(io_to_string)?,
|
||||
)
|
||||
}
|
||||
|
||||
fn run_glob_search(input: GlobSearchInputValue) -> Result<String, String> {
|
||||
to_pretty_json(glob_search(&input.pattern, input.path.as_deref()).map_err(io_to_string)?)
|
||||
}
|
||||
|
||||
fn run_grep_search(input: GrepSearchInput) -> Result<String, String> {
|
||||
to_pretty_json(grep_search(&input).map_err(io_to_string)?)
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user