amicus 4.5.2 → 4.5.4

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.4",
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,60 @@
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.4] - 2026-08-01
7
+
8
+ ### Fixed
9
+
10
+ - **README: corrected a false claim about Electron and install channels.** The install section
11
+ said "the standalone Electron window is npm-only." That is not true — the Council Workspace
12
+ auto-open gate (`src/sidecar/workspace-auto-open.js`) keys on `client === 'code-local'` plus
13
+ Electron presence, **not** on install channel, so a plugin-channel user in Claude Code local
14
+ does get the window. Removed.
15
+
16
+ ### Changed
17
+
18
+ - **README now leads with npm as the recommended install**, with a per-channel comparison table.
19
+ The accurate reason npm is preferable for the interactive experience: the plugin's MCP config
20
+ sets `AMICUS_SKIP_POSTINSTALL=1`, and `scripts/postinstall.js` returns early on that — *before*
21
+ `provisionElectron()`. So the plugin channel gets no `amicus` on `PATH` (every window-opening
22
+ command becomes an `npx` call), no Electron provisioning or cache-heal, no reachable
23
+ `amicus doctor --fix` when the GUI breaks, and a fresh npx cache directory on every release.
24
+ The plugin block keeps its genuine strengths — native registration and the slash commands the
25
+ npm paths don't have — alongside an accurate statement of the tradeoff.
26
+ - **README documents the single-MCP-registration behavior** when both channels are installed.
27
+ Config, API keys, and session history are shared, but the MCP server is one registration named
28
+ `amicus` that resolves to whichever install registered most recently — so the copy the CLI runs
29
+ and the copy Claude's MCP tools run can differ. This is the #76 confusion; `amicus doctor`
30
+ reports the MCP launch path and `--fix` repairs that copy in place.
31
+
32
+ ## [4.5.3] - 2026-08-01
33
+
34
+ ### Fixed
35
+
36
+ - **Unknown CLI flags are rejected instead of silently absorbed.** `parseArgs` treated any
37
+ `--token` as a flag: an unrecognized one landed on the parsed args object, no handler read it,
38
+ and the command ran as though it were never typed. Found while smoke-testing v4.5.2 —
39
+ `amicus start -m deepseek --prompt "…" --headless` printed no error and exited 0, but `start`
40
+ has no `--headless`; the run silently took the **interactive** path, ignored `-m`, and left a
41
+ session running against the default model. A typo (`--modl`), a flag borrowed from another
42
+ command, or an invented one all behaved the same way, and an unknown flag followed by a
43
+ positional would swallow it as its value. Unknown flags now get the same treatment amicus
44
+ already gave an unknown *command*: name it, suggest the nearest real flag, point at `--help`,
45
+ exit 1. The known-flag set is **derived from the usage text** (the same source `getCommandNames()`
46
+ uses, for the same anti-rot reason) plus the boolean-flag list, plus a small explicitly-documented
47
+ allowlist of internal MCP→CLI passthroughs (`--task-id`, `--run-id`, `--council-name`,
48
+ `--cowork-process`) and undocumented-but-working flags (`--briefing`, `--mode`, `--quiet`).
49
+ A regression test asserts every `args.<flag>` any CLI handler reads is in the known set, so the
50
+ check can never silently start rejecting a flag that works.
51
+
52
+ ### Changed
53
+
54
+ - **README Quick start is now four numbered steps**, with **Configure** promoted from a bold line
55
+ buried between two callouts to its own step and TOC entry. Installing without configuring is the
56
+ step people skip, and every council fails at the first model call when they do — so it now says
57
+ so plainly, notes that one OpenRouter key is enough to start, and ends with `amicus doctor` as
58
+ the confirmation that setup actually took.
59
+
6
60
  ## [4.5.2] - 2026-07-31
7
61
 
8
62
  ### 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)
@@ -165,24 +169,40 @@ Any flag you also type on that second line overrides just that value — a pack
165
169
 
166
170
  > **Two install channels — read this first.** Amicus ships two ways, and CLI commands look different in each:
167
171
  >
