diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 8c2bbcb..acf5d28 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -3870,6 +3870,7 @@ impl LiveCli { compact: bool, ) -> Result<(), Box> { match output_format { + CliOutputFormat::Json if compact => self.run_prompt_compact_json(input), CliOutputFormat::Text if compact => self.run_prompt_compact(input), CliOutputFormat::Text => self.run_turn(input), CliOutputFormat::Json => self.run_prompt_json(input), @@ -3889,6 +3890,32 @@ impl LiveCli { Ok(()) } + + fn run_prompt_compact_json(&mut self, input: &str) -> Result<(), Box> { + let (mut runtime, hook_abort_monitor) = self.prepare_turn_runtime(false)?; + let mut permission_prompter = CliPermissionPrompter::new(self.permission_mode); + let result = runtime.run_turn(input, Some(&mut permission_prompter)); + hook_abort_monitor.stop(); + let summary = result?; + self.replace_runtime(runtime)?; + self.persist_session()?; + println!( + "{}", + json!({ + "message": final_assistant_text(&summary), + "compact": true, + "model": self.model, + "usage": { + "input_tokens": summary.usage.input_tokens, + "output_tokens": summary.usage.output_tokens, + "cache_creation_input_tokens": summary.usage.cache_creation_input_tokens, + "cache_read_input_tokens": summary.usage.cache_read_input_tokens, + }, + }) + ); + Ok(()) + } + fn run_prompt_json(&mut self, input: &str) -> Result<(), Box> { let (mut runtime, hook_abort_monitor) = self.prepare_turn_runtime(false)?; let mut permission_prompter = CliPermissionPrompter::new(self.permission_mode); diff --git a/rust/crates/rusty-claude-cli/tests/compact_output.rs b/rust/crates/rusty-claude-cli/tests/compact_output.rs index 456862f..1472044 100644 --- a/rust/crates/rusty-claude-cli/tests/compact_output.rs +++ b/rust/crates/rusty-claude-cli/tests/compact_output.rs @@ -5,6 +5,7 @@ use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{SystemTime, UNIX_EPOCH}; use mock_anthropic_service::{MockAnthropicService, SCENARIO_PREFIX}; +use serde_json::Value; static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0); @@ -125,6 +126,60 @@ fn compact_flag_streaming_text_only_emits_final_message_text() { fs::remove_dir_all(&workspace).expect("workspace cleanup should succeed"); } +#[test] +fn compact_flag_with_json_output_emits_structured_json() { + let runtime = tokio::runtime::Runtime::new().expect("tokio runtime should build"); + let server = runtime + .block_on(MockAnthropicService::spawn()) + .expect("mock service should start"); + let base_url = server.base_url(); + + let workspace = unique_temp_dir("compact-json"); + let config_home = workspace.join("config-home"); + let home = workspace.join("home"); + fs::create_dir_all(&workspace).expect("workspace should exist"); + fs::create_dir_all(&config_home).expect("config home should exist"); + fs::create_dir_all(&home).expect("home should exist"); + + let prompt = format!("{SCENARIO_PREFIX}streaming_text"); + let output = run_claw( + &workspace, + &config_home, + &home, + &base_url, + &[ + "--model", + "sonnet", + "--permission-mode", + "read-only", + "--output-format", + "json", + "--compact", + &prompt, + ], + ); + + assert!( + output.status.success(), + "compact json run should succeed +stdout: +{} + +stderr: +{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8"); + let parsed: Value = serde_json::from_str(&stdout).expect("compact json stdout should parse"); + assert_eq!(parsed["message"], "Mock streaming says hello from the parity harness."); + assert_eq!(parsed["compact"], true); + assert_eq!(parsed["model"], "claude-sonnet-4-6"); + assert!(parsed["usage"].is_object()); + + fs::remove_dir_all(&workspace).expect("workspace cleanup should succeed"); +} + fn run_claw( cwd: &std::path::Path, config_home: &std::path::Path,