amicus 1.5.1 → 1.6.1

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": "1.5.1",
3
+ "version": "1.6.1",
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": { "name": "Christian Wagner" },
6
6
  "homepage": "https://bourbondog.github.io/amicus/",
package/CHANGELOG.md CHANGED
@@ -5,6 +5,73 @@ All notable changes to Amicus are documented here. Format follows
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.6.1] - 2026-06-30
9
+
10
+ Project-directory and session-addressing correctness — agents, sessions, and the interactive GUI now agree on which project they're in.
11
+
12
+ ### Added
13
+ - **`AMICUS_PROJECT_DIR` + MCP `roots` support.** When the project is not passed explicitly, the MCP
14
+ server now resolves the working directory from the client's first `file://` workspace root (falling
15
+ back to `AMICUS_PROJECT_DIR`, then the process cwd) — so a stdio MCP server spawned by a desktop
16
+ client no longer roots agents in the app install directory where they can't see your files.
17
+ - **Global session index.** `amicus_status` / `amicus_read` / `amicus_list` now consult a global
18
+ `taskId -> project` index on a per-project miss, so a session created in one project is still found
19
+ when looked up from another.
20
+ - **Per-command help for the rest of the CLI.** `amicus council --help` (and `continue`, `resume`,
21
+ `doctor`, `setup`, `key`, `mcp`) now print their own scoped usage instead of the full global help.
22
+
23
+ ### Fixed
24
+ - **Interactive `--cwd`: follow-up prompts no longer fail "unable to retrieve session."** When the
25
+ launch directory differs from `--cwd` (the normal sidecar-skill pattern), the OpenCode session is now
26
+ scoped to the project directory and the Electron Web-UI route is built from the **server-echoed**
27
+ session directory rather than a guessed one, so turn 2+ resolve correctly.
28
+ - **Shared-server MCP sessions are scoped to the project directory** — every create and follow-up call
29
+ carries the directory, so headless MCP sessions are found on a server shared across projects.
30
+ - **`amicus_read` surfaces the failure reason** for crashed / timed-out / aborted runs that wrote no
31
+ summary, instead of a bare "No summary available."
32
+ - **`amicus_abort`'s "session not found"** now names the resolved project, matching `status` / `read`.
33
+ - Internal: a single `canonicalProjectPath()` now normalizes project paths (slash direction, drive-letter
34
+ case, trailing slash, UNC shares) so creation and lookup always agree.
35
+
36
+ ## [1.6.0] - 2026-06-30
37
+
38
+ Install resilience and council-failure correctness — the first two blocks of the post-1.5 backlog program.
39
+
40
+ ### Added
41
+ - **Per-subcommand help.** `amicus <command> --help` now prints only that command's options instead of
42
+ the full global usage; bare `amicus --help` is unchanged.
43
+ - **Zero-credit OpenRouter key warning at setup.** Setup now does a non-blocking `GET /api/v1/key`
44
+ check and warns when a key is free-tier or has no remaining credit, so a credit-less key is flagged
45
+ up front instead of 402-ing on the first paid model call.
46
+ - **`amicus doctor` engine-recovery guidance.** The opencode-engine check now explains the
47
+ transient install-rollback failure mode and gives copy-paste recovery steps.
48
+ - **Postinstall verifies the Electron binary.** When the optional Electron download/extract fails (or
49
+ AV quarantines the binary), the install now prints a clear non-fatal notice that headless runs and
50
+ the council still work — instead of silently leaving a broken GUI to discover later.
51
+ - **CI tarball guard.** A new `check:tarball` step asserts every lifecycle-referenced script actually
52
+ ships in the published package, so a future packaging change can't silently drop it.
53
+ - **README "Requirements & Dependencies" section** consolidating Node, git, OpenRouter credits, API-key
54
+ env vars, the optional Electron GUI, the bundled opencode engine, and OS support.
55
+
56
+ ### Fixed
57
+ - **Council / headless runs no longer report success when every model call fails.** On the shared-server
58
+ MCP path, a run whose calls all errored (e.g. an OpenRouter 402) was finalized as `complete` with a
59
+ 0-byte summary, so `amicus_status` showed success and the error was lost. Non-2xx/402 responses are
60
+ now detected at the OpenCode client boundary even when no assistant message is emitted, the
61
+ shared-server finalize routes through the same terminal-state classifier as the CLI, and a failed run
62
+ can never silently default to `complete`; `amicus_read` surfaces the failure reason.
63
+ - **`amicus_status` elapsed time** is now bounded by the run's completed/aborted/crashed timestamp
64
+ instead of wall-clock-since-start, so a finished run reports its real duration.
65
+ - **`amicus_setup` (MCP)** no longer claims an Electron window appeared when Electron is unavailable —
66
+ it pre-flights and returns an honest error directing you to the headless terminal wizard.
67
+ - **Clearer "session not found"** — the message now names the resolved project so you know to pass the
68
+ original `project`.
69
+ - **Non-fatal postinstall.** An internal skill-copy / MCP-registration failure no longer exits non-zero
70
+ and rolls back the entire global install; it warns and continues.
71
+ - **`github:` install on Windows now runs identically to the registry install.** Removed the
72
+ consumer-facing `prepare` lifecycle that triggered npm's clone→prepare→nested-install→cached-pack
73
+ path (the rollback source); git hooks are still configured for contributors via `postinstall`.
74
+
8
75
  ## [1.5.1] - 2026-06-29
