use std::collections::BTreeSet;
use std::io::{self, Write};
use ninmu_commands::{render_slash_command_help_filtered, slash_command_specs};
use crate::format::cost::{LATEST_SESSION_REFERENCE, PRIMARY_SESSION_EXTENSION};
const STUB_COMMANDS: &[&str] = &[
"login",
"logout",
"vim",
"upgrade",
"share",
"feedback",
"files",
"fast",
"exit",
"summary",
"desktop",
"brief",
"advisor",
"stickers",
"insights",
"thinkback",
"release-notes",
"security-review",
"keybindings",
"privacy-settings",
"plan",
"review",
"tasks",
"theme",
"voice",
"usage",
"rename",
"copy",
"hooks",
"context",
"color",
"effort",
"branch",
"rewind",
"ide",
"tag",
"output-style",
"add-dir",
"allowed-tools",
"bookmarks",
"workspace",
"reasoning",
"budget",
"rate-limit",
"changelog",
"diagnostics",
"metrics",
"tool-details",
"focus",
"unfocus",
"pin",
"unpin",
"language",
"profile",
"max-tokens",
"temperature",
"system-prompt",
"notifications",
"telemetry",
"env",
"project",
"terminal-setup",
"api-key",
"reset",
"undo",
"stop",
"retry",
"paste",
"screenshot",
"image",
"search",
"listen",
"speak",
"format",
"test",
"lint",
"build",
"run",
"git",
"stash",
"blame",
"log",
"cron",
"team",
"benchmark",
"migrate",
"templates",
"explain",
"refactor",
"docs",
"fix",
"perf",
"chat",
"web",
"map",
"symbols",
"references",
"definition",
"hover",
"autofix",
"multi",
"macro",
"alias",
"parallel",
"subagent",
"agent",
];
const OFFICIAL_REPO_URL: &str = "https://github.com/deep-thinking-llc/ninmu-code";
const OFFICIAL_REPO_SLUG: &str = "deep-thinking-llc/ninmu-code";
const DEPRECATED_INSTALL_COMMAND: &str = "cargo install ninmu-code";
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum LocalHelpTopic {
Status,
Sandbox,
Doctor,
Acp,
Init,
State,
Export,
Version,
SystemPrompt,
DumpManifests,
BootstrapPlan,
}
pub(crate) fn render_repl_help() -> String {
[
"REPL".to_string(),
" /exit Quit the REPL".to_string(),
" /quit Quit the REPL".to_string(),
" Up/Down Navigate prompt history".to_string(),
" Ctrl-R Reverse-search prompt history".to_string(),
" Tab Complete commands, modes, and recent sessions".to_string(),
" Ctrl-C Clear input (or exit on empty prompt)".to_string(),
" Shift+Enter/Ctrl+J Insert a newline".to_string(),
" Auto-save .claw/sessions/<session-id>.jsonl".to_string(),
" Resume latest /resume latest".to_string(),
" Browse sessions /session list".to_string(),
" Show prompt history /history [count]".to_string(),
String::new(),
render_slash_command_help_filtered(STUB_COMMANDS),
]
.join(
"
",
)
}
pub(crate) fn render_help_topic(topic: LocalHelpTopic) -> String {
match topic {
LocalHelpTopic::Status => "Status
Usage ninmu status [--output-format <format>]
Purpose show the local workspace snapshot without entering the REPL
Output model, permissions, git state, config files, and sandbox status
Formats text (default), json
Related /status · ninmu --resume latest /status"
.to_string(),
LocalHelpTopic::Sandbox => "Sandbox
Usage ninmu sandbox [--output-format <format>]
Purpose inspect the resolved sandbox and isolation state for the current directory
Output namespace, network, filesystem, and fallback details
Formats text (default), json
Related /sandbox · ninmu status"
.to_string(),
LocalHelpTopic::Doctor => "Doctor
Usage ninmu doctor [--output-format <format>]
Purpose diagnose local auth, config, workspace, sandbox, and build metadata
Output local-only health report; no provider request or session resume required
Formats text (default), json
Related /doctor · ninmu --resume latest /doctor"
.to_string(),
LocalHelpTopic::Acp => "ACP / Zed
Usage ninmu acp [serve] [--output-format <format>]
Aliases ninmu --acp · ninmu -acp
Purpose explain the current editor-facing ACP/Zed launch contract without starting the runtime
Status discoverability only; `serve` is a status alias and does not launch a daemon yet
Formats text (default), json
Related ROADMAP #64a (discoverability) · ROADMAP #76 (real ACP support) · ninmu --help"
.to_string(),
LocalHelpTopic::Init => "Init
Usage ninmu init [--output-format <format>]
Purpose create .claw/, .claw.json, .gitignore, and CLAUDE.md in the current project
Output list of created vs. skipped files (idempotent: safe to re-run)
Formats text (default), json
Related ninmu status · ninmu doctor"
.to_string(),
LocalHelpTopic::State => "State
Usage ninmu state [--output-format <format>]
Purpose read .claw/worker-state.json written by the interactive REPL or a one-shot prompt
Output worker id, model, permissions, session reference (text or json)
Formats text (default), json
Produces state `ninmu` (interactive REPL) or `ninmu prompt <text>` (one non-interactive turn)
Observes state `ninmu state` reads; ninmuhip/CI may poll this file without HTTP
Exit codes 0 if state file exists and parses; 1 with actionable hint otherwise
Related ninmu status · ROADMAP #139 (this worker-concept contract)"
.to_string(),
LocalHelpTopic::Export => "Export
Usage ninmu export [--session <id|latest>] [--output <path>] [--output-format <format>]
Purpose serialize a managed session to JSON for review, transfer, or archival
Defaults --session latest (most recent managed session in .claw/sessions/)
Formats text (default), json
Related /session list · ninmu --resume latest"
.to_string(),
LocalHelpTopic::Version => "Version
Usage ninmu version [--output-format <format>]
Aliases ninmu --version · ninmu -V
Purpose print the ninmu CLI version and build metadata
Formats text (default), json
Related ninmu doctor (full build/auth/config diagnostic)"
.to_string(),
LocalHelpTopic::SystemPrompt => "System Prompt
Usage ninmu system-prompt [--cwd <path>] [--date YYYY-MM-DD] [--output-format <format>]
Purpose render the resolved system prompt that `ninmu` would send for the given cwd + date
Options --cwd overrides the workspace dir · --date injects a deterministic date stamp
Formats text (default), json
Related ninmu doctor · ninmu dump-manifests"
.to_string(),
LocalHelpTopic::DumpManifests => "Dump Manifests
Usage ninmu dump-manifests [--manifests-dir <path>] [--output-format <format>]
Purpose emit every skill/agent/tool manifest the resolver would load for the current cwd
Options --manifests-dir scopes discovery to a specific directory
Formats text (default), json
Related ninmu skills · ninmu agents · ninmu doctor"
.to_string(),
LocalHelpTopic::BootstrapPlan => "Bootstrap Plan
Usage ninmu bootstrap-plan [--output-format <format>]
Purpose list the ordered startup phases the CLI would execute before dispatch
Output phase names (text) or structured phase list (json) — primary output is the plan itself
Formats text (default), json
Related ninmu doctor · ninmu status"
.to_string(),
}
}
pub(crate) fn print_help_topic(topic: LocalHelpTopic) {
println!("{}", render_help_topic(topic));
}
pub(crate) fn print_help_to(out: &mut impl Write) -> io::Result<()> {
writeln!(out, "ninmu v{VERSION}")?;
writeln!(out)?;
writeln!(out, "Usage:")?;
writeln!(
out,
" ninmu [--model MODEL] [--allowedTools TOOL[,TOOL...]]"
)?;
writeln!(out, " Start the interactive REPL")?;
writeln!(out, " ninmu --tui [--model MODEL]")?;
writeln!(
out,
" Full-screen TUI mode (alternate screen, experimental)"
)?;
writeln!(
out,
" ninmu [--model MODEL] [--output-format text|json] prompt TEXT"
)?;
writeln!(out, " Send one prompt and exit")?;
writeln!(
out,
" ninmu [--model MODEL] [--output-format text|json] TEXT"
)?;
writeln!(out, " Shorthand non-interactive prompt mode")?;
writeln!(
out,
" ninmu --resume [SESSION.jsonl|session-id|latest] [/status] [/compact] [...]"
)?;
writeln!(
out,
" Inspect or maintain a saved session without entering the REPL"
)?;
writeln!(out, " ninmu help")?;
writeln!(out, " Alias for --help")?;
writeln!(out, " ninmu version")?;
writeln!(out, " Alias for --version")?;
writeln!(out, " ninmu status")?;
writeln!(
out,
" Show the current local workspace status snapshot"
)?;
writeln!(out, " ninmu sandbox")?;
writeln!(out, " Show the current sandbox isolation snapshot")?;
writeln!(out, " ninmu doctor")?;
writeln!(
out,
" Diagnose local auth, config, workspace, and sandbox health"
)?;
writeln!(out, " ninmu acp [serve]")?;
writeln!(
out,
" Show ACP/Zed editor integration status (currently unsupported; aliases: --acp, -acp)"
)?;
writeln!(out, " Source of truth: {OFFICIAL_REPO_SLUG}")?;
writeln!(
out,
" Warning: do not `{DEPRECATED_INSTALL_COMMAND}` (deprecated stub)"
)?;
writeln!(out, " ninmu dump-manifests [--manifests-dir PATH]")?;
writeln!(out, " ninmu bootstrap-plan")?;
writeln!(out, " ninmu agents")?;
writeln!(out, " ninmu mcp")?;
writeln!(out, " ninmu skills")?;
writeln!(
out,
" ninmu system-prompt [--cwd PATH] [--date YYYY-MM-DD]"
)?;
writeln!(out, " ninmu init")?;
writeln!(
out,
" ninmu 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!(
out,
" --model MODEL Override the active model"
)?;
writeln!(
out,
" --output-format FORMAT Non-interactive output format: text or json"
)?;
writeln!(
out,
" --compact Strip tool call details; print only the final assistant text (text mode only; useful for piping)"
)?;
writeln!(
out,
" --permission-mode MODE Set read-only, workspace-write, or danger-full-access"
)?;
writeln!(
out,
" --dangerously-skip-permissions Skip all permission checks"
)?;
writeln!(out, " --allowedTools TOOLS Restrict enabled tools (repeatable; comma-separated aliases supported)")?;
writeln!(
out,
" --version, -V Print version and build information locally"
)?;
writeln!(out)?;
writeln!(out, "Interactive slash commands:")?;
writeln!(out, "{}", render_slash_command_help_filtered(STUB_COMMANDS))?;
writeln!(out)?;
let resume_commands = ninmu_commands::resume_supported_slash_commands()
.into_iter()
.map(|spec| match spec.argument_hint {
Some(argument_hint) => format!("/{} {}", spec.name, argument_hint),
None => format!("/{}", spec.name),
})
.collect::<Vec<_>>()
.join(", ");
writeln!(out, "Resume-safe commands: {resume_commands}")?;
writeln!(out)?;
writeln!(out, "Session shortcuts:")?;
writeln!(
out,
" REPL turns auto-save to .claw/sessions/<session-id>.{PRIMARY_SESSION_EXTENSION}"
)?;
writeln!(
out,
" Use `{LATEST_SESSION_REFERENCE}` with --resume, /resume, or /session switch to target the newest saved session"
)?;
writeln!(
out,
" Use /session list in the REPL to browse managed sessions"
)?;
writeln!(out, "Examples:")?;
writeln!(out, " ninmu --model claude-opus \"summarize this repo\"")?;
writeln!(
out,
" ninmu --output-format json prompt \"explain src/main.rs\""
)?;
writeln!(out, " ninmu --compact \"summarize Cargo.toml\" | wc -l")?;
writeln!(
out,
" ninmu --allowedTools read,glob \"summarize Cargo.toml\""
)?;
writeln!(out, " ninmu --resume {LATEST_SESSION_REFERENCE}")?;
writeln!(
out,
" ninmu --resume {LATEST_SESSION_REFERENCE} /status /diff /export notes.txt"
)?;
writeln!(out, " ninmu agents")?;
writeln!(out, " ninmu mcp show my-server")?;
writeln!(out, " ninmu /skills")?;
writeln!(out, " ninmu doctor")?;
writeln!(out, " source of truth: {OFFICIAL_REPO_URL}")?;
writeln!(
out,
" do not run `{DEPRECATED_INSTALL_COMMAND}` — it installs a deprecated stub"
)?;
writeln!(out, " ninmu init")?;
writeln!(out, " ninmu export")?;
writeln!(out, " ninmu export conversation.md")?;
Ok(())
}
pub(crate) fn slash_command_completion_candidates_with_sessions(
model: &str,
active_session_id: Option<&str>,
recent_session_ids: Vec<String>,
) -> Vec<String> {
let mut completions = BTreeSet::new();
for spec in slash_command_specs() {
if STUB_COMMANDS.contains(&spec.name) {
continue;
}
completions.insert(format!("/{}", spec.name));
for alias in spec.aliases {
if !STUB_COMMANDS.contains(alias) {
completions.insert(format!("/{alias}"));
}
}
}
for candidate in [
"/bughunter ",
"/clear --confirm",
"/config ",
"/config env",
"/config hooks",
"/config model",
"/config plugins",
"/mcp ",
"/mcp list",
"/mcp show ",
"/export ",
"/issue ",
"/model ",
"/model opus",
"/model sonnet",
"/model haiku",
"/permissions ",
"/permissions read-only",
"/permissions workspace-write",
"/permissions danger-full-access",
"/plugin list",
"/plugin install ",
"/plugin enable ",
"/plugin disable ",
"/plugin uninstall ",
"/plugin update ",
"/plugins list",
"/pr ",
"/resume ",
"/session list",
"/session switch ",
"/session fork ",
"/teleport ",
"/ultraplan ",
"/agents help",
"/mcp help",
"/skills help",
] {
completions.insert(candidate.to_string());
}
if !model.trim().is_empty() {
completions.insert(format!("/model {}", resolve_model_alias(model)));
completions.insert(format!("/model {model}"));
}
if let Some(active_session_id) = active_session_id.filter(|value| !value.trim().is_empty()) {
completions.insert(format!("/resume {active_session_id}"));
completions.insert(format!("/session switch {active_session_id}"));
}
for session_id in recent_session_ids
.into_iter()
.filter(|value| !value.trim().is_empty())
.take(10)
{
completions.insert(format!("/resume {session_id}"));
completions.insert(format!("/session switch {session_id}"));
}
completions.into_iter().collect()
}
fn resolve_model_alias(model: &str) -> &str {
match model {
"opus" => "claude-opus-4-6",
"sonnet" => "claude-sonnet-4-6",
"haiku" => "claude-haiku-4-5-20251213",
_ => model,
}
}