mirror of
https://github.com/ultraworkers/claw-code.git
synced 2026-04-24 05:00:25 +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:
parent
faeaa1d30c
commit
7d63699f9f
58
ROADMAP.md
58
ROADMAP.md
@ -5520,3 +5520,61 @@ Exit 0. Full envelope with error surfaced.
|
||||
**Future phase (joins #143 Phase 2).** When typed-error taxonomy lands (§4.44), promote `config_load_error` from string to typed object across `doctor`, `status`, and `mcp` in one pass.
|
||||
|
||||
**Source.** Jobdori dogfood 2026-04-21 18:59 KST on main HEAD `e2a43fc`. Joins **partial-success** cluster (#143, Principle #5) and **surface consistency** cluster. Session tally: ROADMAP #144.
|
||||
|
||||
## Pinpoint #145. `claw plugins` subcommand not wired to CLI parser — word gets treated as a prompt, hits Anthropic API
|
||||
|
||||
**Gap.** `claw plugins` (and `claw plugins list`, `claw plugins --help`, `claw plugins info <name>`, etc.) fall through the top-level subcommand match and get routed into the prompt-execution path. Result: a purely local introspection command triggers an Anthropic API call and surfaces `missing Anthropic credentials` to the user. With valid credentials, it would actually send the string `"plugins"` as a prompt to Claude, burning tokens for a local query.
|
||||
|
||||
**Verified on main HEAD `faeaa1d` (2026-04-21 19:32 KST):**
|
||||
|
||||
```
|
||||
$ claw plugins
|
||||
error: missing Anthropic credentials; export ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY before calling the Anthropic API
|
||||
|
||||
$ claw plugins --output-format json
|
||||
{"error":"missing Anthropic credentials; ...","type":"error"}
|
||||
|
||||
$ claw plugins --help
|
||||
error: missing Anthropic credentials; ...
|
||||
|
||||
$ claw plugins list
|
||||
error: missing Anthropic credentials; ...
|
||||
|
||||
$ ANTHROPIC_API_KEY=dummy claw plugins
|
||||
⠋ 🦀 Thinking...
|
||||
✘ ❌ Request failed
|
||||
error: api returned 401 Unauthorized (authentication_error)
|
||||
```
|
||||
|
||||
Compare `agents`, `mcp`, `skills` — all recognized, all local, all exit 0:
|
||||
|
||||
```
|
||||
$ claw agents
|
||||
No agents found.
|
||||
$ claw mcp
|
||||
MCP
|
||||
Working directory ...
|
||||
Configured servers 0
|
||||
```
|
||||
|
||||
**Root cause.** In `rusty-claude-cli/src/main.rs`, the top-level `match rest[0].as_str()` parser has arms for `agents`, `mcp`, `skills`, `status`, `doctor`, `init`, `export`, `prompt`, etc., but **no arm for `plugins`**. The `CliAction::Plugins` variant exists, has a dispatcher (`print_plugins`), and is produced by `SlashCommand::Plugins` inside the REPL — but the top-level CLI path was never wired. Result: `plugins` matches neither a known subcommand nor a slash path, so it falls through to the default "run as prompt" behavior.
|
||||
|
||||
**Why this is a clawability gap.**
|
||||
1. **Prompt misdelivery (explicit Clawhip category)**: the command string is sent to the LLM instead of dispatched locally. Real risk: without the credentials guard, `claw plugins` would send `"plugins"` as a user prompt to Claude, burning tokens.
|
||||
2. **Surface asymmetry**: `plugins` is the only diagnostic-adjacent command that isn't wired. Documentation, slash command, and dispatcher all exist; parser wiring was missed.
|
||||
3. **`--help` should never hit the network**. Anywhere.
|
||||
4. **Misleading error**: user running `claw plugins` sees an Anthropic credential error. No hint that `plugins` wasn't a recognized subcommand.
|
||||
|
||||
**Fix shape (~20 lines).** Add a `"plugins"` arm to the top-level parser in `main.rs` that produces `CliAction::Plugins { action, target, output_format }`, following the same positional convention as `mcp` (`action` = first positional, `target` = second). The existing `CliAction::Plugins` handler (`LiveCli::print_plugins`) already covers text and JSON.
|
||||
|
||||
**Acceptance.**
|
||||
- `claw plugins` exits 0 with plugins list (empty in a clean workspace, which is the honest state).
|
||||
- `claw plugins --output-format json` emits `{"kind":"plugin","action":"list",...}` with exit 0.
|
||||
- `claw plugins list` exits 0 and matches `claw plugins`.
|
||||
- `claw plugins info <name>` resolves through the existing handler.
|
||||
- No Anthropic network call occurs for any `plugins` invocation.
|
||||
- Regression test: parse `["claw", "plugins"]`, assert `CliAction::Plugins { action: None, target: None, .. }`.
|
||||
|
||||
**Blocker.** None. `CliAction::Plugins` already exists with a working dispatcher.
|
||||
|
||||
**Source.** Jobdori dogfood 2026-04-21 19:30 KST on main HEAD `faeaa1d` in response to Clawhip nudge. Joins **prompt misdelivery** cluster. Session tally: ROADMAP #145.
|
||||
|
||||
@ -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]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user