9
76
 
10
77
  A headless-reliability fix for reasoning-heavy models.
package/README.md CHANGED
@@ -23,6 +23,7 @@ Hand Claude a plan, a design, a diff, an architecture decision, a manuscript —
23
23
 
24
24
  - [What is Amicus](#what-is-amicus)
25
25
  - [Quick start](#quick-start)
26
+ - [Requirements & Dependencies](#requirements--dependencies)
26
27
  - [The Council](#the-council)
27
28
  - [The parallel window](#the-parallel-window)
28
29
  - [Commands](#commands)
@@ -67,16 +68,6 @@ The postinstall step auto-configures everything — no manual registration:
67
68
  - Registers the **MCP server** in Claude Code and in Claude Desktop / Cowork, so the Amicus tools appear natively.
68
69
  - Installs **both skills** into `~/.claude/skills/` — `second-opinion` (the council) and `sidecar` (the chat skill).
69
70
 
70
- ## Prerequisites & what it costs you
71
-
72
- Before your first run:
73
-
74
- - **Node.js ≥ 18** — `node --version` to check.
75
- - **An active Claude Code or Cowork session** — Amicus is orchestrated by Claude; it is not a standalone chatbot.
76
- - **At least one paid model API key** — OpenRouter (covers the most models) or a direct Google / OpenAI / Anthropic / DeepSeek key. Add one with `amicus setup` or `amicus key <provider> <key>`.
77
-
78
- **What a run costs.** A sidecar is a single model call. A full council is typically **~5–8 paid model calls** (e.g. 3 reviewers across 2 fan-out waves + 1 chair). Amicus shows an estimate before each council and enforces a built-in budget gate that refuses ultra-expensive models (o3-pro class) unless you opt in with `--no-cost-gate`. You pay your providers directly for the tokens; Amicus itself is free and open-source.
79
-
80
71
  **Configure:**
81
72
 
82
73
  ```bash
@@ -110,18 +101,55 @@ A window opens alongside your editor with Gemini ready, pre-loaded with your con
110
101
 
111
102
  ### Install from GitHub
112
103
 
113
- The npm package is the primary path. To install straight from the repo instead (the postinstall runs identically same MCP registration, same two skills):
104
+ The npm package is the primary path. To install straight from the repo instead the postinstall runs **identically** (same MCP registration, same two skills) — you just need `git` on your `PATH`:
114
105
 
115
106
  ```bash
116
107
  npm install -g github:BourbonDog/amicus
117
108
  ```
118
109
 
110
+ See [Requirements & Dependencies](#requirements--dependencies) for the full prerequisite list.
111
+
119
112
  ### Contributor setup
120
113
 
121
114
  Cloning to develop Amicus? See **[CONTRIBUTING.md](./CONTRIBUTING.md)** for the dev setup, git-hook wiring, and test commands.
122
115
 
123
116
  ---
124
117
 
118
+ ## Requirements & Dependencies
119
+
120
+ Everything you need before your first run, and what's optional.
121
+
122
+ **Runtime**
123
+
124
+ - **Node.js ≥ 18** — `node --version` to check. This is the only hard runtime prerequisite.
125
+ - **An active Claude Code or Cowork session** — Amicus is orchestrated by Claude; it is not a standalone chatbot.
126
+
127
+ **Install path & the git toolchain**
128
+
129
+ - **From the npm registry (recommended):** `npm install -g amicus`. No build toolchain required — the package ships prebuilt.
130
+ - **From GitHub:** `npm install -g github:BourbonDog/amicus` runs **identically** — same MCP registration, same two skills, same postinstall. The one extra requirement is a working **git** on your `PATH`, since npm clones the repo to install it (`git --version` to check).
131
+
132
+ **Model API keys** — at least one is required
133
+
134
+ - **OpenRouter** covers the most models with one key, or use a **direct Google / OpenAI / Anthropic / DeepSeek** key. Add one with `amicus setup` or `amicus key <provider> <key>`; keys live in `~/.config/amicus/.env`. The supported env vars are `OPENROUTER_API_KEY`, `GOOGLE_GENERATIVE_AI_API_KEY` (the legacy `GEMINI_API_KEY` is still accepted), `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, and `DEEPSEEK_API_KEY`.
135
+ - ⚠️ **OpenRouter keys need purchased credits.** A brand-new, zero-credit key *passes* setup's live validation but then fails at runtime with a **402** on the first real call. Buy a small amount of credit before you run a council.
136
+
137
+ **Electron — optional (GUI only)**
138
+
139
+ - Electron is an **optional** dependency. It powers the graphical setup wizard and the parallel sidecar window. **Headless runs and the full council work without it** — if Electron is absent or can't open a window, `amicus setup` falls back to a readline wizard and sidecars run headless. `amicus doctor` reports Electron's presence accurately and never treats its absence as fatal.
140
+
141
+ **The OpenCode engine**
142
+
143
+ - The bundled **`opencode-ai`** engine (the conversation runtime) installs automatically as a normal dependency — you don't install it separately. Its own postinstall lays down ~11 per-platform binaries; a **transient** failure there (a spawn `ENOENT`, or an antivirus file-lock) can roll back the atomic install. If install fails partway or `amicus doctor` reports the OpenCode binary "not found", just **re-run** `npm install -g amicus` (clear the cache first if it persists: `npm cache clean --force`).
144
+
145
+ **OS support**
146
+
147
+ - **Windows 11, macOS, and Linux** are all supported; Amicus is first-class on **Windows** (developed and tested there, no WSL required). See the [Windows](#windows) section for platform specifics.
148
+
149
+ **What a run costs.** A sidecar is a single model call. A full council is typically **~5–8 paid model calls** (e.g. 3 reviewers across 2 fan-out waves + 1 chair). Amicus shows an estimate before each council and enforces a built-in budget gate that refuses ultra-expensive models (o3-pro class) unless you opt in with `--no-cost-gate`. You pay your providers directly for the tokens; Amicus itself is free and open-source.
150
+
151
+ ---
152
+
125
153
  ## The Council
126
154
 
127
155
  **Why multi-model.** Any single model — including the one running your session — has consistent blind spots. Route the *same* material through models from *different* families and the disagreements surface: missed issues, overstated confidence, claims one model alone would have waved through. The council is the structured version of that idea.
@@ -442,6 +470,7 @@ Most Claude-adjacent tooling assumes macOS/Linux; Amicus doesn't.
442
470
  |---------|--------------|-----|
443
471
  | "council review this" does nothing | The `second-opinion` skill isn't installed | Check `~/.claude/skills/second-opinion/SKILL.md` exists; re-run `npm install -g amicus` (postinstall installs both skills) |
444
472
  | `npm install -g amicus` fails with `EEXIST: … claude-sidecar` | The old upstream `claude-sidecar` package is still installed globally; npm won't overwrite another package's bin shims | `npm uninstall -g claude-sidecar`, then `npm install -g amicus`. Your config and sessions carry over (legacy paths are still read). |
473
+ | Install fails partway, or `amicus doctor` reports the OpenCode binary "not found" | A **transient** error during the OpenCode engine's own postinstall (a spawn `ENOENT`, or an antivirus file-lock while it lays down its 11 per-platform binaries) can roll back the whole atomic install — retrying usually succeeds | Just re-run `npm install -g amicus`. If it still fails, clear the cache first: `npm cache clean --force && npm install -g amicus`. |
445
474
  | `401` / auth error | API key missing, or the model prefix doesn't match the key you have | Run `amicus setup`; make sure the prefix (`openrouter/…` vs `google/…` vs `openai/…` vs `anthropic/…`) matches the credentials you configured. |
446
475
  | Session not found | No session matches the given ID | Run `amicus list`, or omit `--session-id` to use the most recent. |
447
476
  | No conversation history found | Project-path encoding | Check `~/.claude/projects/`; `/` and `_` in the project path are encoded as `-` in the directory name. |
package/bin/amicus.js CHANGED
@@ -75,9 +75,10 @@ async function main() {
75
75
  process.exit(0);
76
76
  }
77
77
 
78
- // Handle --help or no command
78
+ // Handle --help or no command. 'amicus <cmd> --help' scopes the usage to that
79
+ // subcommand; bare 'amicus --help' (or no command) prints the full usage.
79
80
  if (args.help || args._.length === 0) {
80
- console.log(getUsage());
81
+ console.log(getUsage(args.help ? command : undefined));
81
82
  process.exit(0);
82
83
  }
83
84
 
package/electron/main.js CHANGED
@@ -21,6 +21,7 @@ const { createFoldHandler } = require('./fold');
21
21
  const { registerSetupHandlers } = require('./ipc-setup');
22
22
  const { computeWindowPosition } = require('./window-position');
23
23
  const { attachLoadFailsafe, buildLoadErrorHTML } = require('./load-failsafe');
24
+ const { buildSessionRoute } = require('./session-route');
24
25
 
25
26
  const ICON_PATH = path.join(__dirname, 'assets', 'icon.png');
26
27
 
@@ -51,6 +52,11 @@ const MODE = getCompatEnv('MODE') || 'sidecar';
51
52
  const TASK_ID = getCompatEnv('TASK_ID') || 'unknown';
52
53
  const MODEL = getCompatEnv('MODEL') || 'unknown';
53
54
  const CWD = getCompatEnv('CWD') || process.cwd();
55
+ // The directory the OpenCode session was actually scoped to (#45). Set by the
56
+ // interactive launcher as canonicalProjectPath(--cwd) so the Web-UI route is
57
+ // built from the SAME directory createSession used. Falls back to CWD for
58
+ // back-compat with launchers that predate this env var.
59
+ const SESSION_DIRECTORY = getCompatEnv('SESSION_DIRECTORY') || CWD;
54
60
  const CLIENT = getCompatEnv('CLIENT') || 'code-local';
55
61
  const OPENCODE_PORT = parseInt(getCompatEnv('OPENCODE_PORT') || '4096', 10);
56
62
  const OPENCODE_SESSION_ID = getCompatEnv('SESSION_ID');
@@ -152,10 +158,10 @@ function createAmicusWindow() {
152
158
  });
153
159
 
154
160
  // Navigate directly to the session URL to bypass the project selection screen.
155
- // OpenCode's router format: /<base64url(projectPath)>/session/<sessionId>
156
- const contentUrl = OPENCODE_SESSION_ID
157
- ? `${OPENCODE_URL}/${Buffer.from(CWD).toString('base64url')}/session/${OPENCODE_SESSION_ID}`
158
- : OPENCODE_URL;
161
+ // Build the route from SESSION_DIRECTORY — the directory the session was
162
+ // actually scoped to — NOT a fresh base64url(CWD) guess, so Web-UI follow-up
163
+ // prompts resolve the session even when process cwd != --cwd (#45).
164
+ const contentUrl = buildSessionRoute(OPENCODE_URL, OPENCODE_SESSION_ID, SESSION_DIRECTORY);
159
165
 
160
166
  // The window only becomes visible on the success path below. Without this
161
167
  // failsafe, a failed/stalled UI load leaves an invisible window and a
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Web-UI session route builder (#45).
3
+ *
4
+ * OpenCode's router format is `/<base64url(projectPath)>/session/<sessionId>`.
5
+ * The route MUST be built from the directory the OpenCode session was actually
6
+ * created/scoped to — NOT a fresh base64url(process.cwd()) guess. When the
7
+ * amicus process cwd != --cwd (the normal sidecar-skill launch), a CWD-derived
8
+ * route points at a project route with no matching session, so Web-UI follow-up
9
+ * prompts fail "unable to retrieve session". The caller passes the same
10
+ * directory it used to scope createSession (the server-echoed session.directory,
11
+ * or a consistent canonicalProjectPath(--cwd) fallback).
12
+ *
13
+ * Pure function: no electron, no fs, no process state. Safe to unit-test.
14
+ *
15
+ * @param {string} baseUrl - OpenCode server base URL (e.g. http://localhost:4096)
16
+ * @param {string} [sessionId] - OpenCode session id; falsy → return baseUrl only
17
+ * @param {string} sessionDirectory - The directory the session is scoped to
18
+ * @returns {string} Fully-qualified route URL, or baseUrl when no session id.
19
+ */
20
+ function buildSessionRoute(baseUrl, sessionId, sessionDirectory) {
21
+ if (!sessionId) {
22
+ return baseUrl;
23
+ }
24
+ const seg = Buffer.from(sessionDirectory).toString('base64url');
25
+ return `${baseUrl}/${seg}/session/${sessionId}`;
26
+ }
27
+
28
+ module.exports = { buildSessionRoute };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amicus",
3
- "version": "1.5.1",
3
+ "version": "1.6.1",
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
  "keywords": [
6
6
  "claude",
@@ -63,9 +63,10 @@
63
63
  "generate-docs": "node scripts/generate-docs.js",
64
64
  "generate-docs:check": "node scripts/generate-docs.js --check",
65
65
  "validate-docs": "node scripts/validate-docs.js --full",
66
- "prepare": "node scripts/setup-hooks.js",
66
+ "setup-hooks": "node scripts/setup-hooks.js",
67
67
  "check:secrets": "node scripts/check-secrets.js --all",
68
- "check:sizes": "node scripts/check-file-sizes.js --all"
68
+ "check:sizes": "node scripts/check-file-sizes.js --all",
69
+ "check:tarball": "node scripts/check-tarball-lifecycle.js"
69
70
  },
70
71
  "dependencies": {
71
72
  "@modelcontextprotocol/sdk": "^1.27.0",
@@ -13,6 +13,8 @@ const path = require('path');
13
13
  const os = require('os');
14
14
  const { execFileSync } = require('child_process');
15
15
 
16
+ const SETUP_HOOKS_SCRIPT = path.join(__dirname, 'setup-hooks.js');
17
+
16
18
  const SKILL_SOURCE = path.join(__dirname, '..', 'skills', 'sidecar', 'SKILL.md');
17
19
  const COUNCIL_SOURCE_DIR = path.join(__dirname, '..', 'skills', 'second-opinion');
18
20
 
@@ -172,6 +174,79 @@ function registerClaudeDesktop() {
172
174
  addMcpToConfigFile(configPath, 'sidecar', MCP_CONFIG);
173
175
  }
174
176
 
177
+ /**
178
+ * Resolve the Electron binary path the same way src/sidecar/interactive.js
179
+ * getElectronPath() does: require('electron') returns the absolute path to the
180
+ * binary (or throws if the optionalDependency never installed/extracted).
181
+ * @returns {string|null} Path to the Electron binary, or null if unresolvable.
182
+ */
183
+ function resolveElectron() {
184
+ try {
185
+ return require('electron');
186
+ } catch {
187
+ return null;
188
+ }
189
+ }
190
+
191
+ /**
192
+ * NON-FATAL verification that the OPTIONAL electron binary actually extracted.
193
+ * Electron is an optionalDependency: npm exits 0 even if its download/extract
194
+ * fails (or AV quarantines electron.exe), so without this the user only finds
195
+ * out the GUI is broken much later. Warn clearly that headless runs + the
196
+ * council still work, and point at `amicus doctor` / reinstall to get the GUI.
197
+ *
198
+ * This MUST never throw out of postinstall — the whole body is guarded so a
199
+ * resolver failure or a missing fs can never turn into a non-zero exit.
200
+ *
201
+ * @param {object} deps - { resolveElectron } override for testing.
202
+ */
203
+ function verifyElectron(deps = {}) {
204
+ try {
205
+ const _resolve = deps.resolveElectron || resolveElectron;
206
+ const binPath = _resolve();
207
+ if (binPath && fs.existsSync(binPath)) { return; }
208
+ console.warn('[amicus] Warning: the Electron binary did not install — the interactive GUI / setup-wizard is unavailable.');
209
+ console.warn('[amicus] Headless runs and the council still work. Run `amicus doctor` to check, or `npm install -g amicus` to reinstall and add the GUI.');
210
+ } catch {
211
+ // Never let the electron check throw out of postinstall.
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Configure git hooks for DEVELOPERS, folded into postinstall (#35).
217
+ *
218
+ * Previously this ran via npm's "prepare" lifecycle. But "prepare" also fires
219
+ * on the github: install path (`npm install -g github:BourbonDog/amicus`),
220
+ * where npm clones the repo, runs prepare, does a NESTED devDependency install,
221
+ * and re-packs — a fragile pipeline the registry install SKIPS entirely. That
222
+ * divergence (plus a reusable corrupt cached artifact) is what made github:
223
+ * installs roll back, especially on Windows. Removing the consumer-facing
224
+ * prepare lifecycle makes the github: path behave identically to the registry
225
+ * path.
226
+ *
227
+ * Hook setup still needs to happen for devs, so we run setup-hooks.js here.
228
+ * It is a no-op for consumers: setup-hooks.js exits 0 outside a git checkout
229
+ * (the published tarball / github: export has no .git), so this changes
230
+ * nothing for end users while keeping `npm install` wiring hooks for devs.
231
+ *
232
+ * Best-effort and self-contained: never throws (a hook-setup failure must
233
+ * never roll back the install), so it is safe to call before the optional
234
+ * skill/MCP setup.
235
+ *
236
+ * @param {object} deps - { env } override for testing.
237
+ */
238
+ function setupHooks(deps = {}) {
239
+ try {
240
+ execFileSync('node', [SETUP_HOOKS_SCRIPT], {
241
+ stdio: 'inherit',
242
+ env: deps.env || process.env,
243
+ });
244
+ } catch (err) {
245
+ // Hook setup is for devs only and must never fail the install.
246
+ console.warn(`[amicus] Warning: could not configure git hooks: ${err && err.message}`);
247
+ }
248
+ }
249
+
175
250
  function main(deps = {}) {
176
251
  if (process.env.AMICUS_SKIP_POSTINSTALL === '1') {
177
252
  console.log('[amicus] AMICUS_SKIP_POSTINSTALL set — skipping global setup (plugin channel handles registration).');
@@ -181,22 +256,47 @@ function main(deps = {}) {
181
256
  const _installCouncilSkill = deps.installCouncilSkill || installCouncilSkill;
182
257
  const _registerClaudeCode = deps.registerClaudeCode || registerClaudeCode;
183
258
  const _registerClaudeDesktop = deps.registerClaudeDesktop || registerClaudeDesktop;
259
+ const _setupHooks = deps.setupHooks || setupHooks;
184
260
 
185
261
  console.log('[amicus] Installing...');
262
+ // Dev-only: configure git hooks (no-op for consumers). Folded in from the
263
+ // removed "prepare" lifecycle (#35) so github: installs run identically.
264
+ _setupHooks();
186
265
  _installSkill();
187
266
  _installCouncilSkill();
188
267
  _registerClaudeCode();
189
268
  _registerClaudeDesktop();
190
269
 
270
+ // Non-fatal: warn (only) if the optional Electron binary failed to extract.
271
+ verifyElectron(deps);
272
+
191
273
  console.log('');
192
274
  console.log('[amicus] Setup:');
193
275
  console.log(' - Configure API: Run `amicus setup` or set API keys directly');
194
276
  console.log(' - API keys: OPENROUTER_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY, OPENAI_API_KEY, etc.');
195
277
  }
196
278
 
197
- // Only run main when executed directly (not when required for testing)
279
+ /**
280
+ * Top-level entry point. Wraps main() so a failure is never fatal: npm treats
281
+ * a non-zero postinstall as a reason to roll back / uninstall the ENTIRE global
282
+ * package, but skill-copy + MCP registration are optional — amicus itself still
283
+ * works without them. Warn clearly and exit 0 (mirrors scripts/setup-hooks.js).
284
+ */
285
+ function runCli(deps = {}) {
286
+ try {
287
+ main(deps);
288
+ } catch (err) {
289
+ console.warn(`[amicus] Warning: optional post-install setup failed: ${err && err.message}`);
290
+ console.warn('[amicus] Skill install + MCP registration are optional — amicus itself still works.');
291
+ console.warn('[amicus] Run `amicus doctor` to check setup, or re-register manually later.');
292
+ }
293
+ // Always exit 0 so a failure here never rolls back the global install.
294
+ process.exit(0);
295
+ }
296
+
297
+ // Only run when executed directly (not when required for testing)
198
298
  if (require.main === module) {
199
- main();
299
+ runCli();
200
300
  }
201
301
 
202
- module.exports = { main, addMcpToConfigFile, installSkill, installCouncilSkill, COUNCIL_FILES };
302
+ module.exports = { main, runCli, addMcpToConfigFile, installSkill, installCouncilSkill, setupHooks, verifyElectron, resolveElectron, COUNCIL_FILES };
@@ -12,10 +12,14 @@
12
12
  * core.hooksPath at it makes hooks fire in the main clone and in every
13
13
  * worktree with no per-worktree setup.
14
14
  *
15
- * Runs automatically via npm's "prepare" lifecycle. If you install with
16
- * --ignore-scripts (recommended for this repo), run it once by hand:
15
+ * Runs automatically via the postinstall flow (scripts/postinstall.js) so a
16
+ * fresh dev `npm install` still wires hooks. It is NOT a "prepare" script:
17
+ * prepare also fires on the github: install path, where it triggers a nested
18
+ * devDependency install + re-pack that the registry path skips (#35) — the
19
+ * source of the Windows github: rollback. If you install with --ignore-scripts
20
+ * (recommended for this repo), run it once by hand:
17
21
  *
18
- * node scripts/setup-hooks.js
22
+ * npm run setup-hooks # or: node scripts/setup-hooks.js
19
23
  *
20
24
  * Safe to run anywhere: exits 0 outside a git checkout (npm tarball
21
25
  * installs, exported archives) and never fails the install.
@@ -105,7 +105,7 @@ function runDoctorChecks(depsOverride = {}) {
105
105
  checks.push(guard('opencode-bin', 'OpenCode binary', () => (
106
106
  d.hasOpencodeBinary()
107
107
  ? { id: 'opencode-bin', name: 'OpenCode binary', status: 'ok', message: 'found', hint: null }
108
- : { id: 'opencode-bin', name: 'OpenCode binary', status: 'error', message: 'not found', hint: 'npm install -g amicus' }
108
+ : { id: 'opencode-bin', name: 'OpenCode binary', status: 'error', message: 'not found', hint: 'npm install -g amicus (a transient install error can roll back the engine binaries — re-run, or: npm cache clean --force && npm install -g amicus)' }
109
109
  )));
110
110
 
111
111
  checks.push(guard('electron', 'Electron (interactive GUI)', () => (
package/src/cli.js CHANGED
@@ -292,10 +292,15 @@ function isValidDurationFormat(duration) {
292
292
  }
293
293
 
294
294
  /**
295
- * Get usage text
295
+ * Usage text, split into composable parts so 'amicus <cmd> --help' can print
296
+ * only the relevant block while bare 'amicus --help' still composes the full
297
+ * text byte-identically.
298
+ *
299
+ * Each value is a section of the original monolithic template; concatenating
300
+ * USAGE_HEADER + every USAGE_COMMAND_BLOCKS value (in insertion order) +
301
+ * USAGE_TRAILER reproduces the original string exactly.
296
302
  */
297
- function getUsage() {
298
- return `
303
+ const USAGE_HEADER = `
299
304
  Usage: amicus <command> [options]
300
305
 
301
306
  Commands:
@@ -320,7 +325,12 @@ Commands:
320
325
  (no args) List all configured providers
321
326
  update Update to latest version
322
327
  mcp Start MCP server (stdio transport)
328
+ `;
323
329
 
330
+ // Per-command option blocks, keyed by the invoked subcommand. Insertion order
331
+ // must match the original template so the composed full usage is unchanged.
332
+ const USAGE_COMMAND_BLOCKS = {
333
+ start: `
324
334
  Options for 'start':
325
335
  --model <model> Optional (uses config default). Model to use:
326
336
  - Short aliases: gemini, opus, gpt (see 'amicus setup')
@@ -354,7 +364,8 @@ Options for 'start':
354
364
  --validate-model (Deprecated: validation is on by default)
355
365
  --no-validate-model Skip model-catalog validation before launch
356
366
  --position <pos> Window position: right (default), left, center
357
-
367
+ `,
368
+ fanout: `
358
369
  Options for 'fanout':
359
370
  --models <a,b,c> Required. Comma-separated aliases or provider/model IDs
360
371
  --council <name> Run a saved council instead of --models (e.g. free). Mutually exclusive with --models
@@ -369,27 +380,84 @@ Options for 'fanout':
369
380
  Shared per-leg knobs: --agent, --thinking, --timeout, --summary-length,
370
381
  --no-context, --context-*, --mcp*, --no-validate-model, --cwd
371
382
  Exit codes: 0 all legs complete, 2 partial, 1 none complete / hard failure
372
-
383
+ `,
384
+ models: `
373
385
  Options for 'models':
374
386
  --search <q> Filter by substring over model id and name
375
387
  --refresh Force-refresh the catalog from provider APIs
376
388
  --check Audit aliases against the catalog (exit = stale count)
377
389
  --json Machine-readable output
378
-
390
+ `,
391
+ list: `
379
392
  Options for 'list':
380
393
  --status <filter> Filter by status (running, complete)
381
394
  --all Show all projects
382
395
  --json Output as JSON
383
-
396
+ `,
397
+ abort: `
384
398
  Options for 'abort':
385
399
  --all Abort all running sessions in this project
386
-
400
+ `,
401
+ read: `
387
402
  Options for 'read':
388
403
  --summary Show summary (default)
389
404
  --conversation Show full conversation
390
405
  --metadata Show session metadata
391
406
  --json Emit the run/wave result as stable JSON
407
+ `,
408
+ continue: `
409
+ Options for 'continue':
410
+ <task_id> Required. Session to build on (positional)
411
+ --prompt <text> Required. Briefing for the new session
412
+ --model <model> Optional. Override the model (alias or provider/model)
413
+ --cwd <path> Project directory (default: cwd)
414
+ --no-ui Run without GUI (autonomous mode)
415
+ --timeout <minutes> Headless timeout (default: 15)
416
+ --context-turns <N> Max conversation turns (default: 50)
417
+ --context-max-tokens <N> Max context tokens (default: 80000)
418
+ `,
419
+ resume: `
420
+ Options for 'resume':
421
+ <task_id> Required. Session to reopen (positional)
422
+ --cwd <path> Project directory (default: cwd)
423
+ --no-ui Run without GUI (autonomous mode)
424
+ --timeout <minutes> Headless timeout (default: 15)
425
+ `,
426
+ council: `
427
+ Subcommands for 'council':
428
+ tally <input.json> Tally findings → tiers/street-cred (appends to ledger)
429
+ --no-ledger Compute the record without appending to the ledger
430
+ --json Machine-readable output
431
+ stats Reviewer-reliability aggregates from the ledger
432
+ --json Machine-readable output
433
+ report <verdict.json> Disagreement + verdict report
434
+ --wave <wave.json> Include per-leg run stats from a wave file
435
+ --md Emit Markdown (default)
436
+ --html Emit a self-contained HTML page
437
+ `,
438
+ doctor: `
439
+ Options for 'doctor':
440
+ --json Machine-readable output
441
+ `,
442
+ setup: `
443
+ Options for 'setup':
444
+ (no args) Run the interactive setup wizard
445
+ --api-keys Open the API key setup window
446
+ --add-alias <name=model> Add a model alias without the full wizard
447
+ `,
448
+ key: `
449
+ Usage for 'key':
450
+ key <provider> <apikey> Validate and save a key
451
+ key <provider> --remove Remove a saved key
452
+ key List all configured providers
453
+ `,
454
+ mcp: `
455
+ Usage for 'mcp':
456
+ mcp Start the MCP server (stdio transport)
457
+ `
458
+ };
392
459
 
460
+ const USAGE_TRAILER = `
393
461
  OpenCode Agent Types:
394
462
  Chat Reads auto, writes/bash ask permission (interactive default)
395
463
  Build Full tool access (headless default)
@@ -409,6 +477,25 @@ Examples:
409
477
  amicus resume abc123
410
478
  amicus read abc123 --conversation
411
479
  `;
480
+
481
+ /**
482
+ * Get usage text.
483
+ *
484
+ * @param {string} [command] When provided AND it has a dedicated options block,
485
+ * returns the top-level header plus only that command's block (scoped --help).
486
+ * When omitted, or when the command has no dedicated block, returns the full
487
+ * composed usage (byte-identical to the original monolithic string).
488
+ */
489
+ function getUsage(command) {
490
+ // Each part carries its own leading/trailing newlines exactly as they sat in
491
+ // the original single template literal, so plain concatenation reproduces the
492
+ // blank-line separators (the header's trailing "\n" + a block's leading "\n"
493
+ // form the blank line between them). Scoped help keeps the header + trailer
494
+ // and substitutes just the one command's block in place of all blocks.
495
+ const block = command && Object.prototype.hasOwnProperty.call(USAGE_COMMAND_BLOCKS, command)
496
+ ? USAGE_COMMAND_BLOCKS[command]
497
+ : Object.values(USAGE_COMMAND_BLOCKS).join('');
498
+ return USAGE_HEADER + block + USAGE_TRAILER;
412
499
  }
413
500
 
414
501
  module.exports = {