mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-06 08:04:50 +08:00
Complete local claw-first CLI and config surface alignment
This commit is contained in:
@@ -3347,8 +3347,8 @@ fn render_skills_usage_json(unexpected: Option<&str>) -> Value {
|
||||
"usage": {
|
||||
"slash_command": "/skills [list|install <path>|help]",
|
||||
"direct_cli": "claw skills [list|install <path>|help]",
|
||||
"install_root": "$CODEX_HOME/skills or ~/.codex/skills",
|
||||
"sources": [".codex/skills", ".claude/skills", "legacy /commands"],
|
||||
"install_root": "$CLAW_CONFIG_HOME/skills or ~/.claw/skills",
|
||||
"sources": [".claw/skills", "legacy /commands", "legacy fallback dirs still load automatically"],
|
||||
},
|
||||
"unexpected": unexpected,
|
||||
})
|
||||
@@ -3458,11 +3458,15 @@ fn format_mcp_oauth(oauth: Option<&McpOAuthConfig>) -> String {
|
||||
|
||||
fn definition_source_id(source: DefinitionSource) -> &'static str {
|
||||
match source {
|
||||
DefinitionSource::ProjectCodex => "project_codex",
|
||||
DefinitionSource::ProjectClaude => "project_claude",
|
||||
DefinitionSource::UserCodexHome => "user_codex_home",
|
||||
DefinitionSource::UserCodex => "user_codex",
|
||||
DefinitionSource::UserClaude => "user_claude",
|
||||
DefinitionSource::ProjectClaw
|
||||
| DefinitionSource::ProjectCodex
|
||||
| DefinitionSource::ProjectClaude => "project_claw",
|
||||
DefinitionSource::UserClawConfigHome | DefinitionSource::UserCodexHome => {
|
||||
"user_claw_config_home"
|
||||
}
|
||||
DefinitionSource::UserClaw | DefinitionSource::UserCodex | DefinitionSource::UserClaude => {
|
||||
"user_claw"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4442,10 +4446,10 @@ mod tests {
|
||||
assert_eq!(report["summary"]["active"], 3);
|
||||
assert_eq!(report["summary"]["shadowed"], 1);
|
||||
assert_eq!(report["skills"][0]["name"], "plan");
|
||||
assert_eq!(report["skills"][0]["source"]["id"], "project_codex");
|
||||
assert_eq!(report["skills"][0]["source"]["id"], "project_claw");
|
||||
assert_eq!(report["skills"][1]["name"], "deploy");
|
||||
assert_eq!(report["skills"][1]["origin"]["id"], "legacy_commands_dir");
|
||||
assert_eq!(report["skills"][3]["shadowed_by"]["id"], "project_codex");
|
||||
assert_eq!(report["skills"][3]["shadowed_by"]["id"], "project_claw");
|
||||
|
||||
let help = handle_skills_slash_command_json(Some("help"), &workspace).expect("skills help");
|
||||
assert_eq!(help["kind"], "skills");
|
||||
|
||||
@@ -31,14 +31,16 @@ pub mod recovery_recipes;
|
||||
mod remote;
|
||||
pub mod sandbox;
|
||||
mod session;
|
||||
pub mod session_control;
|
||||
#[cfg(test)]
|
||||
mod session_control;
|
||||
mod sse;
|
||||
pub mod stale_branch;
|
||||
pub mod summary_compression;
|
||||
pub mod task_packet;
|
||||
pub mod task_registry;
|
||||
pub mod team_cron_registry;
|
||||
pub mod trust_resolver;
|
||||
#[cfg(test)]
|
||||
mod trust_resolver;
|
||||
mod usage;
|
||||
pub mod worker_boot;
|
||||
|
||||
@@ -141,6 +143,7 @@ pub use stale_branch::{
|
||||
StaleBranchPolicy,
|
||||
};
|
||||
pub use task_packet::{validate_packet, TaskPacket, TaskPacketValidationError, ValidatedPacket};
|
||||
#[cfg(test)]
|
||||
pub use trust_resolver::{TrustConfig, TrustDecision, TrustEvent, TrustPolicy, TrustResolver};
|
||||
pub use usage::{
|
||||
format_usd, pricing_for_model, ModelPricing, TokenUsage, UsageCostEstimate, UsageTracker,
|
||||
|
||||
@@ -129,8 +129,9 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
CliAction::Status {
|
||||
model,
|
||||
permission_mode,
|
||||
} => print_status_snapshot(&model, permission_mode)?,
|
||||
CliAction::Sandbox => print_sandbox_status_snapshot()?,
|
||||
output_format,
|
||||
} => print_status_snapshot(&model, permission_mode, output_format)?,
|
||||
CliAction::Sandbox { output_format } => print_sandbox_status_snapshot(output_format)?,
|
||||
CliAction::Prompt {
|
||||
prompt,
|
||||
model,
|
||||
@@ -181,8 +182,11 @@ enum CliAction {
|
||||
Status {
|
||||
model: String,
|
||||
permission_mode: PermissionMode,
|
||||
output_format: CliOutputFormat,
|
||||
},
|
||||
Sandbox {
|
||||
output_format: CliOutputFormat,
|
||||
},
|
||||
Sandbox,
|
||||
Prompt {
|
||||
prompt: String,
|
||||
model: String,
|
||||
@@ -365,7 +369,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||
if let Some(action) = parse_local_help_action(&rest) {
|
||||
return action;
|
||||
}
|
||||
if let Some(action) = parse_single_word_command_alias(&rest, &model, permission_mode_override) {
|
||||
if let Some(action) = parse_single_word_command_alias(&rest, &model, permission_mode_override, output_format) {
|
||||
return action;
|
||||
}
|
||||
|
||||
@@ -435,6 +439,7 @@ fn parse_single_word_command_alias(
|
||||
rest: &[String],
|
||||
model: &str,
|
||||
permission_mode_override: Option<PermissionMode>,
|
||||
output_format: CliOutputFormat,
|
||||
) -> Option<Result<CliAction, String>> {
|
||||
if rest.len() != 1 {
|
||||
return None;
|
||||
@@ -446,8 +451,9 @@ fn parse_single_word_command_alias(
|
||||
"status" => Some(Ok(CliAction::Status {
|
||||
model: model.to_string(),
|
||||
permission_mode: permission_mode_override.unwrap_or_else(default_permission_mode),
|
||||
output_format,
|
||||
})),
|
||||
"sandbox" => Some(Ok(CliAction::Sandbox)),
|
||||
"sandbox" => Some(Ok(CliAction::Sandbox { output_format })),
|
||||
"doctor" => Some(Ok(CliAction::Doctor)),
|
||||
other => bare_slash_command_guidance(other).map(Err),
|
||||
}
|
||||
@@ -1862,13 +1868,7 @@ fn run_resume_command(
|
||||
};
|
||||
Ok(ResumeCommandOutcome {
|
||||
session: session.clone(),
|
||||
message: Some(match output_format {
|
||||
CliOutputFormat::Text => json!({
|
||||
"kind": "mcp",
|
||||
"message": handle_mcp_slash_command(args.as_deref(), &cwd)?,
|
||||
}),
|
||||
CliOutputFormat::Json => handle_mcp_slash_command_json(args.as_deref(), &cwd)?,
|
||||
}),
|
||||
message: Some(handle_mcp_slash_command(args.as_deref(), &cwd)?),
|
||||
})
|
||||
}
|
||||
SlashCommand::Memory => Ok(ResumeCommandOutcome {
|
||||
@@ -1912,15 +1912,7 @@ fn run_resume_command(
|
||||
let cwd = env::current_dir()?;
|
||||
Ok(ResumeCommandOutcome {
|
||||
session: session.clone(),
|
||||
message: Some(match output_format {
|
||||
CliOutputFormat::Text => json!({
|
||||
"kind": "skills",
|
||||
"message": handle_skills_slash_command(args.as_deref(), &cwd)?,
|
||||
}),
|
||||
CliOutputFormat::Json => {
|
||||
handle_skills_slash_command_json(args.as_deref(), &cwd)?
|
||||
}
|
||||
}),
|
||||
message: Some(handle_skills_slash_command(args.as_deref(), &cwd)?),
|
||||
})
|
||||
}
|
||||
SlashCommand::Doctor => Ok(ResumeCommandOutcome {
|
||||
@@ -3136,7 +3128,7 @@ impl LiveCli {
|
||||
CliOutputFormat::Text => println!("{}", handle_mcp_slash_command(args, &cwd)?),
|
||||
CliOutputFormat::Json => println!(
|
||||
"{}",
|
||||
serialize_json_output(&handle_mcp_slash_command_json(args, &cwd)?)?
|
||||
serde_json::to_string_pretty(&handle_mcp_slash_command_json(args, &cwd)?)?
|
||||
),
|
||||
}
|
||||
Ok(())
|
||||
@@ -3151,7 +3143,7 @@ impl LiveCli {
|
||||
CliOutputFormat::Text => println!("{}", handle_skills_slash_command(args, &cwd)?),
|
||||
CliOutputFormat::Json => println!(
|
||||
"{}",
|
||||
serialize_json_output(&handle_skills_slash_command_json(args, &cwd)?)?
|
||||
serde_json::to_string_pretty(&handle_skills_slash_command_json(args, &cwd)?)?
|
||||
),
|
||||
}
|
||||
Ok(())
|
||||
@@ -3659,22 +3651,68 @@ fn render_repl_help() -> String {
|
||||
fn print_status_snapshot(
|
||||
model: &str,
|
||||
permission_mode: PermissionMode,
|
||||
output_format: CliOutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!(
|
||||
"{}",
|
||||
format_status_report(
|
||||
model,
|
||||
StatusUsage {
|
||||
message_count: 0,
|
||||
turns: 0,
|
||||
latest: TokenUsage::default(),
|
||||
cumulative: TokenUsage::default(),
|
||||
estimated_tokens: 0,
|
||||
},
|
||||
permission_mode.as_str(),
|
||||
&status_context(None)?,
|
||||
)
|
||||
);
|
||||
let usage = StatusUsage {
|
||||
message_count: 0,
|
||||
turns: 0,
|
||||
latest: TokenUsage::default(),
|
||||
cumulative: TokenUsage::default(),
|
||||
estimated_tokens: 0,
|
||||
};
|
||||
let context = status_context(None)?;
|
||||
match output_format {
|
||||
CliOutputFormat::Text => println!(
|
||||
"{}",
|
||||
format_status_report(model, usage, permission_mode.as_str(), &context)
|
||||
),
|
||||
CliOutputFormat::Json => println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&json!({
|
||||
"kind": "status",
|
||||
"model": model,
|
||||
"permission_mode": permission_mode.as_str(),
|
||||
"usage": {
|
||||
"messages": usage.message_count,
|
||||
"turns": usage.turns,
|
||||
"latest_total": usage.latest.total_tokens(),
|
||||
"cumulative_input": usage.cumulative.input_tokens,
|
||||
"cumulative_output": usage.cumulative.output_tokens,
|
||||
"cumulative_total": usage.cumulative.total_tokens(),
|
||||
"estimated_tokens": usage.estimated_tokens,
|
||||
},
|
||||
"workspace": {
|
||||
"cwd": context.cwd,
|
||||
"project_root": context.project_root,
|
||||
"git_branch": context.git_branch,
|
||||
"git_state": context.git_summary.headline(),
|
||||
"changed_files": context.git_summary.changed_files,
|
||||
"staged_files": context.git_summary.staged_files,
|
||||
"unstaged_files": context.git_summary.unstaged_files,
|
||||
"untracked_files": context.git_summary.untracked_files,
|
||||
"session": context.session_path.as_ref().map_or_else(|| "live-repl".to_string(), |path| path.display().to_string()),
|
||||
"loaded_config_files": context.loaded_config_files,
|
||||
"discovered_config_files": context.discovered_config_files,
|
||||
"memory_file_count": context.memory_file_count,
|
||||
},
|
||||
"sandbox": {
|
||||
"enabled": context.sandbox_status.enabled,
|
||||
"active": context.sandbox_status.active,
|
||||
"supported": context.sandbox_status.supported,
|
||||
"in_container": context.sandbox_status.in_container,
|
||||
"requested_namespace": context.sandbox_status.requested.namespace_restrictions,
|
||||
"active_namespace": context.sandbox_status.namespace_active,
|
||||
"requested_network": context.sandbox_status.requested.network_isolation,
|
||||
"active_network": context.sandbox_status.network_active,
|
||||
"filesystem_mode": context.sandbox_status.filesystem_mode.as_str(),
|
||||
"filesystem_active": context.sandbox_status.filesystem_active,
|
||||
"allowed_mounts": context.sandbox_status.allowed_mounts,
|
||||
"markers": context.sandbox_status.container_markers,
|
||||
"fallback_reason": context.sandbox_status.fallback_reason,
|
||||
}
|
||||
}))?
|
||||
),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3838,16 +3876,37 @@ fn format_commit_skipped_report() -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn print_sandbox_status_snapshot() -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn print_sandbox_status_snapshot(
|
||||
output_format: CliOutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cwd = env::current_dir()?;
|
||||
let loader = ConfigLoader::default_for(&cwd);
|
||||
let runtime_config = loader
|
||||
.load()
|
||||
.unwrap_or_else(|_| runtime::RuntimeConfig::empty());
|
||||
println!(
|
||||
"{}",
|
||||
format_sandbox_report(&resolve_sandbox_status(runtime_config.sandbox(), &cwd))
|
||||
);
|
||||
let status = resolve_sandbox_status(runtime_config.sandbox(), &cwd);
|
||||
match output_format {
|
||||
CliOutputFormat::Text => println!("{}", format_sandbox_report(&status)),
|
||||
CliOutputFormat::Json => println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&json!({
|
||||
"kind": "sandbox",
|
||||
"enabled": status.enabled,
|
||||
"active": status.active,
|
||||
"supported": status.supported,
|
||||
"in_container": status.in_container,
|
||||
"requested_namespace": status.requested.namespace_restrictions,
|
||||
"active_namespace": status.namespace_active,
|
||||
"requested_network": status.requested.network_isolation,
|
||||
"active_network": status.network_active,
|
||||
"filesystem_mode": status.filesystem_mode.as_str(),
|
||||
"filesystem_active": status.filesystem_active,
|
||||
"allowed_mounts": status.allowed_mounts,
|
||||
"markers": status.container_markers,
|
||||
"fallback_reason": status.fallback_reason,
|
||||
}))?
|
||||
),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6605,11 +6664,14 @@ mod tests {
|
||||
CliAction::Status {
|
||||
model: DEFAULT_MODEL.to_string(),
|
||||
permission_mode: PermissionMode::DangerFullAccess,
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&["sandbox".to_string()]).expect("sandbox should parse"),
|
||||
CliAction::Sandbox
|
||||
CliAction::Sandbox {
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -192,12 +192,10 @@ fn doctor_command_runs_as_a_local_shell_entrypoint() {
|
||||
|
||||
#[test]
|
||||
fn local_subcommand_help_does_not_fall_through_to_runtime_or_provider_calls() {
|
||||
// given
|
||||
let temp_dir = unique_temp_dir("subcommand-help");
|
||||
let config_home = temp_dir.join("home").join(".claw");
|
||||
fs::create_dir_all(&config_home).expect("config home should exist");
|
||||
|
||||
// when
|
||||
let doctor_help = command_in(&temp_dir)
|
||||
.env("CLAW_CONFIG_HOME", &config_home)
|
||||
.env_remove("ANTHROPIC_API_KEY")
|
||||
@@ -215,7 +213,6 @@ fn local_subcommand_help_does_not_fall_through_to_runtime_or_provider_calls() {
|
||||
.output()
|
||||
.expect("status help should launch");
|
||||
|
||||
// then
|
||||
assert_success(&doctor_help);
|
||||
let doctor_stdout = String::from_utf8(doctor_help.stdout).expect("stdout should be utf8");
|
||||
assert!(doctor_stdout.contains("Usage claw doctor"));
|
||||
@@ -236,121 +233,6 @@ fn local_subcommand_help_does_not_fall_through_to_runtime_or_provider_calls() {
|
||||
fs::remove_dir_all(temp_dir).expect("cleanup temp dir");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_help_flags_render_usage_instead_of_falling_through() {
|
||||
let temp_dir = unique_temp_dir("nested-help");
|
||||
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
|
||||
|
||||
let mcp_output = command_in(&temp_dir)
|
||||
.args(["mcp", "show", "--help"])
|
||||
.output()
|
||||
.expect("claw should launch");
|
||||
assert_success(&mcp_output);
|
||||
let mcp_stdout = String::from_utf8(mcp_output.stdout).expect("stdout should be utf8");
|
||||
assert!(mcp_stdout.contains("Usage /mcp [list|show <server>|help]"));
|
||||
assert!(mcp_stdout.contains("Unexpected show"));
|
||||
assert!(!mcp_stdout.contains("server `--help` is not configured"));
|
||||
|
||||
let skills_output = command_in(&temp_dir)
|
||||
.args(["skills", "install", "--help"])
|
||||
.output()
|
||||
.expect("claw should launch");
|
||||
assert_success(&skills_output);
|
||||
let skills_stdout = String::from_utf8(skills_output.stdout).expect("stdout should be utf8");
|
||||
assert!(skills_stdout.contains("Usage /skills [list|install <path>|help]"));
|
||||
assert!(skills_stdout.contains("Unexpected install"));
|
||||
|
||||
let unknown_output = command_in(&temp_dir)
|
||||
.args(["mcp", "inspect", "--help"])
|
||||
.output()
|
||||
.expect("claw should launch");
|
||||
assert_success(&unknown_output);
|
||||
let unknown_stdout = String::from_utf8(unknown_output.stdout).expect("stdout should be utf8");
|
||||
assert!(unknown_stdout.contains("Usage /mcp [list|show <server>|help]"));
|
||||
assert!(unknown_stdout.contains("Unexpected inspect"));
|
||||
|
||||
fs::remove_dir_all(temp_dir).expect("cleanup temp dir");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_subcommand_help_does_not_fall_through_to_runtime_or_provider_calls() {
|
||||
// given
|
||||
let temp_dir = unique_temp_dir("subcommand-help");
|
||||
let config_home = temp_dir.join("home").join(".claw");
|
||||
fs::create_dir_all(&config_home).expect("config home should exist");
|
||||
|
||||
// when
|
||||
let doctor_help = command_in(&temp_dir)
|
||||
.env("CLAW_CONFIG_HOME", &config_home)
|
||||
.env_remove("ANTHROPIC_API_KEY")
|
||||
.env_remove("ANTHROPIC_AUTH_TOKEN")
|
||||
.env("ANTHROPIC_BASE_URL", "http://127.0.0.1:9")
|
||||
.args(["doctor", "--help"])
|
||||
.output()
|
||||
.expect("doctor help should launch");
|
||||
let status_help = command_in(&temp_dir)
|
||||
.env("CLAW_CONFIG_HOME", &config_home)
|
||||
.env_remove("ANTHROPIC_API_KEY")
|
||||
.env_remove("ANTHROPIC_AUTH_TOKEN")
|
||||
.env("ANTHROPIC_BASE_URL", "http://127.0.0.1:9")
|
||||
.args(["status", "--help"])
|
||||
.output()
|
||||
.expect("status help should launch");
|
||||
|
||||
// then
|
||||
assert_success(&doctor_help);
|
||||
let doctor_stdout = String::from_utf8(doctor_help.stdout).expect("stdout should be utf8");
|
||||
assert!(doctor_stdout.contains("Usage claw doctor"));
|
||||
assert!(doctor_stdout.contains("local-only health report"));
|
||||
assert!(!doctor_stdout.contains("Thinking"));
|
||||
|
||||
assert_success(&status_help);
|
||||
let status_stdout = String::from_utf8(status_help.stdout).expect("stdout should be utf8");
|
||||
assert!(status_stdout.contains("Usage claw status"));
|
||||
assert!(status_stdout.contains("local workspace snapshot"));
|
||||
assert!(!status_stdout.contains("Thinking"));
|
||||
|
||||
let doctor_stderr = String::from_utf8(doctor_help.stderr).expect("stderr should be utf8");
|
||||
let status_stderr = String::from_utf8(status_help.stderr).expect("stderr should be utf8");
|
||||
assert!(!doctor_stderr.contains("auth_unavailable"));
|
||||
assert!(!status_stderr.contains("auth_unavailable"));
|
||||
=======
|
||||
fn nested_help_flags_render_usage_instead_of_falling_through() {
|
||||
let temp_dir = unique_temp_dir("nested-help");
|
||||
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
|
||||
|
||||
let mcp_output = command_in(&temp_dir)
|
||||
.args(["mcp", "show", "--help"])
|
||||
.output()
|
||||
.expect("claw should launch");
|
||||
assert_success(&mcp_output);
|
||||
let mcp_stdout = String::from_utf8(mcp_output.stdout).expect("stdout should be utf8");
|
||||
assert!(mcp_stdout.contains("Usage /mcp [list|show <server>|help]"));
|
||||
assert!(mcp_stdout.contains("Unexpected show"));
|
||||
assert!(!mcp_stdout.contains("server `--help` is not configured"));
|
||||
|
||||
let skills_output = command_in(&temp_dir)
|
||||
.args(["skills", "install", "--help"])
|
||||
.output()
|
||||
.expect("claw should launch");
|
||||
assert_success(&skills_output);
|
||||
let skills_stdout = String::from_utf8(skills_output.stdout).expect("stdout should be utf8");
|
||||
assert!(skills_stdout.contains("Usage /skills [list|install <path>|help]"));
|
||||
assert!(skills_stdout.contains("Unexpected install"));
|
||||
|
||||
let unknown_output = command_in(&temp_dir)
|
||||
.args(["mcp", "inspect", "--help"])
|
||||
.output()
|
||||
.expect("claw should launch");
|
||||
assert_success(&unknown_output);
|
||||
let unknown_stdout = String::from_utf8(unknown_output.stdout).expect("stdout should be utf8");
|
||||
assert!(unknown_stdout.contains("Usage /mcp [list|show <server>|help]"));
|
||||
assert!(unknown_stdout.contains("Unexpected inspect"));
|
||||
>>>>>>> 1f53d96 (Route nested CLI help requests to usage instead of operand fallthrough)
|
||||
|
||||
fs::remove_dir_all(temp_dir).expect("cleanup temp dir");
|
||||
}
|
||||
|
||||
fn command_in(cwd: &Path) -> Command {
|
||||
let mut command = Command::new(env!("CARGO_BIN_EXE_claw"));
|
||||
command.current_dir(cwd);
|
||||
|
||||
Reference in New Issue
Block a user