mirror of
https://github.com/instructkr/claude-code.git
synced 2026-06-04 11:36:44 +00:00
fix: keep session help local
This commit is contained in:
@@ -870,6 +870,9 @@ enum LocalHelpTopic {
|
||||
// `claw <subcommand> --help` has one consistent contract.
|
||||
Init,
|
||||
State,
|
||||
Resume,
|
||||
Session,
|
||||
Compact,
|
||||
Export,
|
||||
Version,
|
||||
SystemPrompt,
|
||||
@@ -1132,6 +1135,10 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||
"acp" => Some(LocalHelpTopic::Acp),
|
||||
"init" => Some(LocalHelpTopic::Init),
|
||||
"state" => Some(LocalHelpTopic::State),
|
||||
"resume" => Some(LocalHelpTopic::Resume),
|
||||
"session" => Some(LocalHelpTopic::Session),
|
||||
"compact" => Some(LocalHelpTopic::Compact),
|
||||
"--resume" => Some(LocalHelpTopic::Resume),
|
||||
"export" => Some(LocalHelpTopic::Export),
|
||||
"version" => Some(LocalHelpTopic::Version),
|
||||
"system-prompt" => Some(LocalHelpTopic::SystemPrompt),
|
||||
@@ -1218,11 +1225,14 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||
allow_broad_cwd,
|
||||
});
|
||||
}
|
||||
if let Some(action) = parse_local_help_action(&rest, output_format) {
|
||||
return action;
|
||||
}
|
||||
if rest.first().map(String::as_str) == Some("--resume") {
|
||||
return parse_resume_args(&rest[1..], output_format);
|
||||
}
|
||||
if let Some(action) = parse_local_help_action(&rest, output_format) {
|
||||
return action;
|
||||
if rest.first().map(String::as_str) == Some("resume") {
|
||||
return parse_resume_args(&rest[1..], output_format);
|
||||
}
|
||||
// #696: `claw compact` is the bare name of the interactive `/compact`
|
||||
// slash command, not a prompt. When extra args such as `--help` appear
|
||||
@@ -1580,6 +1590,9 @@ fn parse_local_help_action(
|
||||
"system-prompt" => LocalHelpTopic::SystemPrompt,
|
||||
"dump-manifests" => LocalHelpTopic::DumpManifests,
|
||||
"bootstrap-plan" => LocalHelpTopic::BootstrapPlan,
|
||||
"resume" | "--resume" => LocalHelpTopic::Resume,
|
||||
"session" => LocalHelpTopic::Session,
|
||||
"compact" => LocalHelpTopic::Compact,
|
||||
"model" | "models" => LocalHelpTopic::Model,
|
||||
"settings" => LocalHelpTopic::Settings,
|
||||
_ => return None,
|
||||
@@ -1642,6 +1655,9 @@ fn parse_single_word_command_alias(
|
||||
"system-prompt" => Some(LocalHelpTopic::SystemPrompt),
|
||||
"dump-manifests" => Some(LocalHelpTopic::DumpManifests),
|
||||
"bootstrap-plan" => Some(LocalHelpTopic::BootstrapPlan),
|
||||
"resume" => Some(LocalHelpTopic::Resume),
|
||||
"session" => Some(LocalHelpTopic::Session),
|
||||
"compact" => Some(LocalHelpTopic::Compact),
|
||||
"agents" | "agent" => Some(LocalHelpTopic::Agents),
|
||||
"skills" | "skill" => Some(LocalHelpTopic::Skills),
|
||||
"plugins" | "plugin" | "marketplace" => Some(LocalHelpTopic::Plugins),
|
||||
@@ -1693,6 +1709,9 @@ fn parse_single_word_command_alias(
|
||||
"system-prompt" => Some(LocalHelpTopic::SystemPrompt),
|
||||
"dump-manifests" => Some(LocalHelpTopic::DumpManifests),
|
||||
"bootstrap-plan" => Some(LocalHelpTopic::BootstrapPlan),
|
||||
"resume" => Some(LocalHelpTopic::Resume),
|
||||
"session" => Some(LocalHelpTopic::Session),
|
||||
"compact" => Some(LocalHelpTopic::Compact),
|
||||
"agents" | "agent" => Some(LocalHelpTopic::Agents),
|
||||
"skills" | "skill" => Some(LocalHelpTopic::Skills),
|
||||
"plugins" | "plugin" | "marketplace" => Some(LocalHelpTopic::Plugins),
|
||||
@@ -3652,6 +3671,7 @@ fn resume_session(session_path: &Path, commands: &[String], output_format: CliOu
|
||||
// #787: fall back to kind-derived hint when message has no \n delimiter
|
||||
let hint =
|
||||
inline_hint.or_else(|| fallback_hint_for_error_kind(kind).map(String::from));
|
||||
let sessions_dir = sessions_dir().ok().map(|path| path.display().to_string());
|
||||
// #819: JSON mode resume errors go to stdout for parity with other
|
||||
// non-interactive command guards.
|
||||
println!(
|
||||
@@ -3664,6 +3684,7 @@ fn resume_session(session_path: &Path, commands: &[String], output_format: CliOu
|
||||
"error": short_reason,
|
||||
"exit_code": 1,
|
||||
"hint": hint,
|
||||
"sessions_dir": sessions_dir,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
@@ -8161,6 +8182,25 @@ fn render_help_topic(topic: LocalHelpTopic) -> String {
|
||||
Exit codes 0 if state file exists and parses; 1 with actionable hint otherwise
|
||||
Related claw status · ROADMAP #139 (this worker-concept contract)"
|
||||
.to_string(),
|
||||
LocalHelpTopic::Resume => format!(
|
||||
"Resume\n Usage claw resume [session-path|session-id|{LATEST_SESSION_REFERENCE}] [/slash-command ...] [--output-format <format>]\n Alias claw --resume [session-path|session-id|{LATEST_SESSION_REFERENCE}]\n Purpose restore or inspect a saved session without starting a new provider turn\n Output session restore or resume-safe command output; missing sessions return session_not_found\n Formats text (default), json\n Related /resume · /session list · claw --resume {LATEST_SESSION_REFERENCE} /status"
|
||||
),
|
||||
LocalHelpTopic::Session => "Session
|
||||
Usage claw session --help [--output-format <format>]
|
||||
Purpose show /session command guidance without loading config, credentials, or a session
|
||||
Actions list · exists <id> · switch <id> · fork <name> · delete <id>
|
||||
Direct use run /session in the REPL or claw --resume SESSION.jsonl /session <action>
|
||||
Formats text (default), json
|
||||
Related claw resume · claw export · .claw/sessions/"
|
||||
.to_string(),
|
||||
LocalHelpTopic::Compact => "Compact
|
||||
Usage claw compact --help [--output-format <format>]
|
||||
Purpose show compaction guidance without loading config, credentials, or a session
|
||||
Direct use run /compact in the REPL or claw --resume SESSION.jsonl /compact
|
||||
Output compaction removes older tool-detail messages when the selected session is large enough
|
||||
Formats text (default), json
|
||||
Related claw resume · /compact · /status"
|
||||
.to_string(),
|
||||
LocalHelpTopic::Export => "Export
|
||||
Usage claw export [--session <id|latest>] [--output <path>] [--output-format <format>]
|
||||
Purpose serialize a managed session to JSON for review, transfer, or archival
|
||||
@@ -8256,6 +8296,9 @@ fn local_help_topic_command(topic: LocalHelpTopic) -> &'static str {
|
||||
LocalHelpTopic::Acp => "acp",
|
||||
LocalHelpTopic::Init => "init",
|
||||
LocalHelpTopic::State => "state",
|
||||
LocalHelpTopic::Resume => "resume",
|
||||
LocalHelpTopic::Session => "session",
|
||||
LocalHelpTopic::Compact => "compact",
|
||||
LocalHelpTopic::Export => "export",
|
||||
LocalHelpTopic::Version => "version",
|
||||
LocalHelpTopic::SystemPrompt => "system-prompt",
|
||||
|
||||
@@ -124,6 +124,133 @@ fn doctor_help_text_stays_plaintext_and_local_702() {
|
||||
serde_json::from_str::<Value>(&stdout).expect_err("text help should remain plaintext");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_session_compact_help_short_circuits_before_config_or_auth_427() {
|
||||
let root = unique_temp_dir("session-help-local-427");
|
||||
let config_home = root.join("config-home");
|
||||
let home = root.join("home");
|
||||
fs::create_dir_all(root.join(".claw")).expect("project config dir should exist");
|
||||
fs::create_dir_all(&config_home).expect("config home should exist");
|
||||
fs::create_dir_all(&home).expect("home should exist");
|
||||
fs::write(root.join(".claw").join("settings.json"), "{").expect("broken config should write");
|
||||
|
||||
let envs = [
|
||||
(
|
||||
"CLAW_CONFIG_HOME",
|
||||
config_home.to_str().expect("utf8 config home"),
|
||||
),
|
||||
("HOME", home.to_str().expect("utf8 home")),
|
||||
("ANTHROPIC_API_KEY", ""),
|
||||
("ANTHROPIC_AUTH_TOKEN", ""),
|
||||
("OPENAI_API_KEY", ""),
|
||||
];
|
||||
|
||||
let text_cases: &[(&[&str], &str)] = &[
|
||||
(&["resume", "--help"], "Resume\n"),
|
||||
(&["--resume", "--help"], "Resume\n"),
|
||||
(&["session", "--help"], "Session\n"),
|
||||
(&["compact", "--help"], "Compact\n"),
|
||||
];
|
||||
for (args, heading) in text_cases {
|
||||
let output = run_claw(&root, args, &envs);
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{args:?} should exit 0 before auth/config; stdout:\n{}\n\nstderr:\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(stdout.starts_with(heading), "{args:?} stdout: {stdout}");
|
||||
assert!(stdout.contains("Usage"), "{args:?} stdout: {stdout}");
|
||||
assert!(
|
||||
!stdout.contains("missing_credentials") && !stderr.contains("missing_credentials"),
|
||||
"{args:?} must not hit provider auth: stdout={stdout:?} stderr={stderr:?}"
|
||||
);
|
||||
assert!(
|
||||
!stdout.contains("config_parse_error") && stderr.is_empty(),
|
||||
"{args:?} must not load broken config: stdout={stdout:?} stderr={stderr:?}"
|
||||
);
|
||||
serde_json::from_str::<Value>(&stdout).expect_err("text help should remain plaintext");
|
||||
}
|
||||
|
||||
let json_cases: &[(&[&str], &str)] = &[
|
||||
(&["resume", "--help", "--output-format", "json"], "resume"),
|
||||
(&["--resume", "--help", "--output-format", "json"], "resume"),
|
||||
(&["session", "--help", "--output-format", "json"], "session"),
|
||||
(&["compact", "--help", "--output-format", "json"], "compact"),
|
||||
];
|
||||
for (args, topic) in json_cases {
|
||||
let parsed = assert_json_command_with_env(&root, args, &envs);
|
||||
assert_eq!(parsed["kind"], "help", "{args:?}: {parsed}");
|
||||
assert_eq!(parsed["status"], "ok", "{args:?}: {parsed}");
|
||||
assert_eq!(parsed["topic"], *topic, "{args:?}: {parsed}");
|
||||
assert!(
|
||||
parsed["message"]
|
||||
.as_str()
|
||||
.is_some_and(|message| message.contains("Usage")),
|
||||
"{args:?} should include static usage text: {parsed}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_missing_session_json_reports_local_store_before_auth_427() {
|
||||
let root = unique_temp_dir("resume-missing-local-427");
|
||||
let config_home = root.join("config-home");
|
||||
let home = root.join("home");
|
||||
fs::create_dir_all(&root).expect("temp dir should exist");
|
||||
fs::create_dir_all(&config_home).expect("config home should exist");
|
||||
fs::create_dir_all(&home).expect("home should exist");
|
||||
|
||||
let envs = [
|
||||
(
|
||||
"CLAW_CONFIG_HOME",
|
||||
config_home.to_str().expect("utf8 config home"),
|
||||
),
|
||||
("HOME", home.to_str().expect("utf8 home")),
|
||||
("ANTHROPIC_API_KEY", ""),
|
||||
("ANTHROPIC_AUTH_TOKEN", ""),
|
||||
("OPENAI_API_KEY", ""),
|
||||
];
|
||||
|
||||
let output = run_claw(
|
||||
&root,
|
||||
&[
|
||||
"resume",
|
||||
"definitely-missing-session",
|
||||
"--output-format",
|
||||
"json",
|
||||
],
|
||||
&envs,
|
||||
);
|
||||
assert_eq!(
|
||||
output.status.code(),
|
||||
Some(1),
|
||||
"missing session should exit 1"
|
||||
);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(
|
||||
stderr.is_empty(),
|
||||
"JSON missing-session stderr should be empty: {stderr:?}"
|
||||
);
|
||||
assert!(
|
||||
!stdout.contains("missing_credentials") && !stderr.contains("missing_credentials"),
|
||||
"missing session must not reach provider auth: stdout={stdout:?} stderr={stderr:?}"
|
||||
);
|
||||
let parsed: Value = serde_json::from_str(stdout.trim())
|
||||
.unwrap_or_else(|_| panic!("resume missing session must emit JSON, got: {stdout:?}"));
|
||||
assert_eq!(parsed["error_kind"], "session_not_found", "{parsed}");
|
||||
assert_eq!(parsed["action"], "restore", "{parsed}");
|
||||
assert!(
|
||||
parsed["sessions_dir"]
|
||||
.as_str()
|
||||
.is_some_and(|path| path.contains(".claw") && path.contains("sessions")),
|
||||
"missing-session JSON should expose the searched sessions_dir: {parsed}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_emits_json_when_requested() {
|
||||
let root = unique_temp_dir("version-json");
|
||||
|
||||
Reference in New Issue
Block a user