mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-06 16:14:49 +08:00
Compare commits
1 Commits
fix/p2-19-
...
fix/p0-10-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a2fa1581e |
@@ -2142,22 +2142,13 @@ pub fn handle_plugins_slash_command(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_agents_slash_command(args: Option<&str>, cwd: &Path) -> std::io::Result<String> {
|
pub fn handle_agents_slash_command(args: Option<&str>, cwd: &Path) -> std::io::Result<String> {
|
||||||
if let Some(args) = normalize_optional_args(args) {
|
|
||||||
if let Some(help_path) = help_path_from_args(args) {
|
|
||||||
return Ok(match help_path.as_slice() {
|
|
||||||
[] => render_agents_usage(None),
|
|
||||||
_ => render_agents_usage(Some(&help_path.join(" "))),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match normalize_optional_args(args) {
|
match normalize_optional_args(args) {
|
||||||
None | Some("list") => {
|
None | Some("list") => {
|
||||||
let roots = discover_definition_roots(cwd, "agents");
|
let roots = discover_definition_roots(cwd, "agents");
|
||||||
let agents = load_agents_from_roots(&roots)?;
|
let agents = load_agents_from_roots(&roots)?;
|
||||||
Ok(render_agents_report(&agents))
|
Ok(render_agents_report(&agents))
|
||||||
}
|
}
|
||||||
Some(args) if is_help_arg(args) => Ok(render_agents_usage(None)),
|
Some("-h" | "--help" | "help") => Ok(render_agents_usage(None)),
|
||||||
Some(args) => Ok(render_agents_usage(Some(args))),
|
Some(args) => Ok(render_agents_usage(Some(args))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2171,16 +2162,6 @@ pub fn handle_mcp_slash_command(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_skills_slash_command(args: Option<&str>, cwd: &Path) -> std::io::Result<String> {
|
pub fn handle_skills_slash_command(args: Option<&str>, cwd: &Path) -> std::io::Result<String> {
|
||||||
if let Some(args) = normalize_optional_args(args) {
|
|
||||||
if let Some(help_path) = help_path_from_args(args) {
|
|
||||||
return Ok(match help_path.as_slice() {
|
|
||||||
[] => render_skills_usage(None),
|
|
||||||
["install", ..] => render_skills_usage(Some("install")),
|
|
||||||
_ => render_skills_usage(Some(&help_path.join(" "))),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match normalize_optional_args(args) {
|
match normalize_optional_args(args) {
|
||||||
None | Some("list") => {
|
None | Some("list") => {
|
||||||
let roots = discover_skill_roots(cwd);
|
let roots = discover_skill_roots(cwd);
|
||||||
@@ -2196,7 +2177,7 @@ pub fn handle_skills_slash_command(args: Option<&str>, cwd: &Path) -> std::io::R
|
|||||||
let install = install_skill(target, cwd)?;
|
let install = install_skill(target, cwd)?;
|
||||||
Ok(render_skill_install_report(&install))
|
Ok(render_skill_install_report(&install))
|
||||||
}
|
}
|
||||||
Some(args) if is_help_arg(args) => Ok(render_skills_usage(None)),
|
Some("-h" | "--help" | "help") => Ok(render_skills_usage(None)),
|
||||||
Some(args) => Ok(render_skills_usage(Some(args))),
|
Some(args) => Ok(render_skills_usage(Some(args))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2206,16 +2187,6 @@ fn render_mcp_report_for(
|
|||||||
cwd: &Path,
|
cwd: &Path,
|
||||||
args: Option<&str>,
|
args: Option<&str>,
|
||||||
) -> Result<String, runtime::ConfigError> {
|
) -> Result<String, runtime::ConfigError> {
|
||||||
if let Some(args) = normalize_optional_args(args) {
|
|
||||||
if let Some(help_path) = help_path_from_args(args) {
|
|
||||||
return Ok(match help_path.as_slice() {
|
|
||||||
[] => render_mcp_usage(None),
|
|
||||||
["show", ..] => render_mcp_usage(Some("show")),
|
|
||||||
_ => render_mcp_usage(Some(&help_path.join(" "))),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match normalize_optional_args(args) {
|
match normalize_optional_args(args) {
|
||||||
None | Some("list") => {
|
None | Some("list") => {
|
||||||
let runtime_config = loader.load()?;
|
let runtime_config = loader.load()?;
|
||||||
@@ -2224,7 +2195,7 @@ fn render_mcp_report_for(
|
|||||||
runtime_config.mcp().servers(),
|
runtime_config.mcp().servers(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
Some(args) if is_help_arg(args) => Ok(render_mcp_usage(None)),
|
Some("-h" | "--help" | "help") => Ok(render_mcp_usage(None)),
|
||||||
Some("show") => Ok(render_mcp_usage(Some("show"))),
|
Some("show") => Ok(render_mcp_usage(Some("show"))),
|
||||||
Some(args) if args.split_whitespace().next() == Some("show") => {
|
Some(args) if args.split_whitespace().next() == Some("show") => {
|
||||||
let mut parts = args.split_whitespace();
|
let mut parts = args.split_whitespace();
|
||||||
@@ -3065,16 +3036,6 @@ fn normalize_optional_args(args: Option<&str>) -> Option<&str> {
|
|||||||
args.map(str::trim).filter(|value| !value.is_empty())
|
args.map(str::trim).filter(|value| !value.is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_help_arg(arg: &str) -> bool {
|
|
||||||
matches!(arg, "help" | "-h" | "--help")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn help_path_from_args(args: &str) -> Option<Vec<&str>> {
|
|
||||||
let parts = args.split_whitespace().collect::<Vec<_>>();
|
|
||||||
let help_index = parts.iter().position(|part| is_help_arg(part))?;
|
|
||||||
Some(parts[..help_index].to_vec())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_agents_usage(unexpected: Option<&str>) -> String {
|
fn render_agents_usage(unexpected: Option<&str>) -> String {
|
||||||
let mut lines = vec![
|
let mut lines = vec![
|
||||||
"Agents".to_string(),
|
"Agents".to_string(),
|
||||||
@@ -4044,17 +4005,7 @@ mod tests {
|
|||||||
|
|
||||||
let skills_unexpected =
|
let skills_unexpected =
|
||||||
super::handle_skills_slash_command(Some("show help"), &cwd).expect("skills usage");
|
super::handle_skills_slash_command(Some("show help"), &cwd).expect("skills usage");
|
||||||
assert!(skills_unexpected.contains("Unexpected show"));
|
assert!(skills_unexpected.contains("Unexpected show help"));
|
||||||
|
|
||||||
let skills_install_help = super::handle_skills_slash_command(Some("install --help"), &cwd)
|
|
||||||
.expect("nested skills help");
|
|
||||||
assert!(skills_install_help.contains("Usage /skills [list|install <path>|help]"));
|
|
||||||
assert!(skills_install_help.contains("Unexpected install"));
|
|
||||||
|
|
||||||
let skills_unknown_help =
|
|
||||||
super::handle_skills_slash_command(Some("show --help"), &cwd).expect("skills help");
|
|
||||||
assert!(skills_unknown_help.contains("Usage /skills [list|install <path>|help]"));
|
|
||||||
assert!(skills_unknown_help.contains("Unexpected show"));
|
|
||||||
|
|
||||||
let _ = fs::remove_dir_all(cwd);
|
let _ = fs::remove_dir_all(cwd);
|
||||||
}
|
}
|
||||||
@@ -4071,16 +4022,6 @@ mod tests {
|
|||||||
super::handle_mcp_slash_command(Some("show alpha beta"), &cwd).expect("mcp usage");
|
super::handle_mcp_slash_command(Some("show alpha beta"), &cwd).expect("mcp usage");
|
||||||
assert!(unexpected.contains("Unexpected show alpha beta"));
|
assert!(unexpected.contains("Unexpected show alpha beta"));
|
||||||
|
|
||||||
let nested_help =
|
|
||||||
super::handle_mcp_slash_command(Some("show --help"), &cwd).expect("mcp help");
|
|
||||||
assert!(nested_help.contains("Usage /mcp [list|show <server>|help]"));
|
|
||||||
assert!(nested_help.contains("Unexpected show"));
|
|
||||||
|
|
||||||
let unknown_help =
|
|
||||||
super::handle_mcp_slash_command(Some("inspect --help"), &cwd).expect("mcp usage");
|
|
||||||
assert!(unknown_help.contains("Usage /mcp [list|show <server>|help]"));
|
|
||||||
assert!(unknown_help.contains("Unexpected inspect"));
|
|
||||||
|
|
||||||
let _ = fs::remove_dir_all(cwd);
|
let _ = fs::remove_dir_all(cwd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ use runtime::{
|
|||||||
UsageTracker,
|
UsageTracker,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::json;
|
use serde_json::{json, Value};
|
||||||
use tools::{GlobalToolRegistry, RuntimeToolDefinition, ToolSearchOutput};
|
use tools::{GlobalToolRegistry, RuntimeToolDefinition, ToolSearchOutput};
|
||||||
|
|
||||||
const DEFAULT_MODEL: &str = "claude-opus-4-6";
|
const DEFAULT_MODEL: &str = "claude-opus-4-6";
|
||||||
@@ -117,12 +117,14 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
CliAction::ResumeSession {
|
CliAction::ResumeSession {
|
||||||
session_path,
|
session_path,
|
||||||
commands,
|
commands,
|
||||||
} => resume_session(&session_path, &commands),
|
output_format,
|
||||||
|
} => resume_session(&session_path, &commands, output_format),
|
||||||
CliAction::Status {
|
CliAction::Status {
|
||||||
model,
|
model,
|
||||||
permission_mode,
|
permission_mode,
|
||||||
} => print_status_snapshot(&model, permission_mode)?,
|
output_format,
|
||||||
CliAction::Sandbox => print_sandbox_status_snapshot()?,
|
} => print_status_snapshot(&model, permission_mode, output_format)?,
|
||||||
|
CliAction::Sandbox { output_format } => print_sandbox_status_snapshot(output_format)?,
|
||||||
CliAction::Prompt {
|
CliAction::Prompt {
|
||||||
prompt,
|
prompt,
|
||||||
model,
|
model,
|
||||||
@@ -165,12 +167,16 @@ enum CliAction {
|
|||||||
ResumeSession {
|
ResumeSession {
|
||||||
session_path: PathBuf,
|
session_path: PathBuf,
|
||||||
commands: Vec<String>,
|
commands: Vec<String>,
|
||||||
|
output_format: CliOutputFormat,
|
||||||
},
|
},
|
||||||
Status {
|
Status {
|
||||||
model: String,
|
model: String,
|
||||||
permission_mode: PermissionMode,
|
permission_mode: PermissionMode,
|
||||||
|
output_format: CliOutputFormat,
|
||||||
|
},
|
||||||
|
Sandbox {
|
||||||
|
output_format: CliOutputFormat,
|
||||||
},
|
},
|
||||||
Sandbox,
|
|
||||||
Prompt {
|
Prompt {
|
||||||
prompt: String,
|
prompt: String,
|
||||||
model: String,
|
model: String,
|
||||||
@@ -339,9 +345,10 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if rest.first().map(String::as_str) == Some("--resume") {
|
if rest.first().map(String::as_str) == Some("--resume") {
|
||||||
return parse_resume_args(&rest[1..]);
|
return parse_resume_args(&rest[1..], output_format);
|
||||||
}
|
}
|
||||||
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;
|
return action;
|
||||||
}
|
}
|
||||||
@@ -392,6 +399,7 @@ fn parse_single_word_command_alias(
|
|||||||
rest: &[String],
|
rest: &[String],
|
||||||
model: &str,
|
model: &str,
|
||||||
permission_mode_override: Option<PermissionMode>,
|
permission_mode_override: Option<PermissionMode>,
|
||||||
|
output_format: CliOutputFormat,
|
||||||
) -> Option<Result<CliAction, String>> {
|
) -> Option<Result<CliAction, String>> {
|
||||||
if rest.len() != 1 {
|
if rest.len() != 1 {
|
||||||
return None;
|
return None;
|
||||||
@@ -403,8 +411,9 @@ fn parse_single_word_command_alias(
|
|||||||
"status" => Some(Ok(CliAction::Status {
|
"status" => Some(Ok(CliAction::Status {
|
||||||
model: model.to_string(),
|
model: model.to_string(),
|
||||||
permission_mode: permission_mode_override.unwrap_or_else(default_permission_mode),
|
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 })),
|
||||||
other => bare_slash_command_guidance(other).map(Err),
|
other => bare_slash_command_guidance(other).map(Err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -698,7 +707,7 @@ fn parse_system_prompt_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
Ok(CliAction::PrintSystemPrompt { cwd, date })
|
Ok(CliAction::PrintSystemPrompt { cwd, date })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_resume_args(args: &[String]) -> Result<CliAction, String> {
|
fn parse_resume_args(args: &[String], output_format: CliOutputFormat) -> Result<CliAction, String> {
|
||||||
let (session_path, command_tokens): (PathBuf, &[String]) = match args.first() {
|
let (session_path, command_tokens): (PathBuf, &[String]) = match args.first() {
|
||||||
None => (PathBuf::from(LATEST_SESSION_REFERENCE), &[]),
|
None => (PathBuf::from(LATEST_SESSION_REFERENCE), &[]),
|
||||||
Some(first) if looks_like_slash_command_token(first) => {
|
Some(first) if looks_like_slash_command_token(first) => {
|
||||||
@@ -738,6 +747,7 @@ fn parse_resume_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
Ok(CliAction::ResumeSession {
|
Ok(CliAction::ResumeSession {
|
||||||
session_path,
|
session_path,
|
||||||
commands,
|
commands,
|
||||||
|
output_format,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -928,7 +938,7 @@ fn print_version() {
|
|||||||
println!("{}", render_version_report());
|
println!("{}", render_version_report());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resume_session(session_path: &Path, commands: &[String]) {
|
fn resume_session(session_path: &Path, commands: &[String], output_format: CliOutputFormat) {
|
||||||
let resolved_path = if session_path.exists() {
|
let resolved_path = if session_path.exists() {
|
||||||
session_path.to_path_buf()
|
session_path.to_path_buf()
|
||||||
} else {
|
} else {
|
||||||
@@ -950,15 +960,31 @@ fn resume_session(session_path: &Path, commands: &[String]) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if commands.is_empty() {
|
if commands.is_empty() {
|
||||||
println!(
|
match output_format {
|
||||||
"Restored session from {} ({} messages).",
|
CliOutputFormat::Text => {
|
||||||
resolved_path.display(),
|
println!(
|
||||||
session.messages.len()
|
"Restored session from {} ({} messages).",
|
||||||
);
|
resolved_path.display(),
|
||||||
|
session.messages.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
CliOutputFormat::Json => {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
serialize_json_output(&json!({
|
||||||
|
"kind": "resume",
|
||||||
|
"session_file": resolved_path.display().to_string(),
|
||||||
|
"messages": session.messages.len(),
|
||||||
|
}))
|
||||||
|
.unwrap_or_else(|error| format!(r#"{{"kind":"error","message":"{error}"}}"#))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut session = session;
|
let mut session = session;
|
||||||
|
let mut json_outputs = Vec::new();
|
||||||
for raw_command in commands {
|
for raw_command in commands {
|
||||||
let command = match SlashCommand::parse(raw_command) {
|
let command = match SlashCommand::parse(raw_command) {
|
||||||
Ok(Some(command)) => command,
|
Ok(Some(command)) => command,
|
||||||
@@ -971,14 +997,19 @@ fn resume_session(session_path: &Path, commands: &[String]) {
|
|||||||
std::process::exit(2);
|
std::process::exit(2);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
match run_resume_command(&resolved_path, &session, &command) {
|
match run_resume_command(&resolved_path, &session, &command, output_format) {
|
||||||
Ok(ResumeCommandOutcome {
|
Ok(ResumeCommandOutcome {
|
||||||
session: next_session,
|
session: next_session,
|
||||||
message,
|
message,
|
||||||
}) => {
|
}) => {
|
||||||
session = next_session;
|
session = next_session;
|
||||||
if let Some(message) = message {
|
if let Some(message) = message {
|
||||||
println!("{message}");
|
match output_format {
|
||||||
|
CliOutputFormat::Text => {
|
||||||
|
println!("{}", render_resume_text_output(&message))
|
||||||
|
}
|
||||||
|
CliOutputFormat::Json => json_outputs.push(message),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -987,12 +1018,27 @@ fn resume_session(session_path: &Path, commands: &[String]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if matches!(output_format, CliOutputFormat::Json) {
|
||||||
|
let payload = if json_outputs.len() == 1 {
|
||||||
|
json_outputs.pop().expect("single json output")
|
||||||
|
} else {
|
||||||
|
Value::Array(json_outputs)
|
||||||
|
};
|
||||||
|
match serialize_json_output(&payload) {
|
||||||
|
Ok(rendered) => println!("{rendered}"),
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("failed to render json output: {error}");
|
||||||
|
std::process::exit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct ResumeCommandOutcome {
|
struct ResumeCommandOutcome {
|
||||||
session: Session,
|
session: Session,
|
||||||
message: Option<String>,
|
message: Option<Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -1317,16 +1363,28 @@ fn parse_git_status_metadata_for(
|
|||||||
(project_root, branch)
|
(project_root, branch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn serialize_json_output(value: &Value) -> Result<String, serde_json::Error> {
|
||||||
|
serde_json::to_string_pretty(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_resume_text_output(value: &Value) -> String {
|
||||||
|
value.get("message").and_then(Value::as_str).map_or_else(
|
||||||
|
|| serialize_json_output(value).unwrap_or_else(|_| value.to_string()),
|
||||||
|
ToString::to_string,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
fn run_resume_command(
|
fn run_resume_command(
|
||||||
session_path: &Path,
|
session_path: &Path,
|
||||||
session: &Session,
|
session: &Session,
|
||||||
command: &SlashCommand,
|
command: &SlashCommand,
|
||||||
|
output_format: CliOutputFormat,
|
||||||
) -> Result<ResumeCommandOutcome, Box<dyn std::error::Error>> {
|
) -> Result<ResumeCommandOutcome, Box<dyn std::error::Error>> {
|
||||||
match command {
|
match command {
|
||||||
SlashCommand::Help => Ok(ResumeCommandOutcome {
|
SlashCommand::Help => Ok(ResumeCommandOutcome {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
message: Some(render_repl_help()),
|
message: Some(json!({ "kind": "help", "message": render_repl_help() })),
|
||||||
}),
|
}),
|
||||||
SlashCommand::Compact => {
|
SlashCommand::Compact => {
|
||||||
let result = runtime::compact_session(
|
let result = runtime::compact_session(
|
||||||
@@ -1342,16 +1400,20 @@ fn run_resume_command(
|
|||||||
result.compacted_session.save_to_path(session_path)?;
|
result.compacted_session.save_to_path(session_path)?;
|
||||||
Ok(ResumeCommandOutcome {
|
Ok(ResumeCommandOutcome {
|
||||||
session: result.compacted_session,
|
session: result.compacted_session,
|
||||||
message: Some(format_compact_report(removed, kept, skipped)),
|
message: Some(json!({
|
||||||
|
"kind": "compact",
|
||||||
|
"message": format_compact_report(removed, kept, skipped),
|
||||||
|
})),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
SlashCommand::Clear { confirm } => {
|
SlashCommand::Clear { confirm } => {
|
||||||
if !confirm {
|
if !confirm {
|
||||||
return Ok(ResumeCommandOutcome {
|
return Ok(ResumeCommandOutcome {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
message: Some(
|
message: Some(json!({
|
||||||
"clear: confirmation required; rerun with /clear --confirm".to_string(),
|
"kind": "clear",
|
||||||
),
|
"message": "clear: confirmation required; rerun with /clear --confirm",
|
||||||
|
})),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let backup_path = write_session_clear_backup(session, session_path)?;
|
let backup_path = write_session_clear_backup(session, session_path)?;
|
||||||
@@ -1361,55 +1423,85 @@ fn run_resume_command(
|
|||||||
cleared.save_to_path(session_path)?;
|
cleared.save_to_path(session_path)?;
|
||||||
Ok(ResumeCommandOutcome {
|
Ok(ResumeCommandOutcome {
|
||||||
session: cleared,
|
session: cleared,
|
||||||
message: Some(format!(
|
message: Some(json!({
|
||||||
"Session cleared\n Mode resumed session reset\n Previous session {previous_session_id}\n Backup {}\n Resume previous claw --resume {}\n New session {new_session_id}\n Session file {}",
|
"kind": "clear",
|
||||||
backup_path.display(),
|
"message": format!(
|
||||||
backup_path.display(),
|
"Session cleared\n Mode resumed session reset\n Previous session {previous_session_id}\n Backup {}\n Resume previous claw --resume {}\n New session {new_session_id}\n Session file {}",
|
||||||
session_path.display()
|
backup_path.display(),
|
||||||
)),
|
backup_path.display(),
|
||||||
|
session_path.display()
|
||||||
|
),
|
||||||
|
})),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
SlashCommand::Status => {
|
SlashCommand::Status => {
|
||||||
let tracker = UsageTracker::from_session(session);
|
let tracker = UsageTracker::from_session(session);
|
||||||
let usage = tracker.cumulative_usage();
|
let usage = tracker.cumulative_usage();
|
||||||
|
let status_usage = StatusUsage {
|
||||||
|
message_count: session.messages.len(),
|
||||||
|
turns: tracker.turns(),
|
||||||
|
latest: tracker.current_turn_usage(),
|
||||||
|
cumulative: usage,
|
||||||
|
estimated_tokens: 0,
|
||||||
|
};
|
||||||
|
let context = status_context(Some(session_path))?;
|
||||||
|
let status_json = status_report_json(
|
||||||
|
"restored-session",
|
||||||
|
status_usage,
|
||||||
|
default_permission_mode().as_str(),
|
||||||
|
&context,
|
||||||
|
);
|
||||||
Ok(ResumeCommandOutcome {
|
Ok(ResumeCommandOutcome {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
message: Some(format_status_report(
|
message: Some(match output_format {
|
||||||
"restored-session",
|
CliOutputFormat::Text => json!({
|
||||||
StatusUsage {
|
"kind": "status-text",
|
||||||
message_count: session.messages.len(),
|
"message": format_status_report(
|
||||||
turns: tracker.turns(),
|
"restored-session",
|
||||||
latest: tracker.current_turn_usage(),
|
status_usage,
|
||||||
cumulative: usage,
|
default_permission_mode().as_str(),
|
||||||
estimated_tokens: 0,
|
&context,
|
||||||
},
|
),
|
||||||
default_permission_mode().as_str(),
|
}),
|
||||||
&status_context(Some(session_path))?,
|
CliOutputFormat::Json => status_json,
|
||||||
)),
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
SlashCommand::Sandbox => {
|
SlashCommand::Sandbox => {
|
||||||
let cwd = env::current_dir()?;
|
let cwd = env::current_dir()?;
|
||||||
let loader = ConfigLoader::default_for(&cwd);
|
let loader = ConfigLoader::default_for(&cwd);
|
||||||
let runtime_config = loader.load()?;
|
let runtime_config = loader.load()?;
|
||||||
|
let sandbox_status = resolve_sandbox_status(runtime_config.sandbox(), &cwd);
|
||||||
Ok(ResumeCommandOutcome {
|
Ok(ResumeCommandOutcome {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
message: Some(format_sandbox_report(&resolve_sandbox_status(
|
message: Some(match output_format {
|
||||||
runtime_config.sandbox(),
|
CliOutputFormat::Text => json!({
|
||||||
&cwd,
|
"kind": "sandbox-text",
|
||||||
))),
|
"message": format_sandbox_report(&sandbox_status),
|
||||||
|
}),
|
||||||
|
CliOutputFormat::Json => json!({
|
||||||
|
"kind": "sandbox",
|
||||||
|
"sandbox": sandbox_status_json(&sandbox_status),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
SlashCommand::Cost => {
|
SlashCommand::Cost => {
|
||||||
let usage = UsageTracker::from_session(session).cumulative_usage();
|
let usage = UsageTracker::from_session(session).cumulative_usage();
|
||||||
Ok(ResumeCommandOutcome {
|
Ok(ResumeCommandOutcome {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
message: Some(format_cost_report(usage)),
|
message: Some(json!({
|
||||||
|
"kind": "cost",
|
||||||
|
"message": format_cost_report(usage),
|
||||||
|
})),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
SlashCommand::Config { section } => Ok(ResumeCommandOutcome {
|
SlashCommand::Config { section } => Ok(ResumeCommandOutcome {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
message: Some(render_config_report(section.as_deref())?),
|
message: Some(json!({
|
||||||
|
"kind": "config",
|
||||||
|
"message": render_config_report(section.as_deref())?,
|
||||||
|
})),
|
||||||
}),
|
}),
|
||||||
SlashCommand::Mcp { action, target } => {
|
SlashCommand::Mcp { action, target } => {
|
||||||
let cwd = env::current_dir()?;
|
let cwd = env::current_dir()?;
|
||||||
@@ -1421,51 +1513,75 @@ fn run_resume_command(
|
|||||||
};
|
};
|
||||||
Ok(ResumeCommandOutcome {
|
Ok(ResumeCommandOutcome {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
message: Some(handle_mcp_slash_command(args.as_deref(), &cwd)?),
|
message: Some(json!({
|
||||||
|
"kind": "mcp",
|
||||||
|
"message": handle_mcp_slash_command(args.as_deref(), &cwd)?,
|
||||||
|
})),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
SlashCommand::Memory => Ok(ResumeCommandOutcome {
|
SlashCommand::Memory => Ok(ResumeCommandOutcome {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
message: Some(render_memory_report()?),
|
message: Some(json!({
|
||||||
|
"kind": "memory",
|
||||||
|
"message": render_memory_report()?,
|
||||||
|
})),
|
||||||
}),
|
}),
|
||||||
SlashCommand::Init => Ok(ResumeCommandOutcome {
|
SlashCommand::Init => Ok(ResumeCommandOutcome {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
message: Some(init_claude_md()?),
|
message: Some(json!({
|
||||||
|
"kind": "init",
|
||||||
|
"message": init_claude_md()?,
|
||||||
|
})),
|
||||||
}),
|
}),
|
||||||
SlashCommand::Diff => Ok(ResumeCommandOutcome {
|
SlashCommand::Diff => Ok(ResumeCommandOutcome {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
message: Some(render_diff_report_for(
|
message: Some(json!({
|
||||||
session_path.parent().unwrap_or_else(|| Path::new(".")),
|
"kind": "diff",
|
||||||
)?),
|
"message": render_diff_report_for(
|
||||||
|
session_path.parent().unwrap_or_else(|| Path::new(".")),
|
||||||
|
)?,
|
||||||
|
})),
|
||||||
}),
|
}),
|
||||||
SlashCommand::Version => Ok(ResumeCommandOutcome {
|
SlashCommand::Version => Ok(ResumeCommandOutcome {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
message: Some(render_version_report()),
|
message: Some(json!({
|
||||||
|
"kind": "version",
|
||||||
|
"message": render_version_report(),
|
||||||
|
})),
|
||||||
}),
|
}),
|
||||||
SlashCommand::Export { path } => {
|
SlashCommand::Export { path } => {
|
||||||
let export_path = resolve_export_path(path.as_deref(), session)?;
|
let export_path = resolve_export_path(path.as_deref(), session)?;
|
||||||
fs::write(&export_path, render_export_text(session))?;
|
fs::write(&export_path, render_export_text(session))?;
|
||||||
Ok(ResumeCommandOutcome {
|
Ok(ResumeCommandOutcome {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
message: Some(format!(
|
message: Some(json!({
|
||||||
"Export\n Result wrote transcript\n File {}\n Messages {}",
|
"kind": "export",
|
||||||
export_path.display(),
|
"message": format!(
|
||||||
session.messages.len(),
|
"Export\n Result wrote transcript\n File {}\n Messages {}",
|
||||||
)),
|
export_path.display(),
|
||||||
|
session.messages.len(),
|
||||||
|
),
|
||||||
|
})),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
SlashCommand::Agents { args } => {
|
SlashCommand::Agents { args } => {
|
||||||
let cwd = env::current_dir()?;
|
let cwd = env::current_dir()?;
|
||||||
Ok(ResumeCommandOutcome {
|
Ok(ResumeCommandOutcome {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
message: Some(handle_agents_slash_command(args.as_deref(), &cwd)?),
|
message: Some(json!({
|
||||||
|
"kind": "agents",
|
||||||
|
"message": handle_agents_slash_command(args.as_deref(), &cwd)?,
|
||||||
|
})),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
SlashCommand::Skills { args } => {
|
SlashCommand::Skills { args } => {
|
||||||
let cwd = env::current_dir()?;
|
let cwd = env::current_dir()?;
|
||||||
Ok(ResumeCommandOutcome {
|
Ok(ResumeCommandOutcome {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
message: Some(handle_skills_slash_command(args.as_deref(), &cwd)?),
|
message: Some(json!({
|
||||||
|
"kind": "skills",
|
||||||
|
"message": handle_skills_slash_command(args.as_deref(), &cwd)?,
|
||||||
|
})),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
SlashCommand::Unknown(name) => Err(format_unknown_slash_command(name).into()),
|
SlashCommand::Unknown(name) => Err(format_unknown_slash_command(name).into()),
|
||||||
@@ -1751,37 +1867,38 @@ impl RuntimeMcpState {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|server_name| !failed_server_names.contains(server_name))
|
.filter(|server_name| !failed_server_names.contains(server_name))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let failed_servers = discovery
|
let failed_servers =
|
||||||
.failed_servers
|
discovery
|
||||||
.iter()
|
.failed_servers
|
||||||
.map(|failure| runtime::McpFailedServer {
|
.iter()
|
||||||
server_name: failure.server_name.clone(),
|
.map(|failure| runtime::McpFailedServer {
|
||||||
phase: runtime::McpLifecyclePhase::ToolDiscovery,
|
server_name: failure.server_name.clone(),
|
||||||
error: runtime::McpErrorSurface::new(
|
phase: runtime::McpLifecyclePhase::ToolDiscovery,
|
||||||
runtime::McpLifecyclePhase::ToolDiscovery,
|
|
||||||
Some(failure.server_name.clone()),
|
|
||||||
failure.error.clone(),
|
|
||||||
std::collections::BTreeMap::new(),
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
})
|
|
||||||
.chain(discovery.unsupported_servers.iter().map(|server| {
|
|
||||||
runtime::McpFailedServer {
|
|
||||||
server_name: server.server_name.clone(),
|
|
||||||
phase: runtime::McpLifecyclePhase::ServerRegistration,
|
|
||||||
error: runtime::McpErrorSurface::new(
|
error: runtime::McpErrorSurface::new(
|
||||||
runtime::McpLifecyclePhase::ServerRegistration,
|
runtime::McpLifecyclePhase::ToolDiscovery,
|
||||||
Some(server.server_name.clone()),
|
Some(failure.server_name.clone()),
|
||||||
server.reason.clone(),
|
failure.error.clone(),
|
||||||
std::collections::BTreeMap::from([(
|
std::collections::BTreeMap::new(),
|
||||||
"transport".to_string(),
|
true,
|
||||||
format!("{:?}", server.transport).to_ascii_lowercase(),
|
|
||||||
)]),
|
|
||||||
false,
|
|
||||||
),
|
),
|
||||||
}
|
})
|
||||||
}))
|
.chain(discovery.unsupported_servers.iter().map(|server| {
|
||||||
.collect::<Vec<_>>();
|
runtime::McpFailedServer {
|
||||||
|
server_name: server.server_name.clone(),
|
||||||
|
phase: runtime::McpLifecyclePhase::ServerRegistration,
|
||||||
|
error: runtime::McpErrorSurface::new(
|
||||||
|
runtime::McpLifecyclePhase::ServerRegistration,
|
||||||
|
Some(server.server_name.clone()),
|
||||||
|
server.reason.clone(),
|
||||||
|
std::collections::BTreeMap::from([(
|
||||||
|
"transport".to_string(),
|
||||||
|
format!("{:?}", server.transport).to_ascii_lowercase(),
|
||||||
|
)]),
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
let degraded_report = (!failed_servers.is_empty()).then(|| {
|
let degraded_report = (!failed_servers.is_empty()).then(|| {
|
||||||
runtime::McpDegradedReport::new(
|
runtime::McpDegradedReport::new(
|
||||||
working_servers,
|
working_servers,
|
||||||
@@ -3179,22 +3296,31 @@ fn render_repl_help() -> String {
|
|||||||
fn print_status_snapshot(
|
fn print_status_snapshot(
|
||||||
model: &str,
|
model: &str,
|
||||||
permission_mode: PermissionMode,
|
permission_mode: PermissionMode,
|
||||||
|
output_format: CliOutputFormat,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
println!(
|
let usage = StatusUsage {
|
||||||
"{}",
|
message_count: 0,
|
||||||
format_status_report(
|
turns: 0,
|
||||||
model,
|
latest: TokenUsage::default(),
|
||||||
StatusUsage {
|
cumulative: TokenUsage::default(),
|
||||||
message_count: 0,
|
estimated_tokens: 0,
|
||||||
turns: 0,
|
};
|
||||||
latest: TokenUsage::default(),
|
let context = status_context(None)?;
|
||||||
cumulative: TokenUsage::default(),
|
match output_format {
|
||||||
estimated_tokens: 0,
|
CliOutputFormat::Text => println!(
|
||||||
},
|
"{}",
|
||||||
permission_mode.as_str(),
|
format_status_report(model, usage, permission_mode.as_str(), &context)
|
||||||
&status_context(None)?,
|
),
|
||||||
)
|
CliOutputFormat::Json => println!(
|
||||||
);
|
"{}",
|
||||||
|
serialize_json_output(&status_report_json(
|
||||||
|
model,
|
||||||
|
usage,
|
||||||
|
permission_mode.as_str(),
|
||||||
|
&context,
|
||||||
|
))?
|
||||||
|
),
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3292,6 +3418,61 @@ fn format_status_report(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn status_report_json(
|
||||||
|
model: &str,
|
||||||
|
usage: StatusUsage,
|
||||||
|
permission_mode: &str,
|
||||||
|
context: &StatusContext,
|
||||||
|
) -> Value {
|
||||||
|
json!({
|
||||||
|
"kind": "status",
|
||||||
|
"model": model,
|
||||||
|
"permission_mode": permission_mode,
|
||||||
|
"messages": usage.message_count,
|
||||||
|
"turns": usage.turns,
|
||||||
|
"estimated_tokens": usage.estimated_tokens,
|
||||||
|
"usage": {
|
||||||
|
"latest": token_usage_json(usage.latest),
|
||||||
|
"cumulative": token_usage_json(usage.cumulative),
|
||||||
|
},
|
||||||
|
"workspace": {
|
||||||
|
"cwd": context.cwd.display().to_string(),
|
||||||
|
"project_root": context.project_root.as_ref().map(|path| path.display().to_string()),
|
||||||
|
"git_branch": context.git_branch.clone(),
|
||||||
|
"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": status_session_label(context.session_path.as_deref()),
|
||||||
|
"config_files": {
|
||||||
|
"loaded": context.loaded_config_files,
|
||||||
|
"discovered": context.discovered_config_files,
|
||||||
|
},
|
||||||
|
"memory_files": context.memory_file_count,
|
||||||
|
"suggested_flow": ["/status", "/diff", "/commit"],
|
||||||
|
},
|
||||||
|
"sandbox": sandbox_status_json(&context.sandbox_status),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn token_usage_json(usage: TokenUsage) -> Value {
|
||||||
|
json!({
|
||||||
|
"input_tokens": usage.input_tokens,
|
||||||
|
"output_tokens": usage.output_tokens,
|
||||||
|
"cache_creation_input_tokens": usage.cache_creation_input_tokens,
|
||||||
|
"cache_read_input_tokens": usage.cache_read_input_tokens,
|
||||||
|
"total_tokens": usage.total_tokens(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn status_session_label(session_path: Option<&Path>) -> String {
|
||||||
|
session_path.map_or_else(
|
||||||
|
|| "live-repl".to_string(),
|
||||||
|
|path| path.display().to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn format_sandbox_report(status: &runtime::SandboxStatus) -> String {
|
fn format_sandbox_report(status: &runtime::SandboxStatus) -> String {
|
||||||
format!(
|
format!(
|
||||||
"Sandbox
|
"Sandbox
|
||||||
@@ -3335,6 +3516,31 @@ fn format_sandbox_report(status: &runtime::SandboxStatus) -> String {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sandbox_status_json(status: &runtime::SandboxStatus) -> Value {
|
||||||
|
json!({
|
||||||
|
"enabled": status.enabled,
|
||||||
|
"active": status.active,
|
||||||
|
"supported": status.supported,
|
||||||
|
"namespace_supported": status.namespace_supported,
|
||||||
|
"namespace_active": status.namespace_active,
|
||||||
|
"network_supported": status.network_supported,
|
||||||
|
"network_active": status.network_active,
|
||||||
|
"filesystem_mode": status.filesystem_mode.as_str(),
|
||||||
|
"filesystem_active": status.filesystem_active,
|
||||||
|
"allowed_mounts": status.allowed_mounts.clone(),
|
||||||
|
"in_container": status.in_container,
|
||||||
|
"container_markers": status.container_markers.clone(),
|
||||||
|
"fallback_reason": status.fallback_reason.clone(),
|
||||||
|
"requested": {
|
||||||
|
"enabled": status.requested.enabled,
|
||||||
|
"namespace_restrictions": status.requested.namespace_restrictions,
|
||||||
|
"network_isolation": status.requested.network_isolation,
|
||||||
|
"filesystem_mode": status.requested.filesystem_mode.as_str(),
|
||||||
|
"allowed_mounts": status.requested.allowed_mounts.clone(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn format_commit_preflight_report(branch: Option<&str>, summary: GitWorkspaceSummary) -> String {
|
fn format_commit_preflight_report(branch: Option<&str>, summary: GitWorkspaceSummary) -> String {
|
||||||
format!(
|
format!(
|
||||||
"Commit
|
"Commit
|
||||||
@@ -3358,16 +3564,25 @@ fn format_commit_skipped_report() -> String {
|
|||||||
.to_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 cwd = env::current_dir()?;
|
||||||
let loader = ConfigLoader::default_for(&cwd);
|
let loader = ConfigLoader::default_for(&cwd);
|
||||||
let runtime_config = loader
|
let runtime_config = loader
|
||||||
.load()
|
.load()
|
||||||
.unwrap_or_else(|_| runtime::RuntimeConfig::empty());
|
.unwrap_or_else(|_| runtime::RuntimeConfig::empty());
|
||||||
println!(
|
let sandbox_status = resolve_sandbox_status(runtime_config.sandbox(), &cwd);
|
||||||
"{}",
|
match output_format {
|
||||||
format_sandbox_report(&resolve_sandbox_status(runtime_config.sandbox(), &cwd))
|
CliOutputFormat::Text => println!("{}", format_sandbox_report(&sandbox_status)),
|
||||||
);
|
CliOutputFormat::Json => println!(
|
||||||
|
"{}",
|
||||||
|
serialize_json_output(&json!({
|
||||||
|
"kind": "sandbox",
|
||||||
|
"sandbox": sandbox_status_json(&sandbox_status),
|
||||||
|
}))?
|
||||||
|
),
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5650,12 +5865,12 @@ mod tests {
|
|||||||
parse_git_status_branch, parse_git_status_metadata_for, parse_git_workspace_summary,
|
parse_git_status_branch, parse_git_status_metadata_for, parse_git_workspace_summary,
|
||||||
permission_policy, print_help_to, push_output_block, render_config_report,
|
permission_policy, print_help_to, push_output_block, render_config_report,
|
||||||
render_diff_report, render_diff_report_for, render_memory_report, render_repl_help,
|
render_diff_report, render_diff_report_for, render_memory_report, render_repl_help,
|
||||||
render_resume_usage, resolve_model_alias, resolve_session_reference, response_to_events,
|
render_resume_text_output, render_resume_usage, resolve_model_alias,
|
||||||
resume_supported_slash_commands, run_resume_command,
|
resolve_session_reference, response_to_events, resume_supported_slash_commands,
|
||||||
slash_command_completion_candidates_with_sessions, status_context, validate_no_args,
|
run_resume_command, slash_command_completion_candidates_with_sessions, status_context,
|
||||||
write_mcp_server_fixture, CliAction, CliOutputFormat, CliToolExecutor, GitWorkspaceSummary,
|
validate_no_args, write_mcp_server_fixture, CliAction, CliOutputFormat, CliToolExecutor,
|
||||||
InternalPromptProgressEvent, InternalPromptProgressState, LiveCli, SlashCommand,
|
GitWorkspaceSummary, InternalPromptProgressEvent, InternalPromptProgressState, LiveCli,
|
||||||
StatusUsage, DEFAULT_MODEL,
|
SlashCommand, StatusUsage, DEFAULT_MODEL,
|
||||||
};
|
};
|
||||||
use api::{MessageResponse, OutputContentBlock, Usage};
|
use api::{MessageResponse, OutputContentBlock, Usage};
|
||||||
use plugins::{
|
use plugins::{
|
||||||
@@ -6063,11 +6278,36 @@ mod tests {
|
|||||||
CliAction::Status {
|
CliAction::Status {
|
||||||
model: DEFAULT_MODEL.to_string(),
|
model: DEFAULT_MODEL.to_string(),
|
||||||
permission_mode: PermissionMode::DangerFullAccess,
|
permission_mode: PermissionMode::DangerFullAccess,
|
||||||
|
output_format: CliOutputFormat::Text,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_args(&["sandbox".to_string()]).expect("sandbox should parse"),
|
parse_args(&["sandbox".to_string()]).expect("sandbox should parse"),
|
||||||
CliAction::Sandbox
|
CliAction::Sandbox {
|
||||||
|
output_format: CliOutputFormat::Text,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_json_output_for_status_and_sandbox_aliases() {
|
||||||
|
let _guard = env_lock();
|
||||||
|
std::env::remove_var("RUSTY_CLAUDE_PERMISSION_MODE");
|
||||||
|
assert_eq!(
|
||||||
|
parse_args(&["--output-format=json".to_string(), "status".to_string()])
|
||||||
|
.expect("json status should parse"),
|
||||||
|
CliAction::Status {
|
||||||
|
model: DEFAULT_MODEL.to_string(),
|
||||||
|
permission_mode: PermissionMode::DangerFullAccess,
|
||||||
|
output_format: CliOutputFormat::Json,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_args(&["--output-format=json".to_string(), "sandbox".to_string()])
|
||||||
|
.expect("json sandbox should parse"),
|
||||||
|
CliAction::Sandbox {
|
||||||
|
output_format: CliOutputFormat::Json,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6173,6 +6413,7 @@ mod tests {
|
|||||||
CliAction::ResumeSession {
|
CliAction::ResumeSession {
|
||||||
session_path: PathBuf::from("session.jsonl"),
|
session_path: PathBuf::from("session.jsonl"),
|
||||||
commands: vec!["/compact".to_string()],
|
commands: vec!["/compact".to_string()],
|
||||||
|
output_format: CliOutputFormat::Text,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -6184,6 +6425,7 @@ mod tests {
|
|||||||
CliAction::ResumeSession {
|
CliAction::ResumeSession {
|
||||||
session_path: PathBuf::from("latest"),
|
session_path: PathBuf::from("latest"),
|
||||||
commands: vec![],
|
commands: vec![],
|
||||||
|
output_format: CliOutputFormat::Text,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -6192,6 +6434,7 @@ mod tests {
|
|||||||
CliAction::ResumeSession {
|
CliAction::ResumeSession {
|
||||||
session_path: PathBuf::from("latest"),
|
session_path: PathBuf::from("latest"),
|
||||||
commands: vec!["/status".to_string()],
|
commands: vec!["/status".to_string()],
|
||||||
|
output_format: CliOutputFormat::Text,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -6214,6 +6457,7 @@ mod tests {
|
|||||||
"/compact".to_string(),
|
"/compact".to_string(),
|
||||||
"/cost".to_string(),
|
"/cost".to_string(),
|
||||||
],
|
],
|
||||||
|
output_format: CliOutputFormat::Text,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -6244,6 +6488,7 @@ mod tests {
|
|||||||
"/export notes.txt".to_string(),
|
"/export notes.txt".to_string(),
|
||||||
"/clear --confirm".to_string(),
|
"/clear --confirm".to_string(),
|
||||||
],
|
],
|
||||||
|
output_format: CliOutputFormat::Text,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -6262,6 +6507,25 @@ mod tests {
|
|||||||
CliAction::ResumeSession {
|
CliAction::ResumeSession {
|
||||||
session_path: PathBuf::from("session.jsonl"),
|
session_path: PathBuf::from("session.jsonl"),
|
||||||
commands: vec!["/export /tmp/notes.txt".to_string(), "/status".to_string()],
|
commands: vec!["/export /tmp/notes.txt".to_string(), "/status".to_string()],
|
||||||
|
output_format: CliOutputFormat::Text,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_json_output_for_resumed_status_queries() {
|
||||||
|
let args = vec![
|
||||||
|
"--output-format=json".to_string(),
|
||||||
|
"--resume".to_string(),
|
||||||
|
"session.jsonl".to_string(),
|
||||||
|
"/status".to_string(),
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
parse_args(&args).expect("json resume status should parse"),
|
||||||
|
CliAction::ResumeSession {
|
||||||
|
session_path: PathBuf::from("session.jsonl"),
|
||||||
|
commands: vec!["/status".to_string()],
|
||||||
|
output_format: CliOutputFormat::Json,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -6782,10 +7046,16 @@ UU conflicted.rs",
|
|||||||
|
|
||||||
let session = Session::load_from_path(&session_path).expect("session should load");
|
let session = Session::load_from_path(&session_path).expect("session should load");
|
||||||
let outcome = with_current_dir(&root, || {
|
let outcome = with_current_dir(&root, || {
|
||||||
run_resume_command(&session_path, &session, &SlashCommand::Diff)
|
run_resume_command(
|
||||||
.expect("resume diff should work")
|
&session_path,
|
||||||
|
&session,
|
||||||
|
&SlashCommand::Diff,
|
||||||
|
CliOutputFormat::Text,
|
||||||
|
)
|
||||||
|
.expect("resume diff should work")
|
||||||
});
|
});
|
||||||
let message = outcome.message.expect("diff message should exist");
|
let message =
|
||||||
|
render_resume_text_output(&outcome.message.expect("diff message should exist"));
|
||||||
assert!(message.contains("Unstaged changes:"));
|
assert!(message.contains("Unstaged changes:"));
|
||||||
assert!(message.contains("tracked.txt"));
|
assert!(message.contains("tracked.txt"));
|
||||||
|
|
||||||
@@ -7509,8 +7779,12 @@ UU conflicted.rs",
|
|||||||
let runtime_config = loader.load().expect("runtime config should load");
|
let runtime_config = loader.load().expect("runtime config should load");
|
||||||
let state = build_runtime_plugin_state_with_loader(&workspace, &loader, &runtime_config)
|
let state = build_runtime_plugin_state_with_loader(&workspace, &loader, &runtime_config)
|
||||||
.expect("runtime plugin state should load");
|
.expect("runtime plugin state should load");
|
||||||
let mut executor =
|
let mut executor = CliToolExecutor::new(
|
||||||
CliToolExecutor::new(None, false, state.tool_registry.clone(), state.mcp_state.clone());
|
None,
|
||||||
|
false,
|
||||||
|
state.tool_registry.clone(),
|
||||||
|
state.mcp_state.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
let search_output = executor
|
let search_output = executor
|
||||||
.execute("ToolSearch", r#"{"query":"remote","max_results":5}"#)
|
.execute("ToolSearch", r#"{"query":"remote","max_results":5}"#)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use std::sync::atomic::{AtomicU64, Ordering};
|
|||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use runtime::Session;
|
use runtime::Session;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
|
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
@@ -37,6 +38,64 @@ fn status_command_applies_model_and_permission_mode_flags() {
|
|||||||
fs::remove_dir_all(temp_dir).expect("cleanup temp dir");
|
fs::remove_dir_all(temp_dir).expect("cleanup temp dir");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn status_command_emits_structured_json_when_requested() {
|
||||||
|
// given
|
||||||
|
let temp_dir = unique_temp_dir("status-json");
|
||||||
|
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
|
||||||
|
|
||||||
|
// when
|
||||||
|
let output = Command::new(env!("CARGO_BIN_EXE_claw"))
|
||||||
|
.current_dir(&temp_dir)
|
||||||
|
.args([
|
||||||
|
"--model",
|
||||||
|
"sonnet",
|
||||||
|
"--permission-mode",
|
||||||
|
"read-only",
|
||||||
|
"--output-format",
|
||||||
|
"json",
|
||||||
|
"status",
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.expect("claw should launch");
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert_success(&output);
|
||||||
|
let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
|
||||||
|
let parsed: Value = serde_json::from_str(stdout.trim()).expect("status output should be json");
|
||||||
|
assert_eq!(parsed["kind"], "status");
|
||||||
|
assert_eq!(parsed["model"], "claude-sonnet-4-6");
|
||||||
|
assert_eq!(parsed["permission_mode"], "read-only");
|
||||||
|
assert_eq!(parsed["workspace"]["session"], "live-repl");
|
||||||
|
assert!(parsed["sandbox"].is_object());
|
||||||
|
|
||||||
|
fs::remove_dir_all(temp_dir).expect("cleanup temp dir");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sandbox_command_emits_structured_json_when_requested() {
|
||||||
|
// given
|
||||||
|
let temp_dir = unique_temp_dir("sandbox-json");
|
||||||
|
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
|
||||||
|
|
||||||
|
// when
|
||||||
|
let output = Command::new(env!("CARGO_BIN_EXE_claw"))
|
||||||
|
.current_dir(&temp_dir)
|
||||||
|
.args(["--output-format", "json", "sandbox"])
|
||||||
|
.output()
|
||||||
|
.expect("claw should launch");
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert_success(&output);
|
||||||
|
let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
|
||||||
|
let parsed: Value = serde_json::from_str(stdout.trim()).expect("sandbox output should be json");
|
||||||
|
assert_eq!(parsed["kind"], "sandbox");
|
||||||
|
assert!(parsed["sandbox"].is_object());
|
||||||
|
assert!(parsed["sandbox"]["requested"].is_object());
|
||||||
|
|
||||||
|
fs::remove_dir_all(temp_dir).expect("cleanup temp dir");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resume_flag_loads_a_saved_session_and_dispatches_status() {
|
fn resume_flag_loads_a_saved_session_and_dispatches_status() {
|
||||||
// given
|
// given
|
||||||
@@ -160,42 +219,6 @@ fn config_command_loads_defaults_from_standard_config_locations() {
|
|||||||
fs::remove_dir_all(temp_dir).expect("cleanup temp dir");
|
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn command_in(cwd: &Path) -> Command {
|
fn command_in(cwd: &Path) -> Command {
|
||||||
let mut command = Command::new(env!("CARGO_BIN_EXE_claw"));
|
let mut command = Command::new(env!("CARGO_BIN_EXE_claw"));
|
||||||
command.current_dir(cwd);
|
command.current_dir(cwd);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
|||||||
|
|
||||||
use runtime::ContentBlock;
|
use runtime::ContentBlock;
|
||||||
use runtime::Session;
|
use runtime::Session;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
|
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
@@ -221,6 +222,52 @@ fn resume_latest_restores_the_most_recent_managed_session() {
|
|||||||
assert!(stdout.contains(newer_path.to_str().expect("utf8 path")));
|
assert!(stdout.contains(newer_path.to_str().expect("utf8 path")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resumed_status_command_emits_structured_json_when_requested() {
|
||||||
|
// given
|
||||||
|
let temp_dir = unique_temp_dir("resume-status-json");
|
||||||
|
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
|
||||||
|
let session_path = temp_dir.join("session.jsonl");
|
||||||
|
|
||||||
|
let mut session = Session::new();
|
||||||
|
session
|
||||||
|
.push_user_text("resume status json fixture")
|
||||||
|
.expect("session write should succeed");
|
||||||
|
session
|
||||||
|
.save_to_path(&session_path)
|
||||||
|
.expect("session should persist");
|
||||||
|
|
||||||
|
// when
|
||||||
|
let output = run_claw(
|
||||||
|
&temp_dir,
|
||||||
|
&[
|
||||||
|
"--output-format",
|
||||||
|
"json",
|
||||||
|
"--resume",
|
||||||
|
session_path.to_str().expect("utf8 path"),
|
||||||
|
"/status",
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"stdout:\n{}\n\nstderr:\n{}",
|
||||||
|
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.trim()).expect("resume status output should be json");
|
||||||
|
assert_eq!(parsed["kind"], "status");
|
||||||
|
assert_eq!(parsed["messages"], 1);
|
||||||
|
assert_eq!(
|
||||||
|
parsed["workspace"]["session"],
|
||||||
|
session_path.to_str().expect("utf8 path")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn run_claw(current_dir: &Path, args: &[&str]) -> Output {
|
fn run_claw(current_dir: &Path, args: &[&str]) -> Output {
|
||||||
run_claw_with_env(current_dir, args, &[])
|
run_claw_with_env(current_dir, args, &[])
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user