amicus 1.7.6 → 1.8.0
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.
- package/.claude-plugin/plugin.json +27 -6
- package/CHANGELOG.md +65 -0
- package/LICENSE +2 -22
- package/README.md +38 -5
- package/bin/amicus.js +9 -4
- package/package.json +1 -1
- package/scripts/postinstall.js +72 -23
- package/src/cli-handlers-doctor.js +43 -0
- package/src/cli-handlers-status.js +76 -0
- package/src/cli-handlers.js +32 -0
- package/src/cli.js +8 -0
- package/src/mcp-server.js +124 -21
- package/src/mcp-tools.js +28 -1
- package/src/mcp-wait.js +163 -0
- package/src/sidecar/continue.js +24 -6
- package/src/sidecar/conversation-mirror.js +17 -4
- package/src/sidecar/interactive-abort.js +112 -0
- package/src/sidecar/interactive.js +32 -2
- package/src/sidecar/progress-fields.js +60 -0
- package/src/sidecar/progress.js +52 -48
- package/src/sidecar/resume.js +23 -7
- package/src/sidecar/start.js +6 -7
- package/src/utils/abort-coordinator.js +91 -0
- package/src/utils/error-doc.js +3 -0
- package/src/utils/legacy-mcp-migration.js +119 -0
- package/src/utils/lifecycle.js +1 -1
- package/src/utils/mcp-discovery.js +6 -9
- package/src/utils/mcp-self-identity.js +69 -0
- package/src/utils/remediation-hints.js +8 -0
- package/src/utils/shared-server.js +50 -18
|
@@ -1,19 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amicus",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
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
|
-
"author": {
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Christian Wagner"
|
|
7
|
+
},
|
|
6
8
|
"homepage": "https://bourbondog.github.io/amicus/",
|
|
7
9
|
"repository": "https://github.com/BourbonDog/amicus",
|
|
8
10
|
"bugs": "https://github.com/BourbonDog/amicus/issues",
|
|
9
11
|
"license": "MIT",
|
|
10
|
-
"keywords": [
|
|
11
|
-
|
|
12
|
+
"keywords": [
|
|
13
|
+
"claude-code",
|
|
14
|
+
"multi-model",
|
|
15
|
+
"llm",
|
|
16
|
+
"council",
|
|
17
|
+
"second-opinion",
|
|
18
|
+
"sidecar",
|
|
19
|
+
"gemini",
|
|
20
|
+
"gpt",
|
|
21
|
+
"deepseek"
|
|
22
|
+
],
|
|
23
|
+
"skills": [
|
|
24
|
+
"./skills/sidecar",
|
|
25
|
+
"./skills/second-opinion"
|
|
26
|
+
],
|
|
12
27
|
"mcpServers": {
|
|
13
28
|
"amicus": {
|
|
14
29
|
"command": "npx",
|
|
15
|
-
"args": [
|
|
16
|
-
|
|
30
|
+
"args": [
|
|
31
|
+
"-y",
|
|
32
|
+
"amicus@latest",
|
|
33
|
+
"mcp"
|
|
34
|
+
],
|
|
35
|
+
"env": {
|
|
36
|
+
"AMICUS_SKIP_POSTINSTALL": "1"
|
|
37
|
+
}
|
|
17
38
|
}
|
|
18
39
|
}
|
|
19
40
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,71 @@ All notable changes to Amicus are documented here. Format follows
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.8.0] - 2026-07-02
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **`amicus_wait` MCP tool: blocking wait for a session or fan-out wave.** Blocks inside one tool call until the
|
|
12
|
+
target reaches a terminal state or the wait window closes, replacing the sleep+`amicus_status` polling loop with
|
|
13
|
+
a single call. Returns the same JSON shape as `amicus_status` plus `waitedMs` and `{timedOut: true}` (with a
|
|
14
|
+
`hint`) on expiry — re-call it while it keeps returning `timedOut: true`. Works for sessions or waves started by
|
|
15
|
+
other processes, not just the caller. Torn-read tolerant: a transient read of `metadata.json` mid-write is
|
|
16
|
+
treated as a missed poll tick, not a hard failure. Legacy alias `sidecar_wait` is available under
|
|
17
|
+
`AMICUS_LEGACY_ALIASES=1`.
|
|
18
|
+
- **Agent-visible progress.** A new `amicus status <task_id>` (or `--wave <id>`) one-shot CLI command delegates
|
|
19
|
+
directly to the MCP status handler — same crash detection and wave-leg rollup, zero duplicated logic.
|
|
20
|
+
`amicus_status` and `amicus_list` are enriched with agent-facing `mode`, `phase`, `messageCount`,
|
|
21
|
+
`lastActivityAt`, and `latestPreview` (the pinned raw `stage` field is unchanged for back-compat; wave legs
|
|
22
|
+
additionally surface the raw `stage` alongside the coarse `phase`). Interactive (Electron GUI) runs now write
|
|
23
|
+
the same lifecycle progress stages headless runs always have (`initializing`, `server_ready`, `session_created`,
|
|
24
|
+
`prompt_sent`), and long-thinking turns emit periodic thinking-delta progress ticks instead of at most one ever
|
|
25
|
+
— so a live GUI run no longer reads "Starting up... | 0 messages" forever.
|
|
26
|
+
- **`amicus doctor` duplicate-registration check.** A new `mcp-legacy` check flags plugin-channel installs
|
|
27
|
+
(`AMICUS_SKIP_POSTINSTALL=1`) that never ran the postinstall migration and still carry a duplicate legacy
|
|
28
|
+
`sidecar` MCP registration; `doctor --fix` cleans it up.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- **`amicus abort` now actually stops interactive sessions and wave legs.** Marker-first, honest output — reports
|
|
32
|
+
what really happened including the unkillable-pid case — and no-ops cleanly with a clear message when the
|
|
33
|
+
target isn't running.
|
|
34
|
+
- **Legacy-MCP remediation's `claude mcp add-json` (CLI) path no longer drops a user's custom `env`** on
|
|
35
|
+
re-registration — it now merges the previous registration's `env` the same way the file-fallback path already did.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
- **Legacy `sidecar_*` MCP tool aliases are now opt-in** via `AMICUS_LEGACY_ALIASES=1` (breaking-adjacent —
|
|
39
|
+
carrying release must be a MINOR, v1.8.0). The default client-visible surface is the `amicus_*` toolset (14
|
|
40
|
+
tools as of this release); saved allowlists that still reference `mcp__amicus__sidecar_*` stop resolving unless
|
|
41
|
+
you opt back in.
|
|
42
|
+
- **Postinstall no longer registers a separate `sidecar` MCP server** and auto-removes a verified-identical
|
|
43
|
+
duplicate left over from pre-1.8 installs. A customized `sidecar` entry or a sole `sidecar` registration (no
|
|
44
|
+
`amicus` twin) is never touched.
|
|
45
|
+
|
|
46
|
+
## [1.7.7] - 2026-07-01
|
|
47
|
+
|
|
48
|
+
Correctness patch from the 2026-07-01 full product review (multi-agent review, every finding adversarially
|
|
49
|
+
verified against source), executed subagent-driven with per-task adversarial review plus a final whole-branch review.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
- **Terminal errors now show their actionable hint.** Human-mode errors printed only the message while `--json`
|
|
53
|
+
carried a `hint` field; the hint now prints on a second ` → …` line. Budget-gate refusals finally tell you the
|
|
54
|
+
offending model, the threshold, and the `--max-cost` / `--no-cost-gate` overrides.
|
|
55
|
+
- **Spawned sidecars no longer inherit Amicus's own MCP server.** The recursive-spawn guard only excluded a server
|
|
56
|
+
literally named `sidecar`, but the product registers as `amicus` — so every child model inherited the full
|
|
57
|
+
Amicus toolset and could spawn recursively. Children now exclude any inherited entry that *is* Amicus, matched
|
|
58
|
+
by name **or** by what the command actually runs (`amicus mcp`, `npx … amicus … mcp`, a `bin/amicus.js … mcp`
|
|
59
|
+
path). Note: this strip has no opt-out — a deliberately configured nested Amicus MCP entry is also removed from
|
|
60
|
+
spawned children.
|
|
61
|
+
- **Shared-server crash detection actually works.** The crash/restart machinery listened on an event emitter the
|
|
62
|
+
real server handle never exposed, so it was dead code — a dead engine silently degraded every later session.
|
|
63
|
+
A pid liveness poll now drives detection and restart, and shutting down during the restart backoff cancels the
|
|
64
|
+
pending restart instead of spawning a server nobody asked for.
|
|
65
|
+
|
|
66
|
+
### Changed
|
|
67
|
+
- **`amicus continue` and `amicus resume` now report failures truthfully** (behavior change): error exits 1,
|
|
68
|
+
timeout exits 2, abort exits 130/143/2 — previously both always exited 0 and recorded the session as
|
|
69
|
+
`complete` even when the model errored or timed out. The session record now finalizes `error`/`timed-out`
|
|
70
|
+
accordingly (interactive sessions that legitimately end with an empty summary still finalize `complete`).
|
|
71
|
+
Scripts that gated on exit code 0 for these verbs will now see real failures.
|
|
72
|
+
|
|
8
73
|
## [1.7.6] - 2026-07-01
|
|
9
74
|
|
|
10
75
|
A second independent review (GLM 5.2), adversarially verified against source, then fixed across 11 lanes.
|
package/LICENSE
CHANGED
|
@@ -1,27 +1,7 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
Copyright (c) 2026 Christian Wagner Amicus
|
|
3
|
+
Copyright (c) 2026 Christian Wagner
|
|
4
|
+
Copyright (c) 2025 John Renaldi
|
|
25
5
|
|
|
26
6
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
27
7
|
of this software and associated documentation files (the "Software"), to deal
|
package/README.md
CHANGED
|
@@ -57,13 +57,37 @@ Claude is the orchestrator. The council and chat skills run *on top of* the engi
|
|
|
57
57
|
|
|
58
58
|
## Quick start
|
|
59
59
|
|
|
60
|
-
**Install
|
|
60
|
+
**Install** — pick whichever fits; all deliver the same CLI, MCP server, and both skills:
|
|
61
|
+
|
|
62
|
+
**As a Claude Code plugin** — the most native path if you use Claude Code:
|
|
63
|
+
|
|
64
|
+
```text
|
|
65
|
+
/plugin marketplace add BourbonDog/amicus
|
|
66
|
+
/plugin install amicus@bourbondog-amicus
|
|
67
|
+
/reload-plugins
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Claude Code registers the MCP server and both skills for you — nothing to configure. (The standalone Electron window is npm-only, and the first council/sidecar call downloads the OpenCode engine.)
|
|
71
|
+
|
|
72
|
+
**With the install script** — macOS, Linux, or Windows (needs [Node.js](https://nodejs.org) ≥ 18):
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# macOS / Linux
|
|
76
|
+
curl -fsSL https://raw.githubusercontent.com/BourbonDog/amicus/main/install.sh | sh
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```powershell
|
|
80
|
+
# Windows (PowerShell)
|
|
81
|
+
irm https://raw.githubusercontent.com/BourbonDog/amicus/main/install.ps1 | iex
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**With npm** — the canonical path (needs [Node.js](https://nodejs.org) ≥ 18):
|
|
61
85
|
|
|
62
86
|
```bash
|
|
63
87
|
npm install -g amicus
|
|
64
88
|
```
|
|
65
89
|
|
|
66
|
-
|
|
90
|
+
For the **npm** and **install-script** paths, a postinstall auto-configures everything — no manual registration:
|
|
67
91
|
|
|
68
92
|
- Registers the **MCP server** in Claude Code and in Claude Desktop / Cowork, so the Amicus tools appear natively.
|
|
69
93
|
- Installs **both skills** into `~/.claude/skills/` — `second-opinion` (the council) and `sidecar` (the chat skill).
|
|
@@ -235,6 +259,7 @@ amicus update
|
|
|
235
259
|
| `amicus resume` | Reopen a previous session with full history. |
|
|
236
260
|
| `amicus continue` | Start a new session building on a previous one. |
|
|
237
261
|
| `amicus read` | Output a session's summary / conversation / metadata. |
|
|
262
|
+
| `amicus status <id>` | One-shot status for a session or fan-out wave (human or `--json`; `--wave <id>` alternative spelling). |
|
|
238
263
|
| `amicus models` | List, search, refresh the catalog, or audit aliases. |
|
|
239
264
|
| `amicus abort` | Abort a running session (or `--all`). |
|
|
240
265
|
| `amicus setup` | Configure default model, API keys, and aliases. |
|
|
@@ -305,6 +330,10 @@ amicus read <id> --conversation # full conversation
|
|
|
305
330
|
amicus read <id> --metadata # session metadata
|
|
306
331
|
amicus read <id> --json # stable JSON (run or wave document)
|
|
307
332
|
|
|
333
|
+
amicus status <id> # one-shot status for a session or wave
|
|
334
|
+
amicus status --wave <id> # alternative spelling for a wave ID
|
|
335
|
+
amicus status <id> --json # machine-readable output
|
|
336
|
+
|
|
308
337
|
amicus resume <id> # reopen with full history
|
|
309
338
|
amicus continue <id> --prompt "..." # new session, previous one as read-only context
|
|
310
339
|
|
|
@@ -347,12 +376,13 @@ amicus models --check # audit your aliases against the catalog
|
|
|
347
376
|
|
|
348
377
|
## MCP integration
|
|
349
378
|
|
|
350
|
-
The MCP server is auto-registered on install (Claude Code and Claude Desktop / Cowork). It exposes
|
|
379
|
+
The MCP server is auto-registered on install (Claude Code and Claude Desktop / Cowork). It exposes fourteen tools:
|
|
351
380
|
|
|
352
381
|
| Tool | What it does |
|
|
353
382
|
|------|--------------|
|
|
354
383
|
| `amicus_start` | Spawn a session; returns a task ID immediately. |
|
|
355
384
|
| `amicus_status` | Poll a task (or a fanout wave) for completion. |
|
|
385
|
+
| `amicus_wait` | Block inside one tool call until a session/wave finishes or the wait window closes. |
|
|
356
386
|
| `amicus_read` | Read results: summary, conversation, metadata, or JSON. |
|
|
357
387
|
| `amicus_list` | List past sessions. |
|
|
358
388
|
| `amicus_resume` | Reopen a session. |
|
|
@@ -361,8 +391,11 @@ The MCP server is auto-registered on install (Claude Code and Claude Desktop / C
|
|
|
361
391
|
| `amicus_setup` | Open the setup wizard. |
|
|
362
392
|
| `amicus_guide` | Return usage guidance (model choice, briefings, polling). |
|
|
363
393
|
| `amicus_fanout` | Launch a same-prompt wave; returns `{ waveId, taskIds[] }`. |
|
|
394
|
+
| `amicus_council_tally` | Aggregate a council wave's reviews into a scored tally. |
|
|
395
|
+
| `amicus_council_stats` | Reviewer-reliability stats from past council runs. |
|
|
396
|
+
| `amicus_verdict` | Build the final council verdict from a tally + decisions. |
|
|
364
397
|
|
|
365
|
-
The async pattern is **start → status → read**: `amicus_start` (or `amicus_fanout`) returns immediately, you poll `amicus_status`, then `amicus_read` once it's done — so the calling agent never blocks.
|
|
398
|
+
The async pattern is **start → status → read**: `amicus_start` (or `amicus_fanout`) returns immediately, you poll `amicus_status`, then `amicus_read` once it's done — so the calling agent never blocks. Prefer `amicus_wait` over manual sleep+status polling for headless runs: it collapses the poll loop into a single blocking call that returns as soon as the run finishes (or the wait window closes).
|
|
366
399
|
|
|
367
400
|
To register manually (user scope):
|
|
368
401
|
|
|
@@ -370,7 +403,7 @@ To register manually (user scope):
|
|
|
370
403
|
claude mcp add-json amicus '{"command":"npx","args":["-y","amicus@latest","mcp"]}' --scope user
|
|
371
404
|
```
|
|
372
405
|
|
|
373
|
-
> Legacy `sidecar_*` tool names are
|
|
406
|
+
> Legacy `sidecar_*` tool names are no longer registered by default (v1.8.0). To restore them, add `"env": {"AMICUS_LEGACY_ALIASES": "1"}` to the server entry. They will be removed entirely in the next major.
|
|
374
407
|
|
|
375
408
|
---
|
|
376
409
|
|
package/bin/amicus.js
CHANGED
|
@@ -94,11 +94,16 @@ async function main() {
|
|
|
94
94
|
case 'list':
|
|
95
95
|
await handleList(args);
|
|
96
96
|
break;
|
|
97
|
+
case 'status': {
|
|
98
|
+
const { handleStatus } = require('../src/cli-handlers-status');
|
|
99
|
+
exitCode = await handleStatus(args);
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
97
102
|
case 'resume':
|
|
98
|
-
await handleResume(args);
|
|
103
|
+
exitCode = await handleResume(args);
|
|
99
104
|
break;
|
|
100
105
|
case 'continue':
|
|
101
|
-
await handleContinue(args);
|
|
106
|
+
exitCode = await handleContinue(args);
|
|
102
107
|
break;
|
|
103
108
|
case 'read':
|
|
104
109
|
await handleRead(args);
|
|
@@ -188,7 +193,7 @@ async function handleResume(args) {
|
|
|
188
193
|
|
|
189
194
|
const { resumeSidecar } = require('../src/index');
|
|
190
195
|
|
|
191
|
-
await resumeSidecar({
|
|
196
|
+
return await resumeSidecar({
|
|
192
197
|
taskId,
|
|
193
198
|
project: args.cwd,
|
|
194
199
|
headless: args['no-ui'],
|
|
@@ -242,7 +247,7 @@ async function handleContinue(args) {
|
|
|
242
247
|
|
|
243
248
|
const { continueSidecar } = require('../src/index');
|
|
244
249
|
|
|
245
|
-
await continueSidecar({
|
|
250
|
+
return await continueSidecar({
|
|
246
251
|
taskId,
|
|
247
252
|
newTaskId: args['task-id'],
|
|
248
253
|
briefing: args.prompt || args.briefing,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amicus",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
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",
|
package/scripts/postinstall.js
CHANGED
|
@@ -15,6 +15,7 @@ const { execFileSync } = require('child_process');
|
|
|
15
15
|
|
|
16
16
|
const { repairElectron } = require('../src/sidecar/electron-install');
|
|
17
17
|
const HINTS = require('../src/utils/remediation-hints');
|
|
18
|
+
const { isAmicusMcpConfig } = require('../src/utils/mcp-self-identity');
|
|
18
19
|
|
|
19
20
|
const SETUP_HOOKS_SCRIPT = path.join(__dirname, 'setup-hooks.js');
|
|
20
21
|
|
|
@@ -41,7 +42,18 @@ const MCP_CONFIG = { command: 'npx', args: ['-y', 'amicus@latest', 'mcp'] };
|
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
44
|
* Add or update an MCP server in a JSON config file.
|
|
44
|
-
*
|
|
45
|
+
*
|
|
46
|
+
* Refreshes command/args on every install/upgrade. If the EXISTING entry at
|
|
47
|
+
* this key is already amicus-shaped (per isAmicusMcpConfig — Phase 1's single
|
|
48
|
+
* source of truth for "this is amicus"), we MERGE instead of overwrite: the
|
|
49
|
+
* user's `env` (and any other extra keys, e.g. a future `cwd`) survive the
|
|
50
|
+
* refresh. Without this, `npm i -g amicus` silently wiped
|
|
51
|
+
* "env": {"AMICUS_LEGACY_ALIASES":"1"} — the exact opt-in escape hatch Phase 4
|
|
52
|
+
* tells users to add — on every upgrade.
|
|
53
|
+
*
|
|
54
|
+
* A NON-amicus-shaped entry at this key is overwritten as before: 'amicus' is
|
|
55
|
+
* a reserved registration name, so a foreign entry there is reclaimed rather
|
|
56
|
+
* than merged with.
|
|
45
57
|
*
|
|
46
58
|
* @param {string} configPath - Path to the JSON config file
|
|
47
59
|
* @param {string} name - MCP server name
|
|
@@ -59,9 +71,10 @@ function addMcpToConfigFile(configPath, name, config) {
|
|
|
59
71
|
if (!existing.mcpServers) { existing.mcpServers = {}; }
|
|
60
72
|
|
|
61
73
|
const prev = existing.mcpServers[name];
|
|
62
|
-
const
|
|
74
|
+
const nextConfig = (prev && isAmicusMcpConfig(prev)) ? { ...prev, ...config } : config;
|
|
75
|
+
const status = !prev ? 'added' : JSON.stringify(prev) !== JSON.stringify(nextConfig) ? 'updated' : 'unchanged';
|
|
63
76
|
|
|
64
|
-
existing.mcpServers[name] =
|
|
77
|
+
existing.mcpServers[name] = nextConfig;
|
|
65
78
|
if (status !== 'unchanged') {
|
|
66
79
|
const dir = path.dirname(configPath);
|
|
67
80
|
if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); }
|
|
@@ -111,28 +124,42 @@ function installCouncilSkill(sourceDir = COUNCIL_SOURCE_DIR) {
|
|
|
111
124
|
}
|
|
112
125
|
}
|
|
113
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Read the previous 'amicus' entry from ~/.claude.json, if any — used by the
|
|
129
|
+
* CLI add-json path to merge env the same way the file-fallback path does.
|
|
130
|
+
* Never throws: a missing/unreadable file just means "no previous entry".
|
|
131
|
+
* @returns {object|undefined}
|
|
132
|
+
*/
|
|
133
|
+
function readPrevClaudeCodeAmicusEntry() {
|
|
134
|
+
try {
|
|
135
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf-8'));
|
|
136
|
+
return parsed && parsed.mcpServers ? parsed.mcpServers.amicus : undefined;
|
|
137
|
+
} catch {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
114
142
|
/** Register MCP server in Claude Code config */
|
|
115
143
|
function registerClaudeCode() {
|
|
116
144
|
// Try the CLI first
|
|
117
145
|
try {
|
|
118
|
-
|
|
146
|
+
// Merge the PREVIOUS registration's env into the add-json payload — same
|
|
147
|
+
// merge semantics as addMcpToConfigFile's file-fallback path (`{ ...prev,
|
|
148
|
+
// ...config }`: prev env keys survive, canonical MCP_CONFIG keys win on
|
|
149
|
+
// collision). Without this, a user's custom env (API key, AMICUS_* tuning
|
|
150
|
+
// knobs) on the old registration was silently dropped whenever the
|
|
151
|
+
// `claude` CLI was present, because the CLI path built its JSON payload
|
|
152
|
+
// from the bare MCP_CONFIG and delegated overwrite semantics to the
|
|
153
|
+
// claude binary — which has no idea about the user's previous entry.
|
|
154
|
+
const prev = readPrevClaudeCodeAmicusEntry();
|
|
155
|
+
const nextConfig = (prev && isAmicusMcpConfig(prev)) ? { ...prev, ...MCP_CONFIG } : MCP_CONFIG;
|
|
156
|
+
const mcpJson = JSON.stringify(nextConfig);
|
|
119
157
|
execFileSync('claude', ['mcp', 'add-json', 'amicus', mcpJson, '--scope', 'user'], {
|
|
120
158
|
stdio: 'pipe',
|
|
121
159
|
timeout: 10000,
|
|
122
160
|
});
|
|
123
161
|
console.log('[amicus] MCP registered in Claude Code (via CLI).');
|
|
124
162
|
|
|
125
|
-
// DEPRECATED(amicus-shim): also register 'sidecar' so existing clients that
|
|
126
|
-
// reference the old server name keep resolving. Remove in next major.
|
|
127
|
-
try {
|
|
128
|
-
execFileSync('claude', ['mcp', 'add-json', 'sidecar', mcpJson, '--scope', 'user'], {
|
|
129
|
-
stdio: 'pipe',
|
|
130
|
-
timeout: 10000,
|
|
131
|
-
});
|
|
132
|
-
} catch {
|
|
133
|
-
// Best-effort; ignore failures for the shim registration
|
|
134
|
-
}
|
|
135
|
-
|
|
136
163
|
return;
|
|
137
164
|
} catch {
|
|
138
165
|
// CLI not available or failed — fall back to file edit
|
|
@@ -148,10 +175,6 @@ function registerClaudeCode() {
|
|
|
148
175
|
} else {
|
|
149
176
|
console.log('[amicus] MCP already registered in Claude Code.');
|
|
150
177
|
}
|
|
151
|
-
|
|
152
|
-
// DEPRECATED(amicus-shim): also register 'sidecar' entry so existing clients
|
|
153
|
-
// that reference the old server name keep resolving. Remove in next major.
|
|
154
|
-
addMcpToConfigFile(claudeConfigPath, 'sidecar', MCP_CONFIG);
|
|
155
178
|
}
|
|
156
179
|
|
|
157
180
|
/** Register MCP server in Claude Desktop / Cowork config */
|
|
@@ -174,10 +197,33 @@ function registerClaudeDesktop() {
|
|
|
174
197
|
} else {
|
|
175
198
|
console.log('[amicus] MCP already registered in Claude Desktop.');
|
|
176
199
|
}
|
|
200
|
+
}
|
|
177
201
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
202
|
+
/**
|
|
203
|
+
* One-shot migration: drop the duplicate legacy 'sidecar' MCP entry that
|
|
204
|
+
* pre-1.8 postinstalls registered alongside 'amicus' (same server twice —
|
|
205
|
+
* doubled the client-visible tool list). Only removes an entry whose command
|
|
206
|
+
* is an amicus MCP invocation; a customized 'sidecar' entry is left alone.
|
|
207
|
+
* Covers both files the three legacy registration paths wrote to:
|
|
208
|
+
* ~/.claude.json (CLI + file fallback) and claude_desktop_config.json.
|
|
209
|
+
* Never throws (postinstall must always exit 0).
|
|
210
|
+
*/
|
|
211
|
+
function migrateLegacyMcp(deps = {}) {
|
|
212
|
+
try {
|
|
213
|
+
const impl = deps.migrateLegacySidecar
|
|
214
|
+
|| require('../src/utils/legacy-mcp-migration').migrateLegacySidecar;
|
|
215
|
+
for (const r of impl()) {
|
|
216
|
+
if (r.result === 'removed') {
|
|
217
|
+
console.log(`[amicus] Removed duplicate legacy 'sidecar' MCP entry from ${r.target} (same server — kept as 'amicus').`);
|
|
218
|
+
} else if (r.result === 'customized') {
|
|
219
|
+
console.log(`[amicus] Kept custom 'sidecar' MCP entry in ${r.target} (does not point at amicus).`);
|
|
220
|
+
} else if (r.result === 'write-failed') {
|
|
221
|
+
console.warn(`[amicus] Warning: could not remove the legacy 'sidecar' MCP entry from ${r.target} — run: amicus doctor --fix`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
} catch (err) {
|
|
225
|
+
console.warn(`[amicus] Warning: legacy MCP cleanup skipped: ${err && err.message}`);
|
|
226
|
+
}
|
|
181
227
|
}
|
|
182
228
|
|
|
183
229
|
/**
|
|
@@ -291,6 +337,7 @@ async function main(deps = {}) {
|
|
|
291
337
|
const _installCouncilSkill = deps.installCouncilSkill || installCouncilSkill;
|
|
292
338
|
const _registerClaudeCode = deps.registerClaudeCode || registerClaudeCode;
|
|
293
339
|
const _registerClaudeDesktop = deps.registerClaudeDesktop || registerClaudeDesktop;
|
|
340
|
+
const _migrateLegacyMcp = deps.migrateLegacyMcp || migrateLegacyMcp;
|
|
294
341
|
const _setupHooks = deps.setupHooks || setupHooks;
|
|
295
342
|
const _provisionElectron = deps.provisionElectron || provisionElectron;
|
|
296
343
|
|
|
@@ -302,6 +349,7 @@ async function main(deps = {}) {
|
|
|
302
349
|
_installCouncilSkill();
|
|
303
350
|
_registerClaudeCode();
|
|
304
351
|
_registerClaudeDesktop();
|
|
352
|
+
_migrateLegacyMcp(deps);
|
|
305
353
|
|
|
306
354
|
// Non-fatal, cache-only: heal the optional Electron binary from local cache
|
|
307
355
|
// or emit a deferred notice (GUI provisions on first use). Never throws.
|
|
@@ -336,4 +384,5 @@ if (require.main === module) {
|
|
|
336
384
|
runCli();
|
|
337
385
|
}
|
|
338
386
|
|
|
339
|
-
module.exports = { main, runCli, addMcpToConfigFile, installSkill, installCouncilSkill,
|
|
387
|
+
module.exports = { main, runCli, addMcpToConfigFile, installSkill, installCouncilSkill,
|
|
388
|
+
setupHooks, provisionElectron, registerClaudeCode, registerClaudeDesktop, migrateLegacyMcp, COUNCIL_FILES };
|
|
@@ -41,6 +41,8 @@ function realDeps() {
|
|
|
41
41
|
fix: false,
|
|
42
42
|
discoverClaudeCodeMcps: () => require('./utils/mcp-discovery').discoverClaudeCodeMcps(),
|
|
43
43
|
discoverCoworkMcps: () => require('./utils/mcp-discovery').discoverCoworkMcps(),
|
|
44
|
+
inspectLegacyMcpEntries: () => require('./utils/legacy-mcp-migration').inspectAllLegacySidecarEntries(),
|
|
45
|
+
migrateLegacyMcpEntries: () => require('./utils/legacy-mcp-migration').migrateLegacySidecar(),
|
|
44
46
|
skillInstalled: () => {
|
|
45
47
|
const dir = path.join(os.homedir(), '.claude', 'skills');
|
|
46
48
|
return fs.existsSync(path.join(dir, 'sidecar', 'SKILL.md'))
|
|
@@ -181,6 +183,47 @@ async function runDoctorChecks(depsOverride = {}) {
|
|
|
181
183
|
return { id: 'mcp', name: 'MCP registration', status: 'ok', message: `registered: Claude Code${extra}`, hint: null };
|
|
182
184
|
}));
|
|
183
185
|
|
|
186
|
+
// Duplicate legacy 'sidecar' MCP registration (same server twice — doubles
|
|
187
|
+
// the client-visible tool list). Detection reads the raw config files via
|
|
188
|
+
// legacy-mcp-migration: mcp-discovery can't see it (it strips 'sidecar' as
|
|
189
|
+
// its own recursion guard). --fix removes only identical-in-effect twins.
|
|
190
|
+
checks.push(guard('mcp-legacy', 'Legacy sidecar MCP entry', () => {
|
|
191
|
+
const id = 'mcp-legacy'; const name = 'Legacy sidecar MCP entry';
|
|
192
|
+
const entries = d.inspectLegacyMcpEntries() || [];
|
|
193
|
+
const dupes = entries.filter(e => e.status === 'removable');
|
|
194
|
+
const custom = entries.filter(e => e.status === 'customized');
|
|
195
|
+
// An unreadable config is neither "no problem" nor a duplicate we can act
|
|
196
|
+
// on — reporting it as ok/'none' would hide a config doctor (and --fix)
|
|
197
|
+
// could not actually inspect. Always surface it, even alongside dupes.
|
|
198
|
+
const unreadable = entries.filter(e => e.status === 'unreadable');
|
|
199
|
+
const unreadableNote = unreadable.length
|
|
200
|
+
? `${unreadable.map(e => e.target).join(', ')} config unreadable — skipped`
|
|
201
|
+
: null;
|
|
202
|
+
if (dupes.length === 0) {
|
|
203
|
+
if (unreadableNote) {
|
|
204
|
+
const suffix = custom.length ? `; custom 'sidecar' entry in ${custom.map(e => e.target).join(', ')} — left alone` : '';
|
|
205
|
+
return { id, name, status: 'warn', message: `${unreadableNote}${suffix}`, hint: null };
|
|
206
|
+
}
|
|
207
|
+
const message = custom.length
|
|
208
|
+
? `custom 'sidecar' entry in ${custom.map(e => e.target).join(', ')} — left alone`
|
|
209
|
+
: 'none';
|
|
210
|
+
return { id, name, status: 'ok', message, hint: null };
|
|
211
|
+
}
|
|
212
|
+
if (d.fix) {
|
|
213
|
+
const removed = (d.migrateLegacyMcpEntries() || []).filter(r => r.result === 'removed');
|
|
214
|
+
if (removed.length >= dupes.length) {
|
|
215
|
+
const message = `removed legacy entry from: ${removed.map(r => r.target).join(', ')}`;
|
|
216
|
+
return unreadableNote
|
|
217
|
+
? { id, name, status: 'warn', message: `${message}; ${unreadableNote}`, hint: HINTS.removeLegacySidecar }
|
|
218
|
+
: { id, name, status: 'ok', message, hint: null };
|
|
219
|
+
}
|
|
220
|
+
const message = `removed ${removed.length}/${dupes.length} duplicate(s) — could not update every config`;
|
|
221
|
+
return { id, name, status: 'warn', message: unreadableNote ? `${message}; ${unreadableNote}` : message, hint: HINTS.removeLegacySidecar };
|
|
222
|
+
}
|
|
223
|
+
const message = `duplicate 'sidecar' entry in ${dupes.map(e => e.target).join(', ')} — doubles the MCP tool list`;
|
|
224
|
+
return { id, name, status: 'warn', message: unreadableNote ? `${message}; ${unreadableNote}` : message, hint: HINTS.removeLegacySidecar };
|
|
225
|
+
}));
|
|
226
|
+
|
|
184
227
|
// #43: OpenRouter credit/free-tier — warns (never errors); skipped when no key.
|
|
185
228
|
checks.push(await guardAsync('openrouter-credit', 'OpenRouter credit', async () => {
|
|
186
229
|
const values = d.readApiKeyValues() || {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `amicus status <task_id>` — one-shot human/JSON status for a session or wave.
|
|
3
|
+
* Reads the SAME sources as the MCP amicus_status handler by calling it
|
|
4
|
+
* directly (requiring mcp-server does NOT start the server; the MCP SDK is
|
|
5
|
+
* only loaded inside startMcpServer()). Zero duplicated status logic — this
|
|
6
|
+
* inherits crash detection, wave leg rollup, and P6-3 enrichment for free.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const { validateTaskId } = require('./utils/validators');
|
|
12
|
+
|
|
13
|
+
/** Render a key-value block for a single-session status payload. */
|
|
14
|
+
function formatRunHuman(d) {
|
|
15
|
+
const lines = [
|
|
16
|
+
`Task: ${d.taskId}`,
|
|
17
|
+
`Status: ${d.status}${d.phase ? ` (${d.phase})` : ''}`,
|
|
18
|
+
`Elapsed: ${d.elapsed}`,
|
|
19
|
+
];
|
|
20
|
+
if (d.model) { lines.push(`Model: ${d.model}`); }
|
|
21
|
+
if (d.mode) { lines.push(`Mode: ${d.mode}`); }
|
|
22
|
+
if (d.messageCount !== undefined) { lines.push(`Messages: ${d.messageCount}`); }
|
|
23
|
+
if (d.lastActivity) { lines.push(`Activity: ${d.lastActivity}`); }
|
|
24
|
+
if (d.latestPreview) { lines.push(`Latest: ${d.latestPreview}`); }
|
|
25
|
+
else if (d.latest) { lines.push(`Latest: ${d.latest}`); }
|
|
26
|
+
if (d.stalled) { lines.push(`STALLED: no activity for ${d.stalledForSeconds}s (see --json for recovery)`); }
|
|
27
|
+
if (d.reason) { lines.push(`Reason: ${d.reason}`); }
|
|
28
|
+
return lines.join('\n');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Render a wave payload: header + one line per leg. */
|
|
32
|
+
function formatWaveHumanStatus(d) {
|
|
33
|
+
const head = `Wave ${d.taskId}: ${d.status} — ${d.legsComplete}/${d.legsTotal} legs done (${d.elapsed})`;
|
|
34
|
+
const legLines = (d.legs || []).map((l) => {
|
|
35
|
+
const label = String(l.model || l.taskId || '').padEnd(28);
|
|
36
|
+
const st = String(l.status || 'unknown').padEnd(10);
|
|
37
|
+
const msgs = l.messages !== undefined ? `${l.messages} msg` : '';
|
|
38
|
+
const latest = l.latestPreview || l.latestActivity || '';
|
|
39
|
+
const flag = l.stalled ? ' ⏳stalled' : '';
|
|
40
|
+
return ` ${label} ${st} ${msgs} | ${latest}${flag}`;
|
|
41
|
+
});
|
|
42
|
+
return [head, ...legLines].join('\n');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Handle 'amicus status'. Exit code 0 = status retrieved (any run state, even
|
|
47
|
+
* a failed/crashed run — the QUERY succeeded); 1 = missing/invalid/unknown id.
|
|
48
|
+
* @param {object} args parsed CLI args
|
|
49
|
+
* @returns {Promise<number>}
|
|
50
|
+
*/
|
|
51
|
+
async function handleStatus(args) {
|
|
52
|
+
const taskId = args.wave || args._[1];
|
|
53
|
+
if (!taskId || taskId === true) {
|
|
54
|
+
process.stderr.write('Error: task_id is required for status\n');
|
|
55
|
+
process.stderr.write('Usage: amicus status <task_id> [--json] (or: amicus status --wave <wave_id>)\n');
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
const check = validateTaskId(String(taskId));
|
|
59
|
+
if (!check.valid) { process.stderr.write(`${check.error}\n`); return 1; }
|
|
60
|
+
|
|
61
|
+
const project = args.cwd || process.cwd();
|
|
62
|
+
const { handlers } = require('./mcp-server');
|
|
63
|
+
const result = await handlers.amicus_status({ taskId: String(taskId) }, project);
|
|
64
|
+
const text = result.content[0].text;
|
|
65
|
+
if (result.isError) { process.stderr.write(`${text}\n`); return 1; }
|
|
66
|
+
|
|
67
|
+
let data;
|
|
68
|
+
try { data = JSON.parse(text); } catch { process.stdout.write(`${text}\n`); return 0; }
|
|
69
|
+
delete data.next_poll; // MCP-agent polling guidance, not CLI output
|
|
70
|
+
|
|
71
|
+
if (args.json) { process.stdout.write(`${JSON.stringify(data, null, 2)}\n`); return 0; }
|
|
72
|
+
process.stdout.write(`${data.type === 'wave' ? formatWaveHumanStatus(data) : formatRunHuman(data)}\n`);
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { handleStatus, formatRunHuman, formatWaveHumanStatus };
|
package/src/cli-handlers.js
CHANGED
|
@@ -123,6 +123,16 @@ async function handleAbort(args) {
|
|
|
123
123
|
console.error(`Session ${taskId} has malformed metadata`);
|
|
124
124
|
process.exit(1);
|
|
125
125
|
}
|
|
126
|
+
// Guard against a completed/terminal session: without this, metadata.pid
|
|
127
|
+
// still holds a value forever and `amicus abort <completed-task>` would
|
|
128
|
+
// wait the grace window then TerminateProcess whatever unrelated process
|
|
129
|
+
// now owns that (possibly recycled) pid. Mirrors MCP's amicus_abort guard
|
|
130
|
+
// (src/mcp-server.js) — same wording, no re-mark, no kill.
|
|
131
|
+
if (meta.status !== 'running') {
|
|
132
|
+
console.log(`Session ${taskId} is not running (status: ${meta.status}).`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
126
136
|
const { markAborted } = require('./utils/session-abort');
|
|
127
137
|
|
|
128
138
|
// F4: aborting a wave aborts every still-running leg too.
|
|
@@ -147,6 +157,28 @@ async function handleAbort(args) {
|
|
|
147
157
|
|
|
148
158
|
markAborted(sessionDir, 'manual abort');
|
|
149
159
|
console.log(`Session ${taskId} marked as aborted.`);
|
|
160
|
+
|
|
161
|
+
// Phase 3: fallback direct-kill for a session that does not honor the
|
|
162
|
+
// marker. Headless loops poll the marker every ~2s and the interactive
|
|
163
|
+
// abort watch does too, so the normal outcome is a graceful exit during
|
|
164
|
+
// the grace window; only a wedged/legacy process gets SIGTERM. The wait is
|
|
165
|
+
// awaited on purpose — bin/amicus.js arms its force-exit watchdog only
|
|
166
|
+
// after this handler returns.
|
|
167
|
+
if (meta.pid) {
|
|
168
|
+
const { waitThenKill, abortGraceMs } = require('./utils/abort-coordinator');
|
|
169
|
+
const graceSec = Math.ceil(abortGraceMs() / 1000);
|
|
170
|
+
console.log(`Waiting up to ${graceSec}s for the session process (pid ${meta.pid}) to exit gracefully...`);
|
|
171
|
+
const { killed, exited } = await waitThenKill(meta.pid);
|
|
172
|
+
if (killed.length > 0) {
|
|
173
|
+
console.log(`Process ${meta.pid} did not exit in time — sent SIGTERM (a hard kill on Windows).`);
|
|
174
|
+
} else if (exited.length > 0) {
|
|
175
|
+
console.log('Process exited cleanly.');
|
|
176
|
+
} else {
|
|
177
|
+
// 3.1 contract: an EPERM-unkillable pid lands in NEITHER array —
|
|
178
|
+
// it is still alive and we could not signal it. Say so honestly.
|
|
179
|
+
console.log(`Process ${meta.pid} is still running — could not signal it (insufficient permission). It may require manual termination.`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
150
182
|
}
|
|
151
183
|
|
|
152
184
|
/**
|
package/src/cli.js
CHANGED
|
@@ -330,6 +330,7 @@ Commands:
|
|
|
330
330
|
start Launch a new amicus session
|
|
331
331
|
fanout Run N models on the same prompt in parallel (headless)
|
|
332
332
|
list Show previous sessions
|
|
333
|
+
status One-shot status for a session or wave (--json)
|
|
333
334
|
resume Reopen a previous session
|
|
334
335
|
continue New session building on previous
|
|
335
336
|
read Output session summary/conversation
|
|
@@ -416,6 +417,13 @@ Options for 'list':
|
|
|
416
417
|
--status <filter> Filter by status (running, complete)
|
|
417
418
|
--all Show all projects
|
|
418
419
|
--json Output as JSON
|
|
420
|
+
`,
|
|
421
|
+
status: `
|
|
422
|
+
Options for 'status':
|
|
423
|
+
<task_id> Required. Session or wave ID (positional)
|
|
424
|
+
--wave <wave_id> Alternative to the positional ID for waves
|
|
425
|
+
--json Machine-readable output
|
|
426
|
+
--cwd <path> Project directory (default: cwd)
|
|
419
427
|
`,
|
|
420
428
|
abort: `
|
|
421
429
|
Options for 'abort':
|