168
- > - **npm global** (`npm install -g amicus` or the install script) puts `amicus`/`am` on your `PATH`. Every `amicus <command>` example in this README works as written.
172
+ > - **npm global** (`npm install -g amicus` or the install script) **the recommended path.** Puts `amicus`/`am` on your `PATH`, so every `amicus <command>` example in this README works as written, and provisions the Electron GUI that the parallel window runs in.
169
173
  > - **Claude Code plugin** (`/plugin install amicus@bourbondog-amicus`) does **not** put a CLI on your `PATH`. CLI calls go through `npx -y amicus@latest <command>` instead — e.g. `amicus doctor` becomes `npx -y amicus@latest doctor`. In exchange, the plugin channel gets two things npm does **not**: the slash commands `/amicus:council` and `/amicus:sidecar`. **These are plugin-channel-ONLY — npm users don't get them** and drive the same skills by saying "council review this" / talking to Claude instead.
170
174
  >
175
+ > See the [comparison table below](#1-install) for the full tradeoff — the short version is that npm is what you want for the interactive window, and the two can be installed side by side.
176
+ >
171
177
  > **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
178
 
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>`):
179
+ ### 1. Install
174
180
 
175
- **As a Claude Code plugin** the most native path if you use Claude Code:
181
+ Every path delivers the MCP server and both skills. They differ in what else you get:
176
182
 
177
- ```text
178
- /plugin marketplace add BourbonDog/amicus
179
- /plugin install amicus@bourbondog-amicus
180
- /reload-plugins
183
+ | | **npm / install script** | **Claude Code plugin** |
184
+ |---|---|---|
185
+ | `amicus` / `am` on your `PATH` | ✅ | ❌ — every call is `npx -y amicus@latest <command>` |
186
+ | **Interactive Electron window** (`amicus start`, `watch --ui`) | ✅ provisioned at install | ⚠️ best-effort — see below |
187
+ | Self-heal when the GUI breaks (`amicus doctor --fix`) | ✅ | ❌ no CLI to run it with |
188
+ | MCP server + both skills | ✅ | ✅ |
189
+ | Slash commands `/amicus:council`, `/amicus:sidecar` | ❌ | ✅ |
190
+
191
+ ---
192
+
193
+ #### With npm — recommended
194
+
195
+ The canonical path, and the one that gets you the full interactive experience (needs [Node.js](https://nodejs.org) ≥ 18):
196
+
197
+ ```bash
198
+ npm install -g amicus
181
199
  ```
182
200
 
183
- Claude Code registers the MCP server and both skills for you nothing to configure. It also gets you two slash commands the npm/install-script paths don't: **`/amicus:council`** (run a full council review) and **`/amicus:sidecar`** (fork a conversation to another model). (The plugin does not put `amicus` on your PATH — CLI calls go through `npx -y amicus@latest <command>`; the standalone Electron window is npm-only; and the first council/sidecar call downloads the OpenCode engine.)
201
+ This is the path to pick unless you specifically want the plugin's slash commands. It puts `amicus`/`am` on your `PATH` which is what the **parallel window** is driven by and its postinstall provisions the Electron GUI, with `amicus doctor --fix` to repair it in place if anything goes wrong later.
202
+
203
+ #### With the install script
184
204
 
185
- **With the install script** — macOS, Linux, or Windows (needs [Node.js](https://nodejs.org) ≥ 18):
205
+ Same result as npm, one command — macOS, Linux, or Windows (needs [Node.js](https://nodejs.org) ≥ 18):
186
206
 
187
207
  ```bash
188
208
  # macOS / Linux
@@ -194,12 +214,26 @@ curl -fsSL https://raw.githubusercontent.com/BourbonDog/amicus/main/install.sh |
194
214
  irm https://raw.githubusercontent.com/BourbonDog/amicus/main/install.ps1 | iex
195
215
  ```
196
216
 
197
- **With npm** the canonical path (needs [Node.js](https://nodejs.org) ≥ 18):
217
+ #### As a Claude Code plugin
198
218
 
199
- ```bash
200
- npm install -g amicus
219
+ The most native *registration* path if you use Claude Code, and the only one with slash commands:
220
+
221
+ ```text
222
+ /plugin marketplace add BourbonDog/amicus
223
+ /plugin install amicus@bourbondog-amicus
224
+ /reload-plugins
201
225
  ```
202
226
 
227
+ Claude Code registers the MCP server and both skills for you — nothing to configure. You also get **`/amicus:council`** (run a full council review) and **`/amicus:sidecar`** (fork a conversation to another model), which the npm paths don't have.
228
+
229
+ > **Know the tradeoff before you pick this.** The plugin does not put `amicus` on your `PATH`, so every CLI call goes through `npx -y amicus@latest <command>` — including the ones that open the interactive window. It also skips amicus's postinstall, which is what provisions and self-heals the Electron GUI. The window still works when Electron lands in the npx cache, and the Council Workspace still auto-opens on a council run from Claude Code — but nothing repairs it when Electron *doesn't* land, and each new release re-resolves into a fresh cache directory. **If you want the parallel window as a daily driver, install with npm.**
230
+ >
231
+ > (Also: the first council/sidecar call downloads the OpenCode engine.)
232
+
233
+ **Running both is supported** — and is what you want if you like the slash commands *and* the window. Install with npm for the CLI and the GUI, then add the plugin for `/amicus:council`. Your config, API keys, and session history live outside either install and are shared automatically.
234
+
235
+ > One thing to know if you do: the MCP server is a **single registration named `amicus`**, so it resolves to one install — whichever registered most recently, which is usually the plugin's `npx -y amicus@latest mcp`. That's harmless (both serve the same tools), but it means the copy your CLI runs and the copy Claude's MCP tools run can differ. `amicus doctor` reports the MCP launch path explicitly and `--fix` repairs that copy in place, so if a GUI or engine problem ever shows up in Claude but not in your terminal, that's the first thing to check.
236
+
203
237
  For the **npm** and **install-script** paths, a postinstall auto-configures everything — no manual registration:
204
238
 
205
239
  - Registers the **MCP server** in Claude Code and in Claude Desktop / Cowork, so the Amicus tools appear natively.
@@ -207,7 +241,15 @@ For the **npm** and **install-script** paths, a postinstall auto-configures ever
207
241
 
208
242
  > **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
243
 
210
- **Configure:**
244
+ ---
245
+
246
+ ### 2. Configure — don't skip this
247
+
248
+ > ### ⚠️ Installing is not enough. Run this or nothing will work.
249
+ >
250
+ > Amicus has **no API keys of its own** — it drives *your* accounts at OpenRouter, Google,
251
+ > OpenAI, Anthropic, or DeepSeek. Until you add at least one key, every council and every
252
+ > sidecar fails at the first model call. **This is the step people skip.**
211
253
 
212
254
  ```bash
213
255
  amicus setup
@@ -215,6 +257,10 @@ amicus setup
215
257
  npx -y amicus@latest setup
216
258
  ```
217
259
 
260
+ **One key is enough to start.** [OpenRouter](https://openrouter.ai/keys) is the usual choice —
261
+ a single key reaches every model in the catalog, which is what makes a mixed-vendor council
262
+ work without four separate accounts.
263
+
218
264
  This opens a graphical wizard:
219
265
 
220
266
  | Step | What it does |
@@ -226,7 +272,20 @@ This opens a graphical wizard:
226
272
 
227
273
  > **Headless environments:** if Electron can't open a window, the wizard falls back to a readline-based setup in the terminal.
228
274
 
229
- **Your first council** — no flags to learn. In Claude Code or Cowork, give Claude a document and say:
275
+ **Confirm it took** — this is the fastest way to know you're actually ready:
276
+
277
+ ```bash
278
+ amicus doctor
279
+ ```
280
+
281
+ Green across the board (`0 error(s)`) means keys, catalog, engine, skills, and MCP registration
282
+ are all in place. Anything red, `amicus doctor` tells you exactly what to run next.
283
+
284
+ ---
285
+
286
+ ### 3. Your first council
287
+
288
+ No flags to learn. In Claude Code or Cowork, give Claude a document and say:
230
289
 
231
290
  > *council review this*
232
291
 
@@ -234,7 +293,9 @@ Plugin-channel users can also type **`/amicus:council`** directly instead of phr
234
293
 
235
294
  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
295
 
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:
296
+ ### 4. Your first sidecar
297
+
298
+ 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
299
 
239
300
  ```bash
240
301
  amicus start --model gemini --prompt "Fact-check the auth approach Claude just proposed"
@@ -380,7 +441,7 @@ $ amicus status demo123 --json
380
441
  "taskId": "demo123",
381
442
  "status": "complete",
382
443
  "elapsed": "5m 0s",
383
- "version": "4.5.2",
444
+ "version": "4.5.4",
384
445
  "model": "google/gemini-2.5-flash",
385
446
  "phase": "terminal"
386
447
  }
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.4",
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.4",
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 };