mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-08 00:54:49 +08:00
feat: b5-multi-provider — batch 5 upstream parity
This commit is contained in:
@@ -164,6 +164,11 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
CliAction::Logout { output_format } => run_logout(output_format)?,
|
||||
CliAction::Doctor { output_format } => run_doctor(output_format)?,
|
||||
CliAction::Init { output_format } => run_init(output_format)?,
|
||||
CliAction::Export {
|
||||
session_reference,
|
||||
output_path,
|
||||
output_format,
|
||||
} => run_export(&session_reference, output_path.as_deref(), output_format)?,
|
||||
CliAction::Repl {
|
||||
model,
|
||||
allowed_tools,
|
||||
@@ -241,6 +246,11 @@ enum CliAction {
|
||||
Init {
|
||||
output_format: CliOutputFormat,
|
||||
},
|
||||
Export {
|
||||
session_reference: String,
|
||||
output_path: Option<PathBuf>,
|
||||
output_format: CliOutputFormat,
|
||||
},
|
||||
Repl {
|
||||
model: String,
|
||||
allowed_tools: Option<AllowedToolSet>,
|
||||
@@ -460,6 +470,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||
"login" => Ok(CliAction::Login { output_format }),
|
||||
"logout" => Ok(CliAction::Logout { output_format }),
|
||||
"init" => Ok(CliAction::Init { output_format }),
|
||||
"export" => parse_export_args(&rest[1..], output_format),
|
||||
"prompt" => {
|
||||
let prompt = rest[1..].join(" ");
|
||||
if prompt.trim().is_empty() {
|
||||
@@ -548,6 +559,7 @@ fn bare_slash_command_guidance(command_name: &str) -> Option<String> {
|
||||
| "logout"
|
||||
| "init"
|
||||
| "prompt"
|
||||
| "export"
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
@@ -912,6 +924,58 @@ fn parse_system_prompt_args(
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_export_args(
|
||||
args: &[String],
|
||||
output_format: CliOutputFormat,
|
||||
) -> Result<CliAction, String> {
|
||||
let mut session_reference = LATEST_SESSION_REFERENCE.to_string();
|
||||
let mut output_path: Option<PathBuf> = None;
|
||||
let mut index = 0;
|
||||
|
||||
while index < args.len() {
|
||||
match args[index].as_str() {
|
||||
"--session" => {
|
||||
let value = args
|
||||
.get(index + 1)
|
||||
.ok_or_else(|| "missing value for --session".to_string())?;
|
||||
session_reference = value.clone();
|
||||
index += 2;
|
||||
}
|
||||
flag if flag.starts_with("--session=") => {
|
||||
session_reference = flag[10..].to_string();
|
||||
index += 1;
|
||||
}
|
||||
"--output" | "-o" => {
|
||||
let value = args
|
||||
.get(index + 1)
|
||||
.ok_or_else(|| format!("missing value for {}", args[index]))?;
|
||||
output_path = Some(PathBuf::from(value));
|
||||
index += 2;
|
||||
}
|
||||
flag if flag.starts_with("--output=") => {
|
||||
output_path = Some(PathBuf::from(&flag[9..]));
|
||||
index += 1;
|
||||
}
|
||||
other if other.starts_with('-') => {
|
||||
return Err(format!("unknown export option: {other}"));
|
||||
}
|
||||
other if output_path.is_none() => {
|
||||
output_path = Some(PathBuf::from(other));
|
||||
index += 1;
|
||||
}
|
||||
other => {
|
||||
return Err(format!("unexpected export argument: {other}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CliAction::Export {
|
||||
session_reference,
|
||||
output_path,
|
||||
output_format,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_resume_args(args: &[String], output_format: CliOutputFormat) -> Result<CliAction, String> {
|
||||
let (session_path, command_tokens): (PathBuf, &[String]) = match args.first() {
|
||||
None => (PathBuf::from(LATEST_SESSION_REFERENCE), &[]),
|
||||
@@ -5257,6 +5321,172 @@ fn resolve_export_path(
|
||||
Ok(cwd.join(final_name))
|
||||
}
|
||||
|
||||
const SESSION_MARKDOWN_TOOL_SUMMARY_LIMIT: usize = 280;
|
||||
|
||||
fn summarize_tool_payload_for_markdown(payload: &str) -> String {
|
||||
let compact = match serde_json::from_str::<serde_json::Value>(payload) {
|
||||
Ok(value) => value.to_string(),
|
||||
Err(_) => payload.split_whitespace().collect::<Vec<_>>().join(" "),
|
||||
};
|
||||
if compact.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
truncate_for_summary(&compact, SESSION_MARKDOWN_TOOL_SUMMARY_LIMIT)
|
||||
}
|
||||
|
||||
fn run_export(
|
||||
session_reference: &str,
|
||||
output_path: Option<&Path>,
|
||||
output_format: CliOutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let handle = resolve_session_reference(session_reference)?;
|
||||
let session = Session::load_from_path(&handle.path)?;
|
||||
let markdown = render_session_markdown(&session, &handle.id, &handle.path);
|
||||
|
||||
if let Some(path) = output_path {
|
||||
fs::write(path, &markdown)?;
|
||||
let report = format!(
|
||||
"Export\n Result wrote markdown transcript\n File {}\n Session {}\n Messages {}",
|
||||
path.display(),
|
||||
handle.id,
|
||||
session.messages.len(),
|
||||
);
|
||||
match output_format {
|
||||
CliOutputFormat::Text => println!("{report}"),
|
||||
CliOutputFormat::Json => println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&json!({
|
||||
"kind": "export",
|
||||
"message": report,
|
||||
"session_id": handle.id,
|
||||
"file": path.display().to_string(),
|
||||
"messages": session.messages.len(),
|
||||
}))?
|
||||
),
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match output_format {
|
||||
CliOutputFormat::Text => {
|
||||
print!("{markdown}");
|
||||
if !markdown.ends_with('\n') {
|
||||
println!();
|
||||
}
|
||||
}
|
||||
CliOutputFormat::Json => println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&json!({
|
||||
"kind": "export",
|
||||
"session_id": handle.id,
|
||||
"file": handle.path.display().to_string(),
|
||||
"messages": session.messages.len(),
|
||||
"markdown": markdown,
|
||||
}))?
|
||||
),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_session_markdown(session: &Session, session_id: &str, session_path: &Path) -> String {
|
||||
let mut lines = vec![
|
||||
"# Conversation Export".to_string(),
|
||||
String::new(),
|
||||
format!("- **Session**: `{session_id}`"),
|
||||
format!("- **File**: `{}`", session_path.display()),
|
||||
format!("- **Messages**: {}", session.messages.len()),
|
||||
];
|
||||
if let Some(workspace_root) = session.workspace_root() {
|
||||
lines.push(format!("- **Workspace**: `{}`", workspace_root.display()));
|
||||
}
|
||||
if let Some(fork) = &session.fork {
|
||||
let branch = fork.branch_name.as_deref().unwrap_or("(unnamed)");
|
||||
lines.push(format!(
|
||||
"- **Forked from**: `{}` (branch `{branch}`)",
|
||||
fork.parent_session_id
|
||||
));
|
||||
}
|
||||
if let Some(compaction) = &session.compaction {
|
||||
lines.push(format!(
|
||||
"- **Compactions**: {} (last removed {} messages)",
|
||||
compaction.count, compaction.removed_message_count
|
||||
));
|
||||
}
|
||||
lines.push(String::new());
|
||||
lines.push("---".to_string());
|
||||
lines.push(String::new());
|
||||
|
||||
for (index, message) in session.messages.iter().enumerate() {
|
||||
let role = match message.role {
|
||||
MessageRole::System => "System",
|
||||
MessageRole::User => "User",
|
||||
MessageRole::Assistant => "Assistant",
|
||||
MessageRole::Tool => "Tool",
|
||||
};
|
||||
lines.push(format!("## {}. {role}", index + 1));
|
||||
lines.push(String::new());
|
||||
for block in &message.blocks {
|
||||
match block {
|
||||
ContentBlock::Text { text } => {
|
||||
let trimmed = text.trim_end();
|
||||
if !trimmed.is_empty() {
|
||||
lines.push(trimmed.to_string());
|
||||
lines.push(String::new());
|
||||
}
|
||||
}
|
||||
ContentBlock::ToolUse { id, name, input } => {
|
||||
lines.push(format!(
|
||||
"**Tool call** `{name}` _(id `{}`)_",
|
||||
short_tool_id(id)
|
||||
));
|
||||
let summary = summarize_tool_payload_for_markdown(input);
|
||||
if !summary.is_empty() {
|
||||
lines.push(format!("> {summary}"));
|
||||
}
|
||||
lines.push(String::new());
|
||||
}
|
||||
ContentBlock::ToolResult {
|
||||
tool_use_id,
|
||||
tool_name,
|
||||
output,
|
||||
is_error,
|
||||
} => {
|
||||
let status = if *is_error { "error" } else { "ok" };
|
||||
lines.push(format!(
|
||||
"**Tool result** `{tool_name}` _(id `{}`, {status})_",
|
||||
short_tool_id(tool_use_id)
|
||||
));
|
||||
let summary = summarize_tool_payload_for_markdown(output);
|
||||
if !summary.is_empty() {
|
||||
lines.push(format!("> {summary}"));
|
||||
}
|
||||
lines.push(String::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(usage) = message.usage {
|
||||
lines.push(format!(
|
||||
"_tokens: in={} out={} cache_create={} cache_read={}_",
|
||||
usage.input_tokens,
|
||||
usage.output_tokens,
|
||||
usage.cache_creation_input_tokens,
|
||||
usage.cache_read_input_tokens,
|
||||
));
|
||||
lines.push(String::new());
|
||||
}
|
||||
}
|
||||
lines.join("\n")
|
||||
}
|
||||
|
||||
fn short_tool_id(id: &str) -> String {
|
||||
let char_count = id.chars().count();
|
||||
if char_count <= 12 {
|
||||
return id.to_string();
|
||||
}
|
||||
let prefix: String = id.chars().take(12).collect();
|
||||
format!("{prefix}…")
|
||||
}
|
||||
|
||||
fn build_system_prompt() -> Result<Vec<String>, Box<dyn std::error::Error>> {
|
||||
Ok(load_system_prompt(
|
||||
env::current_dir()?,
|
||||
@@ -7068,6 +7298,11 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> {
|
||||
writeln!(out, " claw login")?;
|
||||
writeln!(out, " claw logout")?;
|
||||
writeln!(out, " claw init")?;
|
||||
writeln!(out, " claw export [PATH] [--session SESSION] [--output PATH]")?;
|
||||
writeln!(
|
||||
out,
|
||||
" Dump the latest (or named) session as markdown; writes to PATH or stdout"
|
||||
)?;
|
||||
writeln!(out)?;
|
||||
writeln!(out, "Flags:")?;
|
||||
writeln!(
|
||||
@@ -7147,6 +7382,8 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> {
|
||||
writeln!(out, " claw doctor")?;
|
||||
writeln!(out, " claw login")?;
|
||||
writeln!(out, " claw init")?;
|
||||
writeln!(out, " claw export")?;
|
||||
writeln!(out, " claw export conversation.md")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -7180,17 +7417,18 @@ mod tests {
|
||||
format_resume_report, format_status_report, format_tool_call_start, format_tool_result,
|
||||
format_ultraplan_report, format_unknown_slash_command,
|
||||
format_unknown_slash_command_message, format_user_visible_api_error,
|
||||
normalize_permission_mode, parse_args, parse_git_status_branch,
|
||||
parse_git_status_metadata_for, parse_git_workspace_summary, parse_history_count,
|
||||
permission_policy, print_help_to, push_output_block, render_config_report,
|
||||
render_diff_report, render_diff_report_for, render_memory_report,
|
||||
render_prompt_history_report, render_repl_help, render_resume_usage, resolve_model_alias,
|
||||
resolve_repl_model, resolve_session_reference, response_to_events,
|
||||
resume_supported_slash_commands, run_resume_command,
|
||||
slash_command_completion_candidates_with_sessions, status_context, validate_no_args,
|
||||
write_mcp_server_fixture, CliAction, CliOutputFormat, CliToolExecutor, GitWorkspaceSummary,
|
||||
normalize_permission_mode, parse_args, parse_export_args, parse_git_status_branch,
|
||||
parse_git_status_metadata_for, parse_git_workspace_summary, 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_resume_usage,
|
||||
render_session_markdown, resolve_model_alias, resolve_repl_model,
|
||||
resolve_session_reference, response_to_events, resume_supported_slash_commands,
|
||||
run_resume_command, short_tool_id,
|
||||
slash_command_completion_candidates_with_sessions, status_context,
|
||||
summarize_tool_payload_for_markdown, validate_no_args, write_mcp_server_fixture,
|
||||
CliAction, CliOutputFormat, CliToolExecutor, GitWorkspaceSummary,
|
||||
InternalPromptProgressEvent, InternalPromptProgressState, LiveCli, LocalHelpTopic,
|
||||
PromptHistoryEntry, SlashCommand, StatusUsage, DEFAULT_MODEL,
|
||||
SlashCommand, StatusUsage, DEFAULT_MODEL, LATEST_SESSION_REFERENCE,
|
||||
};
|
||||
use api::{ApiError, MessageResponse, OutputContentBlock, Usage};
|
||||
use plugins::{
|
||||
@@ -7982,6 +8220,277 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_bare_export_subcommand_targeting_latest_session() {
|
||||
// given
|
||||
let _guard = env_lock();
|
||||
std::env::remove_var("RUSTY_CLAUDE_PERMISSION_MODE");
|
||||
let args = vec!["export".to_string()];
|
||||
|
||||
// when
|
||||
let parsed = parse_args(&args).expect("bare export should parse");
|
||||
|
||||
// then
|
||||
assert_eq!(
|
||||
parsed,
|
||||
CliAction::Export {
|
||||
session_reference: LATEST_SESSION_REFERENCE.to_string(),
|
||||
output_path: None,
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_export_subcommand_with_positional_output_path() {
|
||||
// given
|
||||
let args = vec!["export".to_string(), "conversation.md".to_string()];
|
||||
|
||||
// when
|
||||
let parsed = parse_args(&args).expect("export with path should parse");
|
||||
|
||||
// then
|
||||
assert_eq!(
|
||||
parsed,
|
||||
CliAction::Export {
|
||||
session_reference: LATEST_SESSION_REFERENCE.to_string(),
|
||||
output_path: Some(PathBuf::from("conversation.md")),
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_export_subcommand_with_session_and_output_flags() {
|
||||
// given
|
||||
let args = vec![
|
||||
"export".to_string(),
|
||||
"--session".to_string(),
|
||||
"session-alpha".to_string(),
|
||||
"--output".to_string(),
|
||||
"/tmp/share.md".to_string(),
|
||||
];
|
||||
|
||||
// when
|
||||
let parsed = parse_args(&args).expect("export flags should parse");
|
||||
|
||||
// then
|
||||
assert_eq!(
|
||||
parsed,
|
||||
CliAction::Export {
|
||||
session_reference: "session-alpha".to_string(),
|
||||
output_path: Some(PathBuf::from("/tmp/share.md")),
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_export_subcommand_with_inline_flag_values() {
|
||||
// given
|
||||
let args = vec![
|
||||
"export".to_string(),
|
||||
"--session=session-beta".to_string(),
|
||||
"--output=/tmp/beta.md".to_string(),
|
||||
];
|
||||
|
||||
// when
|
||||
let parsed = parse_args(&args).expect("export inline flags should parse");
|
||||
|
||||
// then
|
||||
assert_eq!(
|
||||
parsed,
|
||||
CliAction::Export {
|
||||
session_reference: "session-beta".to_string(),
|
||||
output_path: Some(PathBuf::from("/tmp/beta.md")),
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_export_subcommand_with_json_output_format() {
|
||||
// given
|
||||
let args = vec![
|
||||
"--output-format=json".to_string(),
|
||||
"export".to_string(),
|
||||
"/tmp/notes.md".to_string(),
|
||||
];
|
||||
|
||||
// when
|
||||
let parsed = parse_args(&args).expect("json export should parse");
|
||||
|
||||
// then
|
||||
assert_eq!(
|
||||
parsed,
|
||||
CliAction::Export {
|
||||
session_reference: LATEST_SESSION_REFERENCE.to_string(),
|
||||
output_path: Some(PathBuf::from("/tmp/notes.md")),
|
||||
output_format: CliOutputFormat::Json,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unknown_export_options_with_helpful_message() {
|
||||
// given
|
||||
let args = vec!["export".to_string(), "--bogus".to_string()];
|
||||
|
||||
// when
|
||||
let error = parse_args(&args).expect_err("unknown export option should fail");
|
||||
|
||||
// then
|
||||
assert!(error.contains("unknown export option: --bogus"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_export_with_extra_positional_after_path() {
|
||||
// given
|
||||
let args = vec![
|
||||
"export".to_string(),
|
||||
"first.md".to_string(),
|
||||
"second.md".to_string(),
|
||||
];
|
||||
|
||||
// when
|
||||
let error = parse_args(&args).expect_err("multiple positionals should fail");
|
||||
|
||||
// then
|
||||
assert!(error.contains("unexpected export argument: second.md"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_export_args_helper_defaults_to_latest_reference_and_no_output() {
|
||||
// given
|
||||
let args: Vec<String> = vec![];
|
||||
|
||||
// when
|
||||
let parsed = parse_export_args(&args, CliOutputFormat::Text)
|
||||
.expect("empty export args should parse");
|
||||
|
||||
// then
|
||||
assert_eq!(
|
||||
parsed,
|
||||
CliAction::Export {
|
||||
session_reference: LATEST_SESSION_REFERENCE.to_string(),
|
||||
output_path: None,
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_session_markdown_includes_header_and_summarized_tool_calls() {
|
||||
// given
|
||||
let mut session = Session::new();
|
||||
session.session_id = "session-export-test".to_string();
|
||||
session.messages = vec![
|
||||
ConversationMessage::user_text("How do I list files?"),
|
||||
ConversationMessage::assistant(vec![
|
||||
ContentBlock::Text {
|
||||
text: "I'll run a tool.".to_string(),
|
||||
},
|
||||
ContentBlock::ToolUse {
|
||||
id: "toolu_abcdefghijklmnop".to_string(),
|
||||
name: "bash".to_string(),
|
||||
input: r#"{"command":"ls -la"}"#.to_string(),
|
||||
},
|
||||
]),
|
||||
ConversationMessage {
|
||||
role: MessageRole::Tool,
|
||||
blocks: vec![ContentBlock::ToolResult {
|
||||
tool_use_id: "toolu_abcdefghijklmnop".to_string(),
|
||||
tool_name: "bash".to_string(),
|
||||
output: "total 8\ndrwxr-xr-x 2 user staff 64 Apr 7 12:00 .".to_string(),
|
||||
is_error: false,
|
||||
}],
|
||||
usage: None,
|
||||
},
|
||||
];
|
||||
|
||||
// when
|
||||
let markdown = render_session_markdown(
|
||||
&session,
|
||||
"session-export-test",
|
||||
std::path::Path::new("/tmp/sessions/session-export-test.jsonl"),
|
||||
);
|
||||
|
||||
// then
|
||||
assert!(markdown.starts_with("# Conversation Export"));
|
||||
assert!(markdown.contains("- **Session**: `session-export-test`"));
|
||||
assert!(markdown.contains("- **Messages**: 3"));
|
||||
assert!(markdown.contains("## 1. User"));
|
||||
assert!(markdown.contains("How do I list files?"));
|
||||
assert!(markdown.contains("## 2. Assistant"));
|
||||
assert!(markdown.contains("**Tool call** `bash`"));
|
||||
assert!(markdown.contains("toolu_abcdef…"));
|
||||
assert!(markdown.contains("ls -la"));
|
||||
assert!(markdown.contains("## 3. Tool"));
|
||||
assert!(markdown.contains("**Tool result** `bash`"));
|
||||
assert!(markdown.contains("ok"));
|
||||
assert!(markdown.contains("total 8"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_session_markdown_marks_tool_errors_and_skips_empty_summaries() {
|
||||
// given
|
||||
let mut session = Session::new();
|
||||
session.session_id = "errs".to_string();
|
||||
session.messages = vec![ConversationMessage {
|
||||
role: MessageRole::Tool,
|
||||
blocks: vec![ContentBlock::ToolResult {
|
||||
tool_use_id: "short".to_string(),
|
||||
tool_name: "read_file".to_string(),
|
||||
output: " ".to_string(),
|
||||
is_error: true,
|
||||
}],
|
||||
usage: None,
|
||||
}];
|
||||
|
||||
// when
|
||||
let markdown =
|
||||
render_session_markdown(&session, "errs", std::path::Path::new("errs.jsonl"));
|
||||
|
||||
// then
|
||||
assert!(markdown.contains("**Tool result** `read_file` _(id `short`, error)_"));
|
||||
// an empty summary should not produce a stray blockquote line
|
||||
assert!(!markdown.contains("> \n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summarize_tool_payload_for_markdown_compacts_json_and_truncates_overflow() {
|
||||
// given
|
||||
let json_payload = r#"{
|
||||
"command": "ls -la",
|
||||
"cwd": "/tmp"
|
||||
}"#;
|
||||
let long_payload = "a".repeat(600);
|
||||
|
||||
// when
|
||||
let compacted = summarize_tool_payload_for_markdown(json_payload);
|
||||
let truncated = summarize_tool_payload_for_markdown(&long_payload);
|
||||
|
||||
// then
|
||||
assert_eq!(compacted, r#"{"command":"ls -la","cwd":"/tmp"}"#);
|
||||
assert!(truncated.ends_with('…'));
|
||||
assert!(truncated.chars().count() <= 281);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_tool_id_truncates_long_identifiers_with_ellipsis() {
|
||||
// given
|
||||
let long = "toolu_01ABCDEFGHIJKLMN";
|
||||
let short = "tool_1";
|
||||
|
||||
// when
|
||||
let trimmed_long = short_tool_id(long);
|
||||
let trimmed_short = short_tool_id(short);
|
||||
|
||||
// then
|
||||
assert_eq!(trimmed_long, "toolu_01ABCD…");
|
||||
assert_eq!(trimmed_short, "tool_1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_json_output_for_mcp_and_skills_commands() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user