mirror of
https://github.com/lWolvesl/claw-code.git
synced 2026-04-02 20:31:51 +08:00
Merge remote-tracking branch 'origin/rcc/cli' into dev/rust
This commit is contained in:
@@ -132,7 +132,9 @@ cargo run -p rusty-claude-cli -- --resume session.json /memory /config
|
|||||||
- `bootstrap-plan` — print the current bootstrap skeleton
|
- `bootstrap-plan` — print the current bootstrap skeleton
|
||||||
- `system-prompt [--cwd PATH] [--date YYYY-MM-DD]` — render the synthesized system prompt
|
- `system-prompt [--cwd PATH] [--date YYYY-MM-DD]` — render the synthesized system prompt
|
||||||
- `--help` / `-h` — show CLI help
|
- `--help` / `-h` — show CLI help
|
||||||
- `--version` / `-V` — print the CLI version
|
- `--version` / `-V` — print the CLI version and build info locally (no API call)
|
||||||
|
- `--output-format text|json` — choose non-interactive prompt output rendering
|
||||||
|
- `--allowedTools <tool[,tool...]>` — restrict enabled tools for interactive sessions and prompt-mode tool use
|
||||||
|
|
||||||
### Interactive slash commands
|
### Interactive slash commands
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
mod input;
|
mod input;
|
||||||
mod render;
|
mod render;
|
||||||
|
|
||||||
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
@@ -32,6 +33,8 @@ const VERSION: &str = env!("CARGO_PKG_VERSION");
|
|||||||
const BUILD_TARGET: Option<&str> = option_env!("TARGET");
|
const BUILD_TARGET: Option<&str> = option_env!("TARGET");
|
||||||
const GIT_SHA: Option<&str> = option_env!("GIT_SHA");
|
const GIT_SHA: Option<&str> = option_env!("GIT_SHA");
|
||||||
|
|
||||||
|
type AllowedToolSet = BTreeSet<String>;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
if let Err(error) = run() {
|
if let Err(error) = run() {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
@@ -49,6 +52,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
CliAction::DumpManifests => dump_manifests(),
|
CliAction::DumpManifests => dump_manifests(),
|
||||||
CliAction::BootstrapPlan => print_bootstrap_plan(),
|
CliAction::BootstrapPlan => print_bootstrap_plan(),
|
||||||
CliAction::PrintSystemPrompt { cwd, date } => print_system_prompt(cwd, date),
|
CliAction::PrintSystemPrompt { cwd, date } => print_system_prompt(cwd, date),
|
||||||
|
CliAction::Version => print_version(),
|
||||||
CliAction::ResumeSession {
|
CliAction::ResumeSession {
|
||||||
session_path,
|
session_path,
|
||||||
commands,
|
commands,
|
||||||
@@ -57,8 +61,13 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
prompt,
|
prompt,
|
||||||
model,
|
model,
|
||||||
output_format,
|
output_format,
|
||||||
} => LiveCli::new(model, false)?.run_turn_with_output(&prompt, output_format)?,
|
allowed_tools,
|
||||||
CliAction::Repl { model } => run_repl(model)?,
|
} => LiveCli::new(model, false, allowed_tools)?
|
||||||
|
.run_turn_with_output(&prompt, output_format)?,
|
||||||
|
CliAction::Repl {
|
||||||
|
model,
|
||||||
|
allowed_tools,
|
||||||
|
} => run_repl(model, allowed_tools)?,
|
||||||
CliAction::Help => print_help(),
|
CliAction::Help => print_help(),
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -72,6 +81,7 @@ enum CliAction {
|
|||||||
cwd: PathBuf,
|
cwd: PathBuf,
|
||||||
date: String,
|
date: String,
|
||||||
},
|
},
|
||||||
|
Version,
|
||||||
ResumeSession {
|
ResumeSession {
|
||||||
session_path: PathBuf,
|
session_path: PathBuf,
|
||||||
commands: Vec<String>,
|
commands: Vec<String>,
|
||||||
@@ -80,9 +90,11 @@ enum CliAction {
|
|||||||
prompt: String,
|
prompt: String,
|
||||||
model: String,
|
model: String,
|
||||||
output_format: CliOutputFormat,
|
output_format: CliOutputFormat,
|
||||||
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
},
|
},
|
||||||
Repl {
|
Repl {
|
||||||
model: String,
|
model: String,
|
||||||
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
},
|
},
|
||||||
// prompt-mode formatting is only supported for non-interactive runs
|
// prompt-mode formatting is only supported for non-interactive runs
|
||||||
Help,
|
Help,
|
||||||
@@ -109,11 +121,17 @@ impl CliOutputFormat {
|
|||||||
fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||||
let mut model = DEFAULT_MODEL.to_string();
|
let mut model = DEFAULT_MODEL.to_string();
|
||||||
let mut output_format = CliOutputFormat::Text;
|
let mut output_format = CliOutputFormat::Text;
|
||||||
|
let mut wants_version = false;
|
||||||
|
let mut allowed_tool_values = Vec::new();
|
||||||
let mut rest = Vec::new();
|
let mut rest = Vec::new();
|
||||||
let mut index = 0;
|
let mut index = 0;
|
||||||
|
|
||||||
while index < args.len() {
|
while index < args.len() {
|
||||||
match args[index].as_str() {
|
match args[index].as_str() {
|
||||||
|
"--version" | "-V" => {
|
||||||
|
wants_version = true;
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
"--model" => {
|
"--model" => {
|
||||||
let value = args
|
let value = args
|
||||||
.get(index + 1)
|
.get(index + 1)
|
||||||
@@ -136,6 +154,21 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
output_format = CliOutputFormat::parse(&flag[16..])?;
|
output_format = CliOutputFormat::parse(&flag[16..])?;
|
||||||
index += 1;
|
index += 1;
|
||||||
}
|
}
|
||||||
|
"--allowedTools" | "--allowed-tools" => {
|
||||||
|
let value = args
|
||||||
|
.get(index + 1)
|
||||||
|
.ok_or_else(|| "missing value for --allowedTools".to_string())?;
|
||||||
|
allowed_tool_values.push(value.clone());
|
||||||
|
index += 2;
|
||||||
|
}
|
||||||
|
flag if flag.starts_with("--allowedTools=") => {
|
||||||
|
allowed_tool_values.push(flag[15..].to_string());
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
flag if flag.starts_with("--allowed-tools=") => {
|
||||||
|
allowed_tool_values.push(flag[16..].to_string());
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
other => {
|
other => {
|
||||||
rest.push(other.to_string());
|
rest.push(other.to_string());
|
||||||
index += 1;
|
index += 1;
|
||||||
@@ -143,8 +176,17 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if wants_version {
|
||||||
|
return Ok(CliAction::Version);
|
||||||
|
}
|
||||||
|
|
||||||
|
let allowed_tools = normalize_allowed_tools(&allowed_tool_values)?;
|
||||||
|
|
||||||
if rest.is_empty() {
|
if rest.is_empty() {
|
||||||
return Ok(CliAction::Repl { model });
|
return Ok(CliAction::Repl {
|
||||||
|
model,
|
||||||
|
allowed_tools,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if matches!(rest.first().map(String::as_str), Some("--help" | "-h")) {
|
if matches!(rest.first().map(String::as_str), Some("--help" | "-h")) {
|
||||||
return Ok(CliAction::Help);
|
return Ok(CliAction::Help);
|
||||||
@@ -166,17 +208,74 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
prompt,
|
prompt,
|
||||||
model,
|
model,
|
||||||
output_format,
|
output_format,
|
||||||
|
allowed_tools,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
other if !other.starts_with('/') => Ok(CliAction::Prompt {
|
other if !other.starts_with('/') => Ok(CliAction::Prompt {
|
||||||
prompt: rest.join(" "),
|
prompt: rest.join(" "),
|
||||||
model,
|
model,
|
||||||
output_format,
|
output_format,
|
||||||
|
allowed_tools,
|
||||||
}),
|
}),
|
||||||
other => Err(format!("unknown subcommand: {other}")),
|
other => Err(format!("unknown subcommand: {other}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn normalize_allowed_tools(values: &[String]) -> Result<Option<AllowedToolSet>, String> {
|
||||||
|
if values.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let canonical_names = mvp_tool_specs()
|
||||||
|
.into_iter()
|
||||||
|
.map(|spec| spec.name.to_string())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let mut name_map = canonical_names
|
||||||
|
.iter()
|
||||||
|
.map(|name| (normalize_tool_name(name), name.clone()))
|
||||||
|
.collect::<BTreeMap<_, _>>();
|
||||||
|
|
||||||
|
for (alias, canonical) in [
|
||||||
|
("read", "read_file"),
|
||||||
|
("write", "write_file"),
|
||||||
|
("edit", "edit_file"),
|
||||||
|
("glob", "glob_search"),
|
||||||
|
("grep", "grep_search"),
|
||||||
|
] {
|
||||||
|
name_map.insert(alias.to_string(), canonical.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut allowed = AllowedToolSet::new();
|
||||||
|
for value in values {
|
||||||
|
for token in value
|
||||||
|
.split(|ch: char| ch == ',' || ch.is_whitespace())
|
||||||
|
.filter(|token| !token.is_empty())
|
||||||
|
{
|
||||||
|
let normalized = normalize_tool_name(token);
|
||||||
|
let canonical = name_map.get(&normalized).ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"unsupported tool in --allowedTools: {token} (expected one of: {})",
|
||||||
|
canonical_names.join(", ")
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
allowed.insert(canonical.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(allowed))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_tool_name(value: &str) -> String {
|
||||||
|
value.trim().replace('-', "_").to_ascii_lowercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filter_tool_specs(allowed_tools: Option<&AllowedToolSet>) -> Vec<tools::ToolSpec> {
|
||||||
|
mvp_tool_specs()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|spec| allowed_tools.is_none_or(|allowed| allowed.contains(spec.name)))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_system_prompt_args(args: &[String]) -> Result<CliAction, String> {
|
fn parse_system_prompt_args(args: &[String]) -> Result<CliAction, String> {
|
||||||
let mut cwd = env::current_dir().map_err(|error| error.to_string())?;
|
let mut cwd = env::current_dir().map_err(|error| error.to_string())?;
|
||||||
let mut date = DEFAULT_DATE.to_string();
|
let mut date = DEFAULT_DATE.to_string();
|
||||||
@@ -255,6 +354,10 @@ fn print_system_prompt(cwd: PathBuf, date: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn print_version() {
|
||||||
|
println!("{}", render_version_report());
|
||||||
|
}
|
||||||
|
|
||||||
fn resume_session(session_path: &Path, commands: &[String]) {
|
fn resume_session(session_path: &Path, commands: &[String]) {
|
||||||
let session = match Session::load_from_path(session_path) {
|
let session = match Session::load_from_path(session_path) {
|
||||||
Ok(session) => session,
|
Ok(session) => session,
|
||||||
@@ -608,8 +711,11 @@ fn run_resume_command(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_repl(model: String) -> Result<(), Box<dyn std::error::Error>> {
|
fn run_repl(
|
||||||
let mut cli = LiveCli::new(model, true)?;
|
model: String,
|
||||||
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut cli = LiveCli::new(model, true, allowed_tools)?;
|
||||||
let editor = input::LineEditor::new("› ");
|
let editor = input::LineEditor::new("› ");
|
||||||
println!("{}", cli.startup_banner());
|
println!("{}", cli.startup_banner());
|
||||||
|
|
||||||
@@ -647,13 +753,18 @@ struct ManagedSessionSummary {
|
|||||||
|
|
||||||
struct LiveCli {
|
struct LiveCli {
|
||||||
model: String,
|
model: String,
|
||||||
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
system_prompt: Vec<String>,
|
system_prompt: Vec<String>,
|
||||||
runtime: ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>,
|
runtime: ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>,
|
||||||
session: SessionHandle,
|
session: SessionHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LiveCli {
|
impl LiveCli {
|
||||||
fn new(model: String, enable_tools: bool) -> Result<Self, Box<dyn std::error::Error>> {
|
fn new(
|
||||||
|
model: String,
|
||||||
|
enable_tools: bool,
|
||||||
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
|
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
let system_prompt = build_system_prompt()?;
|
let system_prompt = build_system_prompt()?;
|
||||||
let session = create_managed_session_handle()?;
|
let session = create_managed_session_handle()?;
|
||||||
let runtime = build_runtime(
|
let runtime = build_runtime(
|
||||||
@@ -661,9 +772,11 @@ impl LiveCli {
|
|||||||
model.clone(),
|
model.clone(),
|
||||||
system_prompt.clone(),
|
system_prompt.clone(),
|
||||||
enable_tools,
|
enable_tools,
|
||||||
|
allowed_tools.clone(),
|
||||||
)?;
|
)?;
|
||||||
let cli = Self {
|
let cli = Self {
|
||||||
model,
|
model,
|
||||||
|
allowed_tools,
|
||||||
system_prompt,
|
system_prompt,
|
||||||
runtime,
|
runtime,
|
||||||
session,
|
session,
|
||||||
@@ -849,7 +962,13 @@ impl LiveCli {
|
|||||||
let previous = self.model.clone();
|
let previous = self.model.clone();
|
||||||
let session = self.runtime.session().clone();
|
let session = self.runtime.session().clone();
|
||||||
let message_count = session.messages.len();
|
let message_count = session.messages.len();
|
||||||
self.runtime = build_runtime(session, model.clone(), self.system_prompt.clone(), true)?;
|
self.runtime = build_runtime(
|
||||||
|
session,
|
||||||
|
model.clone(),
|
||||||
|
self.system_prompt.clone(),
|
||||||
|
true,
|
||||||
|
self.allowed_tools.clone(),
|
||||||
|
)?;
|
||||||
self.model.clone_from(&model);
|
self.model.clone_from(&model);
|
||||||
self.persist_session()?;
|
self.persist_session()?;
|
||||||
println!(
|
println!(
|
||||||
@@ -883,6 +1002,7 @@ impl LiveCli {
|
|||||||
self.model.clone(),
|
self.model.clone(),
|
||||||
self.system_prompt.clone(),
|
self.system_prompt.clone(),
|
||||||
true,
|
true,
|
||||||
|
self.allowed_tools.clone(),
|
||||||
normalized,
|
normalized,
|
||||||
)?;
|
)?;
|
||||||
self.persist_session()?;
|
self.persist_session()?;
|
||||||
@@ -907,6 +1027,7 @@ impl LiveCli {
|
|||||||
self.model.clone(),
|
self.model.clone(),
|
||||||
self.system_prompt.clone(),
|
self.system_prompt.clone(),
|
||||||
true,
|
true,
|
||||||
|
self.allowed_tools.clone(),
|
||||||
permission_mode_label(),
|
permission_mode_label(),
|
||||||
)?;
|
)?;
|
||||||
self.persist_session()?;
|
self.persist_session()?;
|
||||||
@@ -941,6 +1062,7 @@ impl LiveCli {
|
|||||||
self.model.clone(),
|
self.model.clone(),
|
||||||
self.system_prompt.clone(),
|
self.system_prompt.clone(),
|
||||||
true,
|
true,
|
||||||
|
self.allowed_tools.clone(),
|
||||||
permission_mode_label(),
|
permission_mode_label(),
|
||||||
)?;
|
)?;
|
||||||
self.session = handle;
|
self.session = handle;
|
||||||
@@ -1017,6 +1139,7 @@ impl LiveCli {
|
|||||||
self.model.clone(),
|
self.model.clone(),
|
||||||
self.system_prompt.clone(),
|
self.system_prompt.clone(),
|
||||||
true,
|
true,
|
||||||
|
self.allowed_tools.clone(),
|
||||||
permission_mode_label(),
|
permission_mode_label(),
|
||||||
)?;
|
)?;
|
||||||
self.session = handle;
|
self.session = handle;
|
||||||
@@ -1046,6 +1169,7 @@ impl LiveCli {
|
|||||||
self.model.clone(),
|
self.model.clone(),
|
||||||
self.system_prompt.clone(),
|
self.system_prompt.clone(),
|
||||||
true,
|
true,
|
||||||
|
self.allowed_tools.clone(),
|
||||||
permission_mode_label(),
|
permission_mode_label(),
|
||||||
)?;
|
)?;
|
||||||
self.persist_session()?;
|
self.persist_session()?;
|
||||||
@@ -1571,6 +1695,7 @@ fn build_runtime(
|
|||||||
model: String,
|
model: String,
|
||||||
system_prompt: Vec<String>,
|
system_prompt: Vec<String>,
|
||||||
enable_tools: bool,
|
enable_tools: bool,
|
||||||
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
) -> Result<ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>, Box<dyn std::error::Error>>
|
) -> Result<ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>, Box<dyn std::error::Error>>
|
||||||
{
|
{
|
||||||
build_runtime_with_permission_mode(
|
build_runtime_with_permission_mode(
|
||||||
@@ -1578,6 +1703,7 @@ fn build_runtime(
|
|||||||
model,
|
model,
|
||||||
system_prompt,
|
system_prompt,
|
||||||
enable_tools,
|
enable_tools,
|
||||||
|
allowed_tools,
|
||||||
permission_mode_label(),
|
permission_mode_label(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1587,13 +1713,14 @@ fn build_runtime_with_permission_mode(
|
|||||||
model: String,
|
model: String,
|
||||||
system_prompt: Vec<String>,
|
system_prompt: Vec<String>,
|
||||||
enable_tools: bool,
|
enable_tools: bool,
|
||||||
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
permission_mode: &str,
|
permission_mode: &str,
|
||||||
) -> Result<ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>, Box<dyn std::error::Error>>
|
) -> Result<ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>, Box<dyn std::error::Error>>
|
||||||
{
|
{
|
||||||
Ok(ConversationRuntime::new(
|
Ok(ConversationRuntime::new(
|
||||||
session,
|
session,
|
||||||
AnthropicRuntimeClient::new(model, enable_tools)?,
|
AnthropicRuntimeClient::new(model, enable_tools, allowed_tools.clone())?,
|
||||||
CliToolExecutor::new(),
|
CliToolExecutor::new(allowed_tools),
|
||||||
permission_policy(permission_mode),
|
permission_policy(permission_mode),
|
||||||
system_prompt,
|
system_prompt,
|
||||||
))
|
))
|
||||||
@@ -1604,15 +1731,21 @@ struct AnthropicRuntimeClient {
|
|||||||
client: AnthropicClient,
|
client: AnthropicClient,
|
||||||
model: String,
|
model: String,
|
||||||
enable_tools: bool,
|
enable_tools: bool,
|
||||||
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AnthropicRuntimeClient {
|
impl AnthropicRuntimeClient {
|
||||||
fn new(model: String, enable_tools: bool) -> Result<Self, Box<dyn std::error::Error>> {
|
fn new(
|
||||||
|
model: String,
|
||||||
|
enable_tools: bool,
|
||||||
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
|
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
runtime: tokio::runtime::Runtime::new()?,
|
runtime: tokio::runtime::Runtime::new()?,
|
||||||
client: AnthropicClient::from_env()?,
|
client: AnthropicClient::from_env()?,
|
||||||
model,
|
model,
|
||||||
enable_tools,
|
enable_tools,
|
||||||
|
allowed_tools,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1626,7 +1759,7 @@ impl ApiClient for AnthropicRuntimeClient {
|
|||||||
messages: convert_messages(&request.messages),
|
messages: convert_messages(&request.messages),
|
||||||
system: (!request.system_prompt.is_empty()).then(|| request.system_prompt.join("\n\n")),
|
system: (!request.system_prompt.is_empty()).then(|| request.system_prompt.join("\n\n")),
|
||||||
tools: self.enable_tools.then(|| {
|
tools: self.enable_tools.then(|| {
|
||||||
mvp_tool_specs()
|
filter_tool_specs(self.allowed_tools.as_ref())
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|spec| ToolDefinition {
|
.map(|spec| ToolDefinition {
|
||||||
name: spec.name.to_string(),
|
name: spec.name.to_string(),
|
||||||
@@ -1781,18 +1914,29 @@ fn response_to_events(
|
|||||||
|
|
||||||
struct CliToolExecutor {
|
struct CliToolExecutor {
|
||||||
renderer: TerminalRenderer,
|
renderer: TerminalRenderer,
|
||||||
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CliToolExecutor {
|
impl CliToolExecutor {
|
||||||
fn new() -> Self {
|
fn new(allowed_tools: Option<AllowedToolSet>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
renderer: TerminalRenderer::new(),
|
renderer: TerminalRenderer::new(),
|
||||||
|
allowed_tools,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToolExecutor for CliToolExecutor {
|
impl ToolExecutor for CliToolExecutor {
|
||||||
fn execute(&mut self, tool_name: &str, input: &str) -> Result<String, ToolError> {
|
fn execute(&mut self, tool_name: &str, input: &str) -> Result<String, ToolError> {
|
||||||
|
if self
|
||||||
|
.allowed_tools
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|allowed| !allowed.contains(tool_name))
|
||||||
|
{
|
||||||
|
return Err(ToolError::new(format!(
|
||||||
|
"tool `{tool_name}` is not enabled by the current --allowedTools setting"
|
||||||
|
)));
|
||||||
|
}
|
||||||
let value = serde_json::from_str(input)
|
let value = serde_json::from_str(input)
|
||||||
.map_err(|error| ToolError::new(format!("invalid tool input JSON: {error}")))?;
|
.map_err(|error| ToolError::new(format!("invalid tool input JSON: {error}")))?;
|
||||||
match execute_tool(tool_name, &value) {
|
match execute_tool(tool_name, &value) {
|
||||||
@@ -1864,7 +2008,7 @@ fn print_help() {
|
|||||||
println!("rusty-claude-cli v{VERSION}");
|
println!("rusty-claude-cli v{VERSION}");
|
||||||
println!();
|
println!();
|
||||||
println!("Usage:");
|
println!("Usage:");
|
||||||
println!(" rusty-claude-cli [--model MODEL]");
|
println!(" rusty-claude-cli [--model MODEL] [--allowedTools TOOL[,TOOL...]]");
|
||||||
println!(" Start the interactive REPL");
|
println!(" Start the interactive REPL");
|
||||||
println!(" rusty-claude-cli [--model MODEL] [--output-format text|json] prompt TEXT");
|
println!(" rusty-claude-cli [--model MODEL] [--output-format text|json] prompt TEXT");
|
||||||
println!(" Send one prompt and exit");
|
println!(" Send one prompt and exit");
|
||||||
@@ -1879,6 +2023,8 @@ fn print_help() {
|
|||||||
println!("Flags:");
|
println!("Flags:");
|
||||||
println!(" --model MODEL Override the active model");
|
println!(" --model MODEL Override the active model");
|
||||||
println!(" --output-format FORMAT Non-interactive output format: text or json");
|
println!(" --output-format FORMAT Non-interactive output format: text or json");
|
||||||
|
println!(" --allowedTools TOOLS Restrict enabled tools (repeatable; comma-separated aliases supported)");
|
||||||
|
println!(" --version, -V Print version and build information locally");
|
||||||
println!();
|
println!();
|
||||||
println!("Interactive slash commands:");
|
println!("Interactive slash commands:");
|
||||||
println!("{}", render_slash_command_help());
|
println!("{}", render_slash_command_help());
|
||||||
@@ -1895,18 +2041,20 @@ fn print_help() {
|
|||||||
println!("Examples:");
|
println!("Examples:");
|
||||||
println!(" rusty-claude-cli --model claude-opus \"summarize this repo\"");
|
println!(" rusty-claude-cli --model claude-opus \"summarize this repo\"");
|
||||||
println!(" rusty-claude-cli --output-format json prompt \"explain src/main.rs\"");
|
println!(" rusty-claude-cli --output-format json prompt \"explain src/main.rs\"");
|
||||||
|
println!(" rusty-claude-cli --allowedTools read,glob \"summarize Cargo.toml\"");
|
||||||
println!(" rusty-claude-cli --resume session.json /status /diff /export notes.txt");
|
println!(" rusty-claude-cli --resume session.json /status /diff /export notes.txt");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
format_compact_report, format_cost_report, format_init_report, format_model_report,
|
filter_tool_specs, format_compact_report, format_cost_report, format_init_report,
|
||||||
format_model_switch_report, format_permissions_report, format_permissions_switch_report,
|
format_model_report, format_model_switch_report, format_permissions_report,
|
||||||
format_resume_report, format_status_report, normalize_permission_mode, parse_args,
|
format_permissions_switch_report, format_resume_report, format_status_report,
|
||||||
parse_git_status_metadata, render_config_report, render_init_claude_md,
|
normalize_permission_mode, parse_args, parse_git_status_metadata, render_config_report,
|
||||||
render_memory_report, render_repl_help, resume_supported_slash_commands, status_context,
|
render_init_claude_md, render_memory_report, render_repl_help,
|
||||||
CliAction, CliOutputFormat, SlashCommand, StatusUsage, DEFAULT_MODEL,
|
resume_supported_slash_commands, status_context, CliAction, CliOutputFormat, SlashCommand,
|
||||||
|
StatusUsage, DEFAULT_MODEL,
|
||||||
};
|
};
|
||||||
use runtime::{ContentBlock, ConversationMessage, MessageRole};
|
use runtime::{ContentBlock, ConversationMessage, MessageRole};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -1917,6 +2065,7 @@ mod tests {
|
|||||||
parse_args(&[]).expect("args should parse"),
|
parse_args(&[]).expect("args should parse"),
|
||||||
CliAction::Repl {
|
CliAction::Repl {
|
||||||
model: DEFAULT_MODEL.to_string(),
|
model: DEFAULT_MODEL.to_string(),
|
||||||
|
allowed_tools: None,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1934,6 +2083,7 @@ mod tests {
|
|||||||
prompt: "hello world".to_string(),
|
prompt: "hello world".to_string(),
|
||||||
model: DEFAULT_MODEL.to_string(),
|
model: DEFAULT_MODEL.to_string(),
|
||||||
output_format: CliOutputFormat::Text,
|
output_format: CliOutputFormat::Text,
|
||||||
|
allowed_tools: None,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1953,10 +2103,51 @@ mod tests {
|
|||||||
prompt: "explain this".to_string(),
|
prompt: "explain this".to_string(),
|
||||||
model: "claude-opus".to_string(),
|
model: "claude-opus".to_string(),
|
||||||
output_format: CliOutputFormat::Json,
|
output_format: CliOutputFormat::Json,
|
||||||
|
allowed_tools: None,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_version_flags_without_initializing_prompt_mode() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_args(&["--version".to_string()]).expect("args should parse"),
|
||||||
|
CliAction::Version
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_args(&["-V".to_string()]).expect("args should parse"),
|
||||||
|
CliAction::Version
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_allowed_tools_flags_with_aliases_and_lists() {
|
||||||
|
let args = vec![
|
||||||
|
"--allowedTools".to_string(),
|
||||||
|
"read,glob".to_string(),
|
||||||
|
"--allowed-tools=write_file".to_string(),
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
parse_args(&args).expect("args should parse"),
|
||||||
|
CliAction::Repl {
|
||||||
|
model: DEFAULT_MODEL.to_string(),
|
||||||
|
allowed_tools: Some(
|
||||||
|
["glob_search", "read_file", "write_file"]
|
||||||
|
.into_iter()
|
||||||
|
.map(str::to_string)
|
||||||
|
.collect()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_unknown_allowed_tools() {
|
||||||
|
let error = parse_args(&["--allowedTools".to_string(), "teleport".to_string()])
|
||||||
|
.expect_err("tool should be rejected");
|
||||||
|
assert!(error.contains("unsupported tool in --allowedTools: teleport"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_system_prompt_options() {
|
fn parses_system_prompt_options() {
|
||||||
let args = vec![
|
let args = vec![
|
||||||
@@ -2013,6 +2204,20 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn filtered_tool_specs_respect_allowlist() {
|
||||||
|
let allowed = ["read_file", "grep_search"]
|
||||||
|
.into_iter()
|
||||||
|
.map(str::to_string)
|
||||||
|
.collect();
|
||||||
|
let filtered = filter_tool_specs(Some(&allowed));
|
||||||
|
let names = filtered
|
||||||
|
.into_iter()
|
||||||
|
.map(|spec| spec.name)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(names, vec!["read_file", "grep_search"]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn shared_help_uses_resume_annotation_copy() {
|
fn shared_help_uses_resume_annotation_copy() {
|
||||||
let help = commands::render_slash_command_help();
|
let help = commands::render_slash_command_help();
|
||||||
|
|||||||
Reference in New Issue
Block a user