amicus 4.5.2 → 4.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amicus",
3
- "version": "4.5.2",
3
+ "version": "4.5.3",
4
4
  "description": "Multi-model LLM Council + parallel AI window for Claude Code. Run structured council reviews across Gemini, GPT, DeepSeek and more — or fork a conversation to any model and fold the results back.",
5
5
  "author": {
6
6
  "name": "Christian Wagner"
package/CHANGELOG.md CHANGED
@@ -3,6 +3,34 @@
3
3
  All notable changes to Amicus are documented here. Format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow semver.
5
5
 
6
+ ## [4.5.3] - 2026-08-01
7
+
8
+ ### Fixed
9
+
10
+ - **Unknown CLI flags are rejected instead of silently absorbed.** `parseArgs` treated any
11
+ `--token` as a flag: an unrecognized one landed on the parsed args object, no handler read it,
12
+ and the command ran as though it were never typed. Found while smoke-testing v4.5.2 —
13
+ `amicus start -m deepseek --prompt "…" --headless` printed no error and exited 0, but `start`
14
+ has no `--headless`; the run silently took the **interactive** path, ignored `-m`, and left a
15
+ session running against the default model. A typo (`--modl`), a flag borrowed from another
16
+ command, or an invented one all behaved the same way, and an unknown flag followed by a
17
+ positional would swallow it as its value. Unknown flags now get the same treatment amicus
18
+ already gave an unknown *command*: name it, suggest the nearest real flag, point at `--help`,
19
+ exit 1. The known-flag set is **derived from the usage text** (the same source `getCommandNames()`
20
+ uses, for the same anti-rot reason) plus the boolean-flag list, plus a small explicitly-documented
21
+ allowlist of internal MCP→CLI passthroughs (`--task-id`, `--run-id`, `--council-name`,
22
+ `--cowork-process`) and undocumented-but-working flags (`--briefing`, `--mode`, `--quiet`).
23
+ A regression test asserts every `args.<flag>` any CLI handler reads is in the known set, so the
24
+ check can never silently start rejecting a flag that works.
25
+
26
+ ### Changed
27
+
28
+ - **README Quick start is now four numbered steps**, with **Configure** promoted from a bold line
29
+ buried between two callouts to its own step and TOC entry. Installing without configuring is the
30
+ step people skip, and every council fails at the first model call when they do — so it now says
31
+ so plainly, notes that one OpenRouter key is enough to start, and ends with `amicus doctor` as
32
+ the confirmation that setup actually took.
33
+
6
34
  ## [4.5.2] - 2026-07-31
7
35
 
8
36
  ### Fixed
package/README.md CHANGED
@@ -29,6 +29,10 @@ Hand Claude a plan, a design, a diff, an architecture decision, a manuscript —
29
29
  - [The Council](#the-council)
30
30
  - [Ways to run the council](#ways-to-run-the-council)
31
31
  - [Quick start](#quick-start)
32
+ - [1. Install](#1-install)
33
+ - [**2. Configure — don't skip this**](#2-configure--dont-skip-this)
34
+ - [3. Your first council](#3-your-first-council)
35
+ - [4. Your first sidecar](#4-your-first-sidecar)
32
36
  - [Requirements & Dependencies](#requirements--dependencies)
33
37
  - [The parallel window](#the-parallel-window)
34
38
  - [Commands](#commands)
@@ -170,7 +174,9 @@ Any flag you also type on that second line overrides just that value — a pack
170
174
  >
171
175
  > **Convention used throughout this README:** plugin-channel users: prefix CLI examples with `npx -y amicus@latest` (skip the bare `amicus`/`am`). Individual code blocks are not duplicated per channel — this note is the one translation you need.
172
176
 
173
- **Install** — pick whichever fits. Every path delivers the MCP server and both skills; the `amicus`/`am` CLI lands on your PATH with the **npm and install-script paths** (the plugin path runs the CLI on demand via `npx -y amicus@latest <command>`):
177
+ ### 1. Install
178
+
179
+ Pick whichever fits. Every path delivers the MCP server and both skills; the `amicus`/`am` CLI lands on your PATH with the **npm and install-script paths** (the plugin path runs the CLI on demand via `npx -y amicus@latest <command>`):
174
180
 
175
181
  **As a Claude Code plugin** — the most native path if you use Claude Code:
176
182
 
@@ -207,7 +213,15 @@ For the **npm** and **install-script** paths, a postinstall auto-configures ever
207
213
 
208
214
  > **Skipped the postinstall?** `--ignore-scripts` npm installs never run it, and the plugin channel skips it by design (Claude Code registers the plugin's MCP server and skills itself). Either way, run `amicus init` (plugin channel: `npx -y amicus@latest init`) any time to (re)register on demand — e.g. to also wire up Claude Desktop, which the plugin path doesn't touch. See [`amicus init`](./docs/usage.md#amicus-init).
209
215
 
210
- **Configure:**
216
+ ---
217
+
218
+ ### 2. Configure — don't skip this
219
+
220
+ > ### ⚠️ Installing is not enough. Run this or nothing will work.
221
+ >
222
+ > Amicus has **no API keys of its own** — it drives *your* accounts at OpenRouter, Google,
223
+ > OpenAI, Anthropic, or DeepSeek. Until you add at least one key, every council and every
224
+ > sidecar fails at the first model call. **This is the step people skip.**
211
225
 
212
226
  ```bash
213
227
  amicus setup
@@ -215,6 +229,10 @@ amicus setup
215
229
  npx -y amicus@latest setup
216
230
  ```
217
231
 
232
+ **One key is enough to start.** [OpenRouter](https://openrouter.ai/keys) is the usual choice —
233
+ a single key reaches every model in the catalog, which is what makes a mixed-vendor council
234
+ work without four separate accounts.
235
+
218
236
  This opens a graphical wizard:
219
237
 
220
238
  | Step | What it does |
@@ -226,7 +244,20 @@ This opens a graphical wizard:
226
244
 
227
245
  > **Headless environments:** if Electron can't open a window, the wizard falls back to a readline-based setup in the terminal.
228
246
 
229
- **Your first council** — no flags to learn. In Claude Code or Cowork, give Claude a document and say:
247
+ **Confirm it took** — this is the fastest way to know you're actually ready:
248
+
249
+ ```bash
250
+ amicus doctor
251
+ ```
252
+
253
+ Green across the board (`0 error(s)`) means keys, catalog, engine, skills, and MCP registration
254
+ are all in place. Anything red, `amicus doctor` tells you exactly what to run next.
255
+
256
+ ---
257
+
258
+ ### 3. Your first council
259
+
260
+ No flags to learn. In Claude Code or Cowork, give Claude a document and say:
230
261
 
231
262
  > *council review this*
232
263
 
@@ -234,7 +265,9 @@ Plugin-channel users can also type **`/amicus:council`** directly instead of phr
234
265
 
235
266
  Claude prepares the material, recommends a bench of models, discloses the run shape and cost, and orchestrates the rest. You make the accept/deny calls at the end. (The `second-opinion` skill is what teaches Claude to recognize this — if nothing happens, run `amicus doctor` (or `npx -y amicus@latest doctor`). npm/install-script installs place the skill at `~/.claude/skills/second-opinion/`; plugin installs keep it inside the plugin itself — check `/plugin` in Claude Code to confirm amicus is enabled.)
236
267
 
237
- **Your first sidecar.** The sidecar is the lower-level path — you can invoke it by phrase through Claude too, but the CLI gives you the flags directly:
268
+ ### 4. Your first sidecar
269
+
270
+ The sidecar is the lower-level path — you can invoke it by phrase through Claude too, but the CLI gives you the flags directly:
238
271
 
239
272
  ```bash
240
273
  amicus start --model gemini --prompt "Fact-check the auth approach Claude just proposed"
@@ -380,7 +413,7 @@ $ amicus status demo123 --json
380
413
  "taskId": "demo123",
381
414
  "status": "complete",
382
415
  "elapsed": "5m 0s",
383
- "version": "4.5.2",
416
+ "version": "4.5.3",
384
417
  "model": "google/gemini-2.5-flash",
385
418
  "phase": "terminal"
386
419
  }
package/bin/amicus.js CHANGED
@@ -22,6 +22,7 @@ const { handleStart, handleFanout, handleRead } = require('../src/cli-handlers-r
22
22
  const { handleResume, handleContinue } = require('../src/cli-handlers-resume-continue');
23
23
  const { isOneShotCommand, armExitWatchdog } = require('../src/utils/lifecycle');
24
24
  const { suggestCommand } = require('../src/utils/input-validators');
25
+ const { unknownFlags, getKnownFlags } = require('../src/utils/known-flags');
25
26
  const { logger } = require('../src/utils/logger');
26
27
 
27
28
  const VERSION = require('../package.json').version;
@@ -30,6 +31,28 @@ async function main() {
30
31
  const args = parseArgs(process.argv.slice(2));
31
32
  const command = args._[0];
32
33
 
34
+ // Reject unknown flags BEFORE anything can act on a half-understood command
35
+ // line. parseArgs accepts any `--token`, so an unrecognized one used to land
36
+ // on `args`, go unread, and let the command run as though it were never typed
37
+ // — `start … --headless` silently took the interactive path, ignored `--model`
38
+ // and left a session running. Same treatment as an unknown command: name it,
39
+ // suggest the nearest real flag, point at help, exit 1. See
40
+ // src/utils/known-flags.js for what counts as known and why.
41
+ const badFlags = unknownFlags(args);
42
+ if (badFlags.length > 0) {
43
+ for (const flag of badFlags) {
44
+ console.error(`Unknown option: --${flag}`);
45
+ const candidates = suggestCommand(flag, [...getKnownFlags()]);
46
+ if (candidates.length > 0) {
47
+ console.error(`Did you mean: ${candidates.map(c => `--${c}`).join(', ')}`);
48
+ }
49
+ }
50
+ console.error(command
51
+ ? `Run \`amicus ${command} --help\` to see valid options.`
52
+ : 'Run `amicus --help` to see valid options.');
53
+ process.exit(1);
54
+ }
55
+
33
56
  // Install crash handler for MCP-spawned processes (have --task-id)
34
57
  if (args['task-id'] && (command === 'start' || command === 'continue')) {
35
58
  const { installCrashHandler } = require('../src/sidecar/crash-handler');
package/docs/usage.md CHANGED
@@ -443,7 +443,7 @@ $ amicus status demo123 --json
443
443
  "taskId": "demo123",
444
444
  "status": "complete",
445
445
  "elapsed": "5m 0s",
446
- "version": "4.5.2",
446
+ "version": "4.5.3",
447
447
  "model": "google/gemini-2.5-flash",
448
448
  "phase": "terminal"
449
449
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amicus",
3
- "version": "4.5.2",
3
+ "version": "4.5.3",
4
4
  "mcpName": "io.github.BourbonDog/amicus",
5
5
  "description": "Multi-model LLM Council + parallel AI window for Claude Code. Run structured council reviews across Gemini, GPT, DeepSeek and more — or fork a conversation to any model and fold the results back.",
6
6
  "keywords": [
package/src/cli.js CHANGED
@@ -122,10 +122,12 @@ function parseArgs(argv) {
122
122
  }
123
123
 
124
124
  /**
125
- * Check if a flag is boolean (doesn't take a value)
125
+ * Flags that take no value. Module-level (not rebuilt per call) so
126
+ * `getBooleanFlags()` can hand the same list to src/utils/known-flags.js —
127
+ * which needs it to tell a real boolean flag from an unknown token, and must
128
+ * not keep a second copy that can rot out of sync with this one.
126
129
  */
127
- function isBooleanFlag(key) {
128
- const booleanFlags = [
130
+ const BOOLEAN_FLAGS = [
129
131
  'no-ui',
130
132
  'no-mcp',
131
133
  'no-context',
@@ -156,8 +158,21 @@ function isBooleanFlag(key) {
156
158
  'ui', // watch: open the Council Workspace window; v4.4 seam (v4.3 Task 11)
157
159
  'follow', // fanout / council run: stream this run's own events to stderr (v4.3 Task 13)
158
160
  'fallback', // fanout / council run: opt-in cheaper-model substitution (v4.3 Task 18, spec 6.2); --no-fallback negates via the generic no-* catch-all below
159
- ];
160
- return booleanFlags.includes(key);
161
+ ];
162
+
163
+ /**
164
+ * Check if a flag is boolean (doesn't take a value)
165
+ */
166
+ function isBooleanFlag(key) {
167
+ return BOOLEAN_FLAGS.includes(key);
168
+ }
169
+
170
+ /**
171
+ * The boolean-flag names, for the unknown-flag check.
172
+ * @returns {string[]} copy — callers must not mutate the source list
173
+ */
174
+ function getBooleanFlags() {
175
+ return [...BOOLEAN_FLAGS];
161
176
  }
162
177
 
163
178
  /**
@@ -765,5 +780,6 @@ module.exports = {
765
780
  validateStartArgs,
766
781
  getUsage,
767
782
  getCommandNames,
783
+ getBooleanFlags,
768
784
  DEFAULTS
769
785
  };
@@ -0,0 +1,90 @@
1
+ // src/utils/known-flags.js
2
+ 'use strict';
3
+
4
+ /**
5
+ * @module utils/known-flags
6
+ * The set of `--flags` amicus accepts, and the check that rejects the rest.
7
+ *
8
+ * WHY THIS EXISTS. `parseArgs` (src/cli.js) treats ANY `--token` as a flag: an
9
+ * unrecognized one lands on the parsed object, no handler reads it, and the
10
+ * command proceeds as if it were never typed. Found in the field while smoke-
11
+ * testing v4.5.2 — `amicus start -m deepseek --prompt "…" --headless` printed no
12
+ * error and exited 0, but `start` has no `--headless`. The run silently took the
13
+ * INTERACTIVE path, ignored `-m`, and left a session running against the default
14
+ * model. A typo (`--modl`), a flag borrowed from another command, or an invented
15
+ * one all behave the same way, and an unknown flag followed by a positional
16
+ * SWALLOWS it as its value.
17
+ *
18
+ * amicus already handles an unknown COMMAND correctly — error, "Did you mean",
19
+ * usage, exit 1. This gives flags the same treatment.
20
+ *
21
+ * DERIVED, NOT HAND-MAINTAINED. The bulk of the set is scraped from the usage
22
+ * text, which is the same source `getCommandNames()` uses and for the same
23
+ * stated reason: a second hand-maintained list would rot out of sync with the
24
+ * first. Only flags that are deliberately absent from usage are listed here, and
25
+ * each says why.
26
+ */
27
+
28
+ /**
29
+ * Real flags that appear in NO usage block. Rejecting any of these would break
30
+ * working callers, so they are enumerated deliberately rather than derived.
31
+ *
32
+ * ⚠️ The first four are spawned by the MCP server onto its own CLI children
33
+ * (src/mcp-server.js, src/mcp-council-run.js). They are not user-facing and are
34
+ * intentionally undocumented — but they are on the argv of every MCP-launched
35
+ * run, so rejecting them would break the entire MCP surface.
36
+ */
37
+ const INTERNAL_FLAGS = new Set([
38
+ 'task-id', // MCP → `start`/`continue`: pins the child's task id
39
+ 'run-id', // MCP → `council run`: pins the child's run id
40
+ 'council-name', // MCP → `council run`: preset name for ledger attribution
41
+ 'cowork-process', // MCP → `start`: Cowork process handle for context capture
42
+
43
+ // User-facing but undocumented, and read by real handlers today. Listed so the
44
+ // rejection is a bug fix and not a silent removal of working behaviour; if any
45
+ // of these should be dropped, that is a separate, deliberate change.
46
+ 'briefing', // src/cli-handlers-resume-continue.js — alias for --prompt
47
+ 'mode', // src/cli-handlers-run.js — legacy alias for --agent
48
+ 'quiet', // src/cli-handlers.js — suppresses the interactive prompt
49
+ 'help', // handled before dispatch; never reaches a usage block body
50
+ ]);
51
+
52
+ /**
53
+ * Every flag amicus accepts: usage-derived ∪ boolean flags ∪ INTERNAL_FLAGS.
54
+ *
55
+ * Computed on each call rather than cached at module load, because `getUsage()`
56
+ * is itself composed at call time and a cached copy would pin whatever the first
57
+ * caller happened to see.
58
+ *
59
+ * @returns {Set<string>} kebab-case flag names, without the leading `--`
60
+ */
61
+ function getKnownFlags() {
62
+ // Required lazily: src/cli.js is the module that will consume this one, and a
63
+ // top-level require here would close a cycle.
64
+ const { getUsage, getBooleanFlags } = require('../cli');
65
+ const known = new Set(INTERNAL_FLAGS);
66
+ for (const m of getUsage().matchAll(/--([a-z][a-z0-9-]*)/gi)) {
67
+ known.add(m[1].toLowerCase());
68
+ }
69
+ for (const f of getBooleanFlags()) { known.add(f); }
70
+ return known;
71
+ }
72
+
73
+ /**
74
+ * The unknown flags present on a parsed-argv object, in the order they were
75
+ * typed.
76
+ *
77
+ * Reads `__explicit` — the set parseArgs fills with every key it saw on the
78
+ * command line — so defaults (which are merged in from DEFAULTS and were never
79
+ * typed) are correctly ignored.
80
+ *
81
+ * @param {object} parsed result of parseArgs()
82
+ * @returns {string[]} kebab-case flag names, without the leading `--`
83
+ */
84
+ function unknownFlags(parsed) {
85
+ if (!parsed || !parsed.__explicit) { return []; }
86
+ const known = getKnownFlags();
87
+ return [...parsed.__explicit].filter(f => !known.has(String(f).toLowerCase()));
88
+ }
89
+
90
+ module.exports = { getKnownFlags, unknownFlags, INTERNAL_FLAGS };