mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-27 03:44:58 +08:00
feat: #145 — wire claw plugins subcommand to CLI parser (prompt misdelivery fix)
## Problem `claw plugins` (and `claw plugins list`, `claw plugins --help`, `claw plugins info <name>`, etc.) fell through the top-level subcommand match and got routed into the prompt-execution path. Result: a purely local introspection command triggered an Anthropic API call and surfaced `missing Anthropic credentials` to the user. With valid credentials, it would actually send the literal string "plugins" as a user prompt to Claude, burning tokens for a local query. $ claw plugins error: missing Anthropic credentials; export ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY before calling the Anthropic API $ ANTHROPIC_API_KEY=dummy claw plugins ⠋ 🦀 Thinking... ✘ ❌ Request failed error: api returned 401 Unauthorized Meanwhile siblings (`agents`, `mcp`, `skills`) all worked correctly: $ claw agents No agents found. $ claw mcp MCP Working directory ... Configured servers 0 ## Root cause `CliAction::Plugins` exists, has a working dispatcher (`LiveCli::print_plugins`), and is produced inside the REPL via `SlashCommand::Plugins`. But the top-level CLI parser in `parse_subcommand()` had arms for `agents`, `mcp`, `skills`, `status`, `doctor`, `init`, `export`, `prompt`, etc., and **no arm for `plugins`**. The dispatch never ran from the CLI entry point. ## Changes ### rust/crates/rusty-claude-cli/src/main.rs Added a `"plugins"` arm to the top-level match in `parse_subcommand()` that produces `CliAction::Plugins { action, target, output_format }`, following the same positional convention as `mcp` (`action` = first positional, `target` = second). Rejects >2 positional args with a clear error. Added four regression assertions in the existing `parse_args` test: - `plugins` alone → `CliAction::Plugins { action: None, target: None }` - `plugins list` → action: Some("list"), target: None - `plugins enable <name>` → action: Some("enable"), target: Some(...) - `plugins --output-format json` → action: None, output_format: Json ### ROADMAP.md Added Pinpoint #145 documenting the gap, verification, root cause, fix shape, and acceptance. ## Live verification $ claw plugins # no credentials set Plugins example-bundled v0.1.0 disabled sample-hooks v0.1.0 disabled $ claw plugins --output-format json # no credentials set { "action": "list", "kind": "plugin", "message": "Plugins\n example-bundled ...\n sample-hooks ...", "reload_runtime": false, "target": null } Exit 0 in all modes. No network call. No "missing credentials" error. ## Tests - rusty-claude-cli bin: 177 tests pass (new plugin assertions included) - Full workspace green except pre-existing resume_latest flake (unrelated) Closes ROADMAP #145.
This commit is contained in:
@@ -661,6 +661,30 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||
args: join_optional_args(&rest[1..]),
|
||||
output_format,
|
||||
}),
|
||||
// #145: `plugins` was routed through the prompt fallback because no
|
||||
// top-level parser arm produced CliAction::Plugins. That made `claw
|
||||
// plugins` (and `claw plugins --help`, `claw plugins list`, ...)
|
||||
// attempt an Anthropic network call, surfacing the misleading error
|
||||
// `missing Anthropic credentials` even though the command is purely
|
||||
// local introspection. Mirror `agents`/`mcp`/`skills`: action is the
|
||||
// first positional arg, target is the second.
|
||||
"plugins" => {
|
||||
let tail = &rest[1..];
|
||||
let action = tail.first().cloned();
|
||||
let target = tail.get(1).cloned();
|
||||
if tail.len() > 2 {
|
||||
return Err(format!(
|
||||
"unexpected extra arguments after `claw plugins {}`: {}",
|
||||
tail[..2].join(" "),
|
||||
tail[2..].join(" ")
|
||||
));
|
||||
}
|
||||
Ok(CliAction::Plugins {
|
||||
action,
|
||||
target,
|
||||
output_format,
|
||||
})
|
||||
}
|
||||
"skills" => {
|
||||
let args = join_optional_args(&rest[1..]);
|
||||
match classify_skills_slash_command(args.as_deref()) {
|
||||
@@ -9549,6 +9573,52 @@ mod tests {
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
// #145: `plugins` must parse as CliAction::Plugins (not fall through
|
||||
// to the prompt path, which would hit the Anthropic API for a purely
|
||||
// local introspection command).
|
||||
assert_eq!(
|
||||
parse_args(&["plugins".to_string()]).expect("plugins should parse"),
|
||||
CliAction::Plugins {
|
||||
action: None,
|
||||
target: None,
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&["plugins".to_string(), "list".to_string()])
|
||||
.expect("plugins list should parse"),
|
||||
CliAction::Plugins {
|
||||
action: Some("list".to_string()),
|
||||
target: None,
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&[
|
||||
"plugins".to_string(),
|
||||
"enable".to_string(),
|
||||
"example-bundled".to_string(),
|
||||
])
|
||||
.expect("plugins enable <target> should parse"),
|
||||
CliAction::Plugins {
|
||||
action: Some("enable".to_string()),
|
||||
target: Some("example-bundled".to_string()),
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&[
|
||||
"plugins".to_string(),
|
||||
"--output-format".to_string(),
|
||||
"json".to_string(),
|
||||
])
|
||||
.expect("plugins --output-format json should parse"),
|
||||
CliAction::Plugins {
|
||||
action: None,
|
||||
target: None,
|
||||
output_format: CliOutputFormat::Json,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user