From f43375f067a4480fd88dbc52ed4236024b1c6936 Mon Sep 17 00:00:00 2001 From: Yeachan-Heo Date: Sun, 5 Apr 2026 18:11:25 +0000 Subject: [PATCH] Complete local claw-first CLI and config surface alignment --- rust/crates/commands/src/lib.rs | 22 +-- rust/crates/runtime/src/lib.rs | 7 +- rust/crates/rusty-claude-cli/src/main.rs | 150 +++++++++++++----- .../tests/cli_flags_and_config_defaults.rs | 118 -------------- 4 files changed, 124 insertions(+), 173 deletions(-) diff --git a/rust/crates/commands/src/lib.rs b/rust/crates/commands/src/lib.rs index fa5d790..e34fb85 100644 --- a/rust/crates/commands/src/lib.rs +++ b/rust/crates/commands/src/lib.rs @@ -3347,8 +3347,8 @@ fn render_skills_usage_json(unexpected: Option<&str>) -> Value { "usage": { "slash_command": "/skills [list|install |help]", "direct_cli": "claw skills [list|install |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"); diff --git a/rust/crates/runtime/src/lib.rs b/rust/crates/runtime/src/lib.rs index 7550c49..3c4472a 100644 --- a/rust/crates/runtime/src/lib.rs +++ b/rust/crates/runtime/src/lib.rs @@ -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, diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 435f431..3f01652 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -129,8 +129,9 @@ fn run() -> Result<(), Box> { 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 { 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, + output_format: CliOutputFormat, ) -> Option> { 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> { - 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> { +fn print_sandbox_status_snapshot( + output_format: CliOutputFormat, +) -> Result<(), Box> { 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, + } ); } diff --git a/rust/crates/rusty-claude-cli/tests/cli_flags_and_config_defaults.rs b/rust/crates/rusty-claude-cli/tests/cli_flags_and_config_defaults.rs index b68d2d5..8988291 100644 --- a/rust/crates/rusty-claude-cli/tests/cli_flags_and_config_defaults.rs +++ b/rust/crates/rusty-claude-cli/tests/cli_flags_and_config_defaults.rs @@ -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 |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 |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 |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 |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 |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 |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);