mirror of
https://github.com/lWolvesl/claw-code.git
synced 2026-04-03 00:51:52 +08:00
This adds token and estimated cost reporting to runtime usage tracking and surfaces it in the CLI status and turn output. It also upgrades compaction summaries so users see a clearer resumable summary and token savings after /compact. The verification path required cleaning existing workspace clippy and test friction in adjacent crates so cargo fmt, cargo clippy -D warnings, and cargo test succeed from the Rust workspace root in this repo state. Constraint: Keep the change incremental and user-visible without a large CLI rewrite Constraint: Verification must pass with cargo fmt, cargo clippy --all-targets --all-features -- -D warnings, and cargo test Rejected: Implement a full model-pricing table now | would add more surface area than needed for this first UX slice Confidence: high Scope-risk: moderate Reversibility: clean Directive: If pricing becomes model-specific later, keep the current estimate labeling explicit rather than implying exact billing Tested: cargo fmt; cargo clippy --all-targets --all-features -- -D warnings; cargo test -q Not-tested: Live Anthropic API interaction and real streaming terminal sessions
257 lines
8.0 KiB
Rust
257 lines
8.0 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| error.to_string())?,
|
|
)
|
|
}
|
|
|
|
fn run_write_file(input: &WriteFileInput) -> Result<String, String> {
|
|
to_pretty_json(write_file(&input.path, &input.content).map_err(|error| error.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(|error| error.to_string())?,
|
|
)
|
|
}
|
|
|
|
fn run_glob_search(input: &GlobSearchInputValue) -> Result<String, String> {
|
|
to_pretty_json(
|
|
glob_search(&input.pattern, input.path.as_deref()).map_err(|error| error.to_string())?,
|
|
)
|
|
}
|
|
|
|
fn run_grep_search(input: &GrepSearchInput) -> Result<String, String> {
|
|
to_pretty_json(grep_search(input).map_err(|error| error.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())
|
|
}
|
|
|
|
#[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"));
|
|
}
|
|
}
|