amicus 1.6.1 → 1.7.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 +1 -1
- package/CHANGELOG.md +31 -0
- package/electron/load-failsafe.js +11 -4
- package/electron/main.js +12 -9
- package/electron/opencode-theme.js +130 -0
- package/electron/preload.js +1 -1
- package/package.json +1 -1
- package/scripts/postinstall.js +59 -35
- package/src/cli-handlers-doctor.js +90 -14
- package/src/cli-handlers-run.js +4 -0
- package/src/cli.js +3 -0
- package/src/mcp-server.js +26 -6
- package/src/mcp-tools.js +17 -1
- package/src/sidecar/electron-cache.js +42 -0
- package/src/sidecar/electron-ensure.js +92 -0
- package/src/sidecar/electron-install.js +244 -0
- package/src/sidecar/fanout.js +3 -0
- package/src/sidecar/interactive.js +22 -10
- package/src/sidecar/setup-window.js +12 -7
- package/src/sidecar/setup.js +2 -0
- package/src/utils/project-root-sanity.js +66 -0
- package/src/utils/remediation-hints.js +51 -0
- package/src/utils/result-schema.js +16 -3
- package/src/utils/version-info.js +49 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amicus",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.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
|
"author": { "name": "Christian Wagner" },
|
|
6
6
|
"homepage": "https://bourbondog.github.io/amicus/",
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,37 @@ All notable changes to Amicus are documented here. Format follows
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.7.0] - 2026-06-30
|
|
9
|
+
|
|
10
|
+
Electron self-heal, a real `amicus doctor`, and the GUI on the design system — plus MCP/diagnostics correctness.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Electron self-heal.** Amicus now detects a broken or quarantined Electron install (a half-extracted
|
|
14
|
+
or AV-removed binary) and repairs it from the local download cache — **fully offline**. New
|
|
15
|
+
`amicus doctor --fix` heals in place, the GUI lazily provisions itself on first use, and an opt-in
|
|
16
|
+
`AMICUS_PREFETCH_ELECTRON=1` aggressively prewarms it. Install-time provisioning is cache-only (no
|
|
17
|
+
network during `npm install`) and never fails the install.
|
|
18
|
+
- **`amicus doctor` is now a recovery hub.** Checks carry copy-paste remediation hints, report
|
|
19
|
+
OpenRouter credit/free-tier status, and warn when the resolved project root looks like an app/install
|
|
20
|
+
directory rather than your repo.
|
|
21
|
+
- **Running version in MCP responses.** `amicus_status` / `amicus_guide` now report the running amicus
|
|
22
|
+
version and warn when the on-disk package is newer (restart your MCP client to load it).
|
|
23
|
+
- **The GUI is on the design system.** The embedded OpenCode session UI is themed to the clay/gold
|
|
24
|
+
tokens, the load-failsafe error page and window backgrounds are token-driven, and a drift guard keeps
|
|
25
|
+
new hardcoded colors/fonts out of `electron/`.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- **Electron no longer reads "installed" when the binary is missing.** Runtime checks (including
|
|
29
|
+
`amicus doctor` and the GUI launch path) now stat the actual executable instead of trusting
|
|
30
|
+
`path.txt`, so a quarantined/half-extracted Electron is correctly detected — the root cause of the
|
|
31
|
+
silently-broken setup wizard.
|
|
32
|
+
- **`amicus_fanout` forwards Cowork session pinning** (`--cowork-process` / parent session) to its
|
|
33
|
+
spawned legs, so context-inheriting fan-outs pin the right parent.
|
|
34
|
+
- **`amicus_status` annotation corrected** — it is no longer declared read-only/idempotent, since its
|
|
35
|
+
wave branch updates metadata during crash detection.
|
|
36
|
+
- **Wave counts account for crashed / idle-timeout legs** (documented remainder rule), so consumers
|
|
37
|
+
summing the named buckets no longer mismatch the total.
|
|
38
|
+
|
|
8
39
|
## [1.6.1] - 2026-06-30
|
|
9
40
|
|
|
10
41
|
Project-directory and session-addressing correctness — agents, sessions, and the interactive GUI now agree on which project they're in.
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
'use strict';
|
|
16
16
|
|
|
17
|
+
const { tokenCss } = require('../src/design/tokens');
|
|
18
|
+
|
|
17
19
|
const DEFAULT_TIMEOUT_MS = 15000;
|
|
18
20
|
const ERR_ABORTED = -3; // benign: fires on in-page redirects/navigation replacement
|
|
19
21
|
|
|
@@ -62,14 +64,19 @@ function escapeHTML(value) {
|
|
|
62
64
|
* @returns {string} full HTML document
|
|
63
65
|
*/
|
|
64
66
|
function buildLoadErrorHTML({ url, errorCode, errorDescription }) {
|
|
67
|
+
// This page renders after the OpenCode UI definitively failed to load, in a
|
|
68
|
+
// data: URL context. Inline the design tokens with absolute font URLs so the
|
|
69
|
+
// bundled webfonts resolve and var(--x) references work with no external CSS.
|
|
65
70
|
return `<!DOCTYPE html>
|
|
66
71
|
<html><head><style>
|
|
67
|
-
|
|
72
|
+
${tokenCss({ absoluteFontUrls: true })}
|
|
73
|
+
body { background: var(--bg); color: var(--text-2);
|
|
74
|
+
font-family: var(--font-sans);
|
|
68
75
|
display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
|
|
69
76
|
.box { max-width: 440px; padding: 24px; }
|
|
70
|
-
h1 { color:
|
|
71
|
-
p { font-size: 13px; line-height: 1.5; color:
|
|
72
|
-
code { font-family:
|
|
77
|
+
h1 { color: var(--accent); font-size: 16px; margin: 0 0 12px; }
|
|
78
|
+
p { font-size: 13px; line-height: 1.5; color: var(--text-2); margin: 0 0 10px; }
|
|
79
|
+
code { font-family: var(--font-mono); font-size: 11px; color: var(--text-1);
|
|
73
80
|
word-break: break-all; }
|
|
74
81
|
</style></head><body>
|
|
75
82
|
<div class="box">
|
package/electron/main.js
CHANGED
|
@@ -22,6 +22,8 @@ const { registerSetupHandlers } = require('./ipc-setup');
|
|
|
22
22
|
const { computeWindowPosition } = require('./window-position');
|
|
23
23
|
const { attachLoadFailsafe, buildLoadErrorHTML } = require('./load-failsafe');
|
|
24
24
|
const { buildSessionRoute } = require('./session-route');
|
|
25
|
+
const { buildOpencodeThemeCSS } = require('./opencode-theme');
|
|
26
|
+
const { TOKENS } = require('../src/design/tokens');
|
|
25
27
|
|
|
26
28
|
const ICON_PATH = path.join(__dirname, 'assets', 'icon.png');
|
|
27
29
|
|
|
@@ -98,7 +100,7 @@ function createAmicusWindow() {
|
|
|
98
100
|
width: WIN_W, height: WIN_H, minWidth: 550, minHeight: 600,
|
|
99
101
|
x: winX, y: winY,
|
|
100
102
|
show: false,
|
|
101
|
-
frame: true, backgroundColor:
|
|
103
|
+
frame: true, backgroundColor: TOKENS.bg,
|
|
102
104
|
title: 'Amicus',
|
|
103
105
|
icon: ICON_PATH,
|
|
104
106
|
webPreferences: {
|
|
@@ -148,13 +150,14 @@ function createAmicusWindow() {
|
|
|
148
150
|
url: OPENCODE_URL, sessionId: OPENCODE_SESSION_ID, taskId: TASK_ID
|
|
149
151
|
});
|
|
150
152
|
|
|
151
|
-
// Use Electron's insertCSS API on dom-ready to hide OpenCode branding
|
|
152
|
-
//
|
|
153
|
+
// Use Electron's insertCSS API on dom-ready to hide OpenCode branding AND
|
|
154
|
+
// theme the embedded chat surface to the clay/gold tokens (#49). The theme
|
|
155
|
+
// is token-driven — it inlines tokenCss() and remaps OpenCode's own :root
|
|
156
|
+
// custom properties — so it tracks the toolbar without brittle class
|
|
157
|
+
// selectors. insertCSS is more reliable than preload DOM injection in a
|
|
158
|
+
// BrowserView. NOTE: the live visual match is a user-side CDP/manual check.
|
|
153
159
|
contentView.webContents.on('dom-ready', () => {
|
|
154
|
-
contentView.webContents.insertCSS(
|
|
155
|
-
#root > div > header { display: none !important; }
|
|
156
|
-
svg[viewBox="0 0 234 42"] { visibility: hidden !important; }
|
|
157
|
-
`).catch(() => {});
|
|
160
|
+
contentView.webContents.insertCSS(buildOpencodeThemeCSS()).catch(() => {});
|
|
158
161
|
});
|
|
159
162
|
|
|
160
163
|
// Navigate directly to the session URL to bypass the project selection screen.
|
|
@@ -290,7 +293,7 @@ async function createSetupWindow() {
|
|
|
290
293
|
|
|
291
294
|
mainWindow = new BrowserWindow({
|
|
292
295
|
width: 560, height: 680, minWidth: 480, minHeight: 580,
|
|
293
|
-
frame: true, backgroundColor:
|
|
296
|
+
frame: true, backgroundColor: TOKENS.bg,
|
|
294
297
|
title: `${getBrandName(CLIENT)} Setup`,
|
|
295
298
|
icon: ICON_PATH,
|
|
296
299
|
resizable: false,
|
|
@@ -443,7 +446,7 @@ function createSettingsChildWindow() {
|
|
|
443
446
|
const settingsWin = new BrowserWindow({
|
|
444
447
|
width: 560, height: 680,
|
|
445
448
|
parent: mainWindow, modal: false,
|
|
446
|
-
frame: true, backgroundColor:
|
|
449
|
+
frame: true, backgroundColor: TOKENS.bg,
|
|
447
450
|
title: `${getBrandName(CLIENT)} Settings`,
|
|
448
451
|
icon: ICON_PATH,
|
|
449
452
|
resizable: false,
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Issue #49 — token-driven theme for the embedded OpenCode web UI.
|
|
5
|
+
*
|
|
6
|
+
* main.js injects this via `contentView.webContents.insertCSS(...)` on
|
|
7
|
+
* `dom-ready`. Before #49 that hook only HID OpenCode's header/wordmark; this
|
|
8
|
+
* module extends it so the embedded chat surface inherits the clay/gold tokens
|
|
9
|
+
* and matches the token-driven Amicus toolbar (toolbar.js).
|
|
10
|
+
*
|
|
11
|
+
* Robustness note (the fragile bit): OpenCode (1.17.11 at time of writing) is a
|
|
12
|
+
* SolidStart app whose generated class names can change between releases. So we
|
|
13
|
+
* PREFER overriding OpenCode's OWN :root CSS custom properties — a much more
|
|
14
|
+
* stable surface than `.css-abc123` class selectors. We:
|
|
15
|
+
* 1. inline the canonical token CSS (tokenCss) so OUR vars + @font-face are
|
|
16
|
+
* available inside the BrowserView (absolute font URLs, same as toolbar.js
|
|
17
|
+
* / setup-ui-styles.js / load-failsafe.js do),
|
|
18
|
+
* 2. remap a generous superset of OpenCode's plausible theme custom-property
|
|
19
|
+
* names to our tokens (var(--...)), covering the prefixes OpenCode has
|
|
20
|
+
* shipped (--sst-*, --theme-*, --ock-*, --color-*), and
|
|
21
|
+
* 3. add a few low-specificity element fallbacks (body/font) for the case
|
|
22
|
+
* where a release renames its root vars entirely.
|
|
23
|
+
*
|
|
24
|
+
* Keep this side-effect-free and Electron-free so it stays unit-testable
|
|
25
|
+
* without booting Electron (main.js boots Electron on require).
|
|
26
|
+
*
|
|
27
|
+
* NOT verifiable headless: the live visual match against a running OpenCode
|
|
28
|
+
* session is a USER-SIDE CDP/manual check.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const { tokenCss } = require('../src/design/tokens');
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The original hide-only rules (header + wordmark) preserved from the pre-#49
|
|
35
|
+
* dom-ready hook. rebrandUI() swaps the wordmark; these keep OpenCode's own
|
|
36
|
+
* chrome out of view until/while that runs.
|
|
37
|
+
*/
|
|
38
|
+
const HIDE_CHROME = `
|
|
39
|
+
#root > div > header { display: none !important; }
|
|
40
|
+
svg[viewBox="0 0 234 42"] { visibility: hidden !important; }
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Map OpenCode's own theme custom properties onto our clay/gold tokens. We list
|
|
45
|
+
* the property names under every prefix OpenCode has used so a rename in one
|
|
46
|
+
* family still leaves the others driving the surface. Unknown vars are simply
|
|
47
|
+
* inert — setting an unused custom property has no effect — so over-listing is
|
|
48
|
+
* safe and is the robust play here.
|
|
49
|
+
*/
|
|
50
|
+
const OPENCODE_ROOT_OVERRIDES = `
|
|
51
|
+
:root {
|
|
52
|
+
/* --- backgrounds / surfaces --- */
|
|
53
|
+
--sst-color-background: var(--bg);
|
|
54
|
+
--sst-color-background-panel: var(--surface-1);
|
|
55
|
+
--sst-color-background-element: var(--surface-2);
|
|
56
|
+
--theme-background: var(--bg);
|
|
57
|
+
--theme-background-panel: var(--surface-1);
|
|
58
|
+
--theme-background-element: var(--surface-2);
|
|
59
|
+
--ock-background: var(--bg);
|
|
60
|
+
--ock-background-panel: var(--surface-1);
|
|
61
|
+
--color-background: var(--bg);
|
|
62
|
+
--color-surface: var(--surface-1);
|
|
63
|
+
--color-surface-raised: var(--surface-2);
|
|
64
|
+
|
|
65
|
+
/* --- borders --- */
|
|
66
|
+
--sst-color-border: var(--border);
|
|
67
|
+
--theme-border: var(--border);
|
|
68
|
+
--ock-border: var(--border);
|
|
69
|
+
--color-border: var(--border);
|
|
70
|
+
--color-border-strong: var(--border-strong);
|
|
71
|
+
|
|
72
|
+
/* --- text --- */
|
|
73
|
+
--sst-color-text: var(--text-1);
|
|
74
|
+
--sst-color-text-secondary: var(--text-2);
|
|
75
|
+
--sst-color-text-muted: var(--text-3);
|
|
76
|
+
--theme-text: var(--text-1);
|
|
77
|
+
--theme-text-muted: var(--text-2);
|
|
78
|
+
--ock-text: var(--text-1);
|
|
79
|
+
--ock-text-muted: var(--text-2);
|
|
80
|
+
--color-text: var(--text-1);
|
|
81
|
+
--color-text-muted: var(--text-2);
|
|
82
|
+
|
|
83
|
+
/* --- brand accent (clay) --- */
|
|
84
|
+
--sst-color-primary: var(--accent-500);
|
|
85
|
+
--sst-color-accent: var(--accent-500);
|
|
86
|
+
--theme-primary: var(--accent-500);
|
|
87
|
+
--theme-accent: var(--accent-500);
|
|
88
|
+
--ock-primary: var(--accent-500);
|
|
89
|
+
--ock-accent: var(--accent-500);
|
|
90
|
+
--color-primary: var(--accent-500);
|
|
91
|
+
--color-accent: var(--accent-500);
|
|
92
|
+
--color-link: var(--accent-400);
|
|
93
|
+
|
|
94
|
+
/* --- counter-accent (gold) --- */
|
|
95
|
+
--sst-color-secondary: var(--gold-400);
|
|
96
|
+
--theme-secondary: var(--gold-400);
|
|
97
|
+
--ock-secondary: var(--gold-400);
|
|
98
|
+
--color-secondary: var(--gold-400);
|
|
99
|
+
}
|
|
100
|
+
`;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Low-specificity element fallbacks. These only bite if OpenCode renamed its
|
|
104
|
+
* root vars entirely (so the overrides above no-op); they intentionally avoid
|
|
105
|
+
* generated class names. Kept gentle so they don't fight OpenCode's layout.
|
|
106
|
+
*/
|
|
107
|
+
const ELEMENT_FALLBACKS = `
|
|
108
|
+
html, body {
|
|
109
|
+
background-color: var(--bg);
|
|
110
|
+
color: var(--text-1);
|
|
111
|
+
font-family: var(--font-sans);
|
|
112
|
+
}
|
|
113
|
+
a { color: var(--accent-500); }
|
|
114
|
+
::selection { background: var(--accent-soft); }
|
|
115
|
+
`;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Build the full theme CSS string injected into the OpenCode BrowserView.
|
|
119
|
+
* @returns {string} hide-chrome + inlined tokens + :root overrides + fallbacks.
|
|
120
|
+
*/
|
|
121
|
+
function buildOpencodeThemeCSS() {
|
|
122
|
+
return [
|
|
123
|
+
HIDE_CHROME,
|
|
124
|
+
tokenCss({ absoluteFontUrls: true }),
|
|
125
|
+
OPENCODE_ROOT_OVERRIDES,
|
|
126
|
+
ELEMENT_FALLBACKS,
|
|
127
|
+
].join('\n');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = { buildOpencodeThemeCSS };
|
package/electron/preload.js
CHANGED
|
@@ -34,7 +34,7 @@ function injectBrandingCss() {
|
|
|
34
34
|
try {
|
|
35
35
|
const style = document.createElement('style');
|
|
36
36
|
style.textContent = [
|
|
37
|
-
'html, body { background-color: #
|
|
37
|
+
'html, body { background-color: var(--bg, #0a0a0a) !important; }',
|
|
38
38
|
'#root > div > header { display: none !important; }',
|
|
39
39
|
'svg[viewBox="0 0 234 42"] { display: none !important; }',
|
|
40
40
|
].join('\n');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amicus",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.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
|
@@ -13,8 +13,14 @@ const path = require('path');
|
|
|
13
13
|
const os = require('os');
|
|
14
14
|
const { execFileSync } = require('child_process');
|
|
15
15
|
|
|
16
|
+
const { repairElectron } = require('../src/sidecar/electron-install');
|
|
17
|
+
const HINTS = require('../src/utils/remediation-hints');
|
|
18
|
+
|
|
16
19
|
const SETUP_HOOKS_SCRIPT = path.join(__dirname, 'setup-hooks.js');
|
|
17
20
|
|
|
21
|
+
/** Short cache-only provision budget: a slow disk must never hang the install. */
|
|
22
|
+
const PROVISION_TIMEOUT_MS = 15000;
|
|
23
|
+
|
|
18
24
|
const SKILL_SOURCE = path.join(__dirname, '..', 'skills', 'sidecar', 'SKILL.md');
|
|
19
25
|
const COUNCIL_SOURCE_DIR = path.join(__dirname, '..', 'skills', 'second-opinion');
|
|
20
26
|
|
|
@@ -175,40 +181,56 @@ function registerClaudeDesktop() {
|
|
|
175
181
|
}
|
|
176
182
|
|
|
177
183
|
/**
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
*
|
|
193
|
-
*
|
|
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.
|
|
184
|
+
* NON-FATAL, CACHE-ONLY provisioning of the OPTIONAL electron binary (#57,
|
|
185
|
+
* supersedes #30's warn-only verifyElectron). Electron is an optionalDependency:
|
|
186
|
+
* npm exits 0 even if its download/extract failed (or AV quarantined
|
|
187
|
+
* electron.exe), so without this the user only finds out the GUI is broken much
|
|
188
|
+
* later.
|
|
189
|
+
*
|
|
190
|
+
* We heal from the LOCAL cache via repairElectron({cacheOnly:true}) — NO network
|
|
191
|
+
* during install. When a cached zip extracts cleanly the GUI just works. When
|
|
192
|
+
* there is no cache (or the repair defers/contends), we emit a clear notice that
|
|
193
|
+
* the GUI provisions on first use and that headless runs + the council already
|
|
194
|
+
* work, then point at `amicus doctor --fix` (#56) — NOT a reinstall, which can
|
|
195
|
+
* loop. A short timeout keeps a slow disk from ever hanging the install.
|
|
196
|
+
*
|
|
197
|
+
* This MUST never throw out of postinstall — the whole body (sync setup, the
|
|
198
|
+
* awaited repair, and a synchronous-throw resolver) is guarded so nothing here
|
|
199
|
+
* can turn into a non-zero exit (#29 always-exit-0 guard preserved).
|
|
197
200
|
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
201
|
+
* OPT-IN PREWARM (#60): when AMICUS_PREFETCH_ELECTRON=1 the user has asked to
|
|
202
|
+
* aggressively prewarm the GUI at install time. We additionally drive a full
|
|
203
|
+
* (possibly-networked) fetch via repairElectron({force:true}). This is the ONLY
|
|
204
|
+
* path that may hit the network during install; it stays opt-in and non-fatal
|
|
205
|
+
* (a throw here is swallowed and exit 0 is preserved). The DEFAULT remains
|
|
206
|
+
* cache-only (#57).
|
|
200
207
|
*
|
|
201
|
-
* @param {object} deps - {
|
|
208
|
+
* @param {object} deps - { repairElectron } override for testing.
|
|
209
|
+
* @returns {Promise<void>}
|
|
202
210
|
*/
|
|
203
|
-
function
|
|
211
|
+
async function provisionElectron(deps = {}) {
|
|
204
212
|
try {
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
213
|
+
const _repair = deps.repairElectron || repairElectron;
|
|
214
|
+
|
|
215
|
+
// Opt-in aggressive prewarm (#60): full fetch if needed. Non-fatal.
|
|
216
|
+
if (process.env.AMICUS_PREFETCH_ELECTRON === '1') {
|
|
217
|
+
console.log('[amicus] AMICUS_PREFETCH_ELECTRON=1 — prewarming the Electron GUI binary (may download)...');
|
|
218
|
+
const forced = await _repair({ force: true });
|
|
219
|
+
if (forced && (forced.repaired || forced.usable)) {
|
|
220
|
+
console.log('[amicus] Electron GUI binary prewarmed.');
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
console.warn('[amicus] Note: Electron prewarm did not complete now — the GUI provisions on first use.');
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const result = await _repair({ cacheOnly: true, timeoutMs: PROVISION_TIMEOUT_MS });
|
|
228
|
+
if (result && (result.repaired || result.usable)) { return; }
|
|
229
|
+
// No cache hit (deferred), contended, or otherwise not provisioned now.
|
|
230
|
+
console.warn('[amicus] Note: the Electron GUI binary is not provisioned yet — it will download on first use of the interactive GUI / setup-wizard.');
|
|
231
|
+
console.warn(`[amicus] Headless runs and the council already work. To provision the GUI now: ${HINTS.doctorFix}`);
|
|
210
232
|
} catch {
|
|
211
|
-
// Never let
|
|
233
|
+
// Never let provisioning throw out of postinstall.
|
|
212
234
|
}
|
|
213
235
|
}
|
|
214
236
|
|
|
@@ -247,7 +269,7 @@ function setupHooks(deps = {}) {
|
|
|
247
269
|
}
|
|
248
270
|
}
|
|
249
271
|
|
|
250
|
-
function main(deps = {}) {
|
|
272
|
+
async function main(deps = {}) {
|
|
251
273
|
if (process.env.AMICUS_SKIP_POSTINSTALL === '1') {
|
|
252
274
|
console.log('[amicus] AMICUS_SKIP_POSTINSTALL set — skipping global setup (plugin channel handles registration).');
|
|
253
275
|
return;
|
|
@@ -257,6 +279,7 @@ function main(deps = {}) {
|
|
|
257
279
|
const _registerClaudeCode = deps.registerClaudeCode || registerClaudeCode;
|
|
258
280
|
const _registerClaudeDesktop = deps.registerClaudeDesktop || registerClaudeDesktop;
|
|
259
281
|
const _setupHooks = deps.setupHooks || setupHooks;
|
|
282
|
+
const _provisionElectron = deps.provisionElectron || provisionElectron;
|
|
260
283
|
|
|
261
284
|
console.log('[amicus] Installing...');
|
|
262
285
|
// Dev-only: configure git hooks (no-op for consumers). Folded in from the
|
|
@@ -267,8 +290,9 @@ function main(deps = {}) {
|
|
|
267
290
|
_registerClaudeCode();
|
|
268
291
|
_registerClaudeDesktop();
|
|
269
292
|
|
|
270
|
-
// Non-fatal
|
|
271
|
-
|
|
293
|
+
// Non-fatal, cache-only: heal the optional Electron binary from local cache
|
|
294
|
+
// or emit a deferred notice (GUI provisions on first use). Never throws.
|
|
295
|
+
await _provisionElectron(deps);
|
|
272
296
|
|
|
273
297
|
console.log('');
|
|
274
298
|
console.log('[amicus] Setup:');
|
|
@@ -282,9 +306,9 @@ function main(deps = {}) {
|
|
|
282
306
|
* package, but skill-copy + MCP registration are optional — amicus itself still
|
|
283
307
|
* works without them. Warn clearly and exit 0 (mirrors scripts/setup-hooks.js).
|
|
284
308
|
*/
|
|
285
|
-
function runCli(deps = {}) {
|
|
309
|
+
async function runCli(deps = {}) {
|
|
286
310
|
try {
|
|
287
|
-
main(deps);
|
|
311
|
+
await main(deps);
|
|
288
312
|
} catch (err) {
|
|
289
313
|
console.warn(`[amicus] Warning: optional post-install setup failed: ${err && err.message}`);
|
|
290
314
|
console.warn('[amicus] Skill install + MCP registration are optional — amicus itself still works.');
|
|
@@ -299,4 +323,4 @@ if (require.main === module) {
|
|
|
299
323
|
runCli();
|
|
300
324
|
}
|
|
301
325
|
|
|
302
|
-
module.exports = { main, runCli, addMcpToConfigFile, installSkill, installCouncilSkill, setupHooks,
|
|
326
|
+
module.exports = { main, runCli, addMcpToConfigFile, installSkill, installCouncilSkill, setupHooks, provisionElectron, COUNCIL_FILES };
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
// src/cli-handlers-doctor.js
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const HINTS = require('./utils/remediation-hints');
|
|
5
|
+
|
|
4
6
|
const MAX_CATALOG_AGE_MS = 24 * 60 * 60 * 1000; // 24h (mirrors model-catalog DEFAULT_MAX_AGE_MS)
|
|
5
7
|
|
|
8
|
+
/** #56: keep `doctor --fix`'s electron self-heal from ever hanging on a slow disk/network. */
|
|
9
|
+
const FIX_TIMEOUT_MS = 90 * 1000;
|
|
10
|
+
|
|
6
11
|
/** Default real helpers; tests override via deps. */
|
|
7
12
|
function realDeps() {
|
|
8
13
|
const fs = require('fs');
|
|
@@ -11,6 +16,13 @@ function realDeps() {
|
|
|
11
16
|
return {
|
|
12
17
|
nodeVersion: process.version,
|
|
13
18
|
readApiKeys: () => require('./utils/api-key-store').readApiKeys(),
|
|
19
|
+
readApiKeyValues: () => require('./utils/api-key-store').readApiKeyValues(),
|
|
20
|
+
checkOpenRouterCredit: (key) => require('./utils/api-key-validation').checkOpenRouterCredit(key),
|
|
21
|
+
getCwd: () => process.cwd(),
|
|
22
|
+
readProjectMarkers: (dir) => {
|
|
23
|
+
const exists = (name) => { try { return fs.existsSync(path.join(dir, name)); } catch (_e) { return false; } };
|
|
24
|
+
return { hasGit: exists('.git'), hasPackageJson: exists('package.json'), hasClaude: exists('.claude') };
|
|
25
|
+
},
|
|
14
26
|
getConfigDir: () => require('./utils/config').getConfigDir(),
|
|
15
27
|
resolveModel: () => require('./utils/config').resolveModel(),
|
|
16
28
|
readCache: () => require('./utils/model-catalog').readCache(),
|
|
@@ -27,6 +39,10 @@ function realDeps() {
|
|
|
27
39
|
return candidates.some(p => fs.existsSync(p));
|
|
28
40
|
},
|
|
29
41
|
getElectronPath: () => require('./sidecar/interactive').getElectronPath(),
|
|
42
|
+
// #56: self-heal primitive for `doctor --fix`. Pure probe (getElectronPath)
|
|
43
|
+
// stays separate; repair only runs when fix is requested.
|
|
44
|
+
repairElectron: (opts) => require('./sidecar/electron-install').repairElectron(opts),
|
|
45
|
+
fix: false,
|
|
30
46
|
discoverClaudeCodeMcps: () => require('./utils/mcp-discovery').discoverClaudeCodeMcps(),
|
|
31
47
|
discoverCoworkMcps: () => require('./utils/mcp-discovery').discoverCoworkMcps(),
|
|
32
48
|
skillInstalled: () => {
|
|
@@ -43,12 +59,19 @@ function guard(id, name, fn) {
|
|
|
43
59
|
catch (e) { return { id, name, status: 'error', message: e.message, hint: null }; }
|
|
44
60
|
}
|
|
45
61
|
|
|
62
|
+
/** Async variant of guard; a thrown/rejected fn becomes an error line. */
|
|
63
|
+
async function guardAsync(id, name, fn) {
|
|
64
|
+
try { return await fn(); }
|
|
65
|
+
catch (e) { return { id, name, status: 'error', message: e.message, hint: null }; }
|
|
66
|
+
}
|
|
67
|
+
|
|
46
68
|
/**
|
|
47
|
-
* Compose the health checks. Never throws
|
|
69
|
+
* Compose the health checks. Never throws (async; awaits non-blocking
|
|
70
|
+
* network checks such as the OpenRouter credit probe).
|
|
48
71
|
* @param {object} [depsOverride]
|
|
49
|
-
* @returns {Array<{id,name,status,message,hint}
|
|
72
|
+
* @returns {Promise<Array<{id,name,status,message,hint}>>}
|
|
50
73
|
*/
|
|
51
|
-
function runDoctorChecks(depsOverride = {}) {
|
|
74
|
+
async function runDoctorChecks(depsOverride = {}) {
|
|
52
75
|
const d = { ...realDeps(), ...depsOverride };
|
|
53
76
|
const checks = [];
|
|
54
77
|
|
|
@@ -105,19 +128,43 @@ function runDoctorChecks(depsOverride = {}) {
|
|
|
105
128
|
checks.push(guard('opencode-bin', 'OpenCode binary', () => (
|
|
106
129
|
d.hasOpencodeBinary()
|
|
107
130
|
? { 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:
|
|
131
|
+
: { id: 'opencode-bin', name: 'OpenCode binary', status: 'error', message: 'not found', hint: HINTS.reinstallEngine }
|
|
109
132
|
)));
|
|
110
133
|
|
|
111
|
-
checks.push(
|
|
112
|
-
d.getElectronPath()
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
134
|
+
checks.push(await guardAsync('electron', 'Electron (interactive GUI)', async () => {
|
|
135
|
+
if (d.getElectronPath()) {
|
|
136
|
+
return { id: 'electron', name: 'Electron (interactive GUI)', status: 'ok', message: 'installed', hint: null };
|
|
137
|
+
}
|
|
138
|
+
// Broken (missing / quarantined). With --fix, self-heal in place (#56):
|
|
139
|
+
// repairElectron provisions the binary; {deferred} (no cache, no network)
|
|
140
|
+
// maps to WARN — a deferred download is not a failure. Without --fix, just
|
|
141
|
+
// point the user at `amicus doctor --fix`.
|
|
142
|
+
if (d.fix) {
|
|
143
|
+
let res;
|
|
144
|
+
try {
|
|
145
|
+
res = await d.repairElectron({ timeoutMs: FIX_TIMEOUT_MS });
|
|
146
|
+
} catch (e) {
|
|
147
|
+
return { id: 'electron', name: 'Electron (interactive GUI)', status: 'warn', message: `repair failed: ${e.message} — headless still works`, hint: HINTS.doctorFix };
|
|
148
|
+
}
|
|
149
|
+
res = res || {};
|
|
150
|
+
if (res.repaired || res.usable) {
|
|
151
|
+
return { id: 'electron', name: 'Electron (interactive GUI)', status: 'ok', message: 'installed (self-healed)', hint: null };
|
|
152
|
+
}
|
|
153
|
+
const why = res.reason ? ` — ${res.reason}` : '';
|
|
154
|
+
const detail = res.deferred
|
|
155
|
+
? `deferred${why}`
|
|
156
|
+
: res.contended
|
|
157
|
+
? `repair already in progress${why}`
|
|
158
|
+
: `not provisioned${why}`;
|
|
159
|
+
return { id: 'electron', name: 'Electron (interactive GUI)', status: 'warn', message: `${detail} — headless still works`, hint: HINTS.doctorFix };
|
|
160
|
+
}
|
|
161
|
+
return { id: 'electron', name: 'Electron (interactive GUI)', status: 'warn', message: 'not installed — headless still works', hint: HINTS.doctorFix };
|
|
162
|
+
}));
|
|
116
163
|
|
|
117
164
|
checks.push(guard('skills', 'Skills installed', () => (
|
|
118
165
|
d.skillInstalled()
|
|
119
166
|
? { id: 'skills', name: 'Skills installed', status: 'ok', message: '~/.claude/skills/{sidecar,second-opinion}', hint: null }
|
|
120
|
-
: { id: 'skills', name: 'Skills installed', status: 'warn', message: 'one or both skills missing', hint:
|
|
167
|
+
: { id: 'skills', name: 'Skills installed', status: 'warn', message: 'one or both skills missing', hint: `${HINTS.reinstall} (re-runs the skill install)` }
|
|
121
168
|
)));
|
|
122
169
|
|
|
123
170
|
checks.push(guard('mcp', 'MCP registration', () => {
|
|
@@ -127,12 +174,37 @@ function runDoctorChecks(depsOverride = {}) {
|
|
|
127
174
|
const inCowork = !!(cowork && cowork.amicus);
|
|
128
175
|
// Primary signal: Claude Code MCP registration. Cowork/Desktop is reported as bonus only.
|
|
129
176
|
if (!inCode) {
|
|
130
|
-
return { id: 'mcp', name: 'MCP registration', status: 'warn', message: 'not registered in Claude Code', hint:
|
|
177
|
+
return { id: 'mcp', name: 'MCP registration', status: 'warn', message: 'not registered in Claude Code', hint: `${HINTS.reinstall} (or install the amicus plugin)` };
|
|
131
178
|
}
|
|
132
179
|
const extra = inCowork ? ', Cowork/Desktop' : '';
|
|
133
180
|
return { id: 'mcp', name: 'MCP registration', status: 'ok', message: `registered: Claude Code${extra}`, hint: null };
|
|
134
181
|
}));
|
|
135
182
|
|
|
183
|
+
// #43: OpenRouter credit/free-tier — warns (never errors); skipped when no key.
|
|
184
|
+
checks.push(await guardAsync('openrouter-credit', 'OpenRouter credit', async () => {
|
|
185
|
+
const values = d.readApiKeyValues() || {};
|
|
186
|
+
const key = values.openrouter;
|
|
187
|
+
if (!key) {
|
|
188
|
+
return { id: 'openrouter-credit', name: 'OpenRouter credit', status: 'ok', message: 'no OpenRouter key — skipped', hint: null };
|
|
189
|
+
}
|
|
190
|
+
// Reuses the #38 non-blocking probe; resolves warning:null on any failure.
|
|
191
|
+
const res = (await d.checkOpenRouterCredit(key)) || {};
|
|
192
|
+
if (res.warning) {
|
|
193
|
+
return { id: 'openrouter-credit', name: 'OpenRouter credit', status: 'warn', message: res.warning, hint: 'Add credit at openrouter.ai/credits, or build a free council (amicus setup → option 2).' };
|
|
194
|
+
}
|
|
195
|
+
const remaining = (typeof res.limitRemaining === 'number') ? ` ($${res.limitRemaining} remaining)` : '';
|
|
196
|
+
return { id: 'openrouter-credit', name: 'OpenRouter credit', status: 'ok', message: `credit ok${remaining}`, hint: null };
|
|
197
|
+
}));
|
|
198
|
+
|
|
199
|
+
// #43: project-root sanity — warns when cwd looks like an app/install dir or lacks project markers.
|
|
200
|
+
checks.push(guard('project-root', 'Project root', () => {
|
|
201
|
+
const dir = d.getCwd();
|
|
202
|
+
const markers = d.readProjectMarkers(dir);
|
|
203
|
+
const { assessProjectRoot } = require('./utils/project-root-sanity');
|
|
204
|
+
const r = assessProjectRoot(dir, markers);
|
|
205
|
+
return { id: 'project-root', name: 'Project root', status: r.status, message: r.message, hint: r.hint };
|
|
206
|
+
}));
|
|
207
|
+
|
|
136
208
|
return checks;
|
|
137
209
|
}
|
|
138
210
|
|
|
@@ -151,14 +223,18 @@ function renderHuman(checks) {
|
|
|
151
223
|
}
|
|
152
224
|
|
|
153
225
|
/**
|
|
154
|
-
* `amicus doctor [--json]`. Injectable `runChecks` for tests.
|
|
155
|
-
*
|
|
226
|
+
* `amicus doctor [--json] [--fix]`. Injectable `runChecks` for tests.
|
|
227
|
+
* `--fix` (#56) self-heals fixable checks in place (electron via repairElectron).
|
|
228
|
+
* @param {{_:string[], json?:boolean, fix?:boolean}} args
|
|
156
229
|
* @param {(deps?:object)=>Array} [runChecks]
|
|
157
230
|
* @returns {Promise<number>} exit code
|
|
158
231
|
*/
|
|
159
232
|
async function handleDoctor(args, runChecks = runDoctorChecks) {
|
|
160
233
|
const useJson = !!args.json;
|
|
161
|
-
|
|
234
|
+
// #56: --fix flows into runDoctorChecks as a dep so fixable checks (electron)
|
|
235
|
+
// self-heal in place. Omitted (not false) when absent so the injected
|
|
236
|
+
// test-double sees a clean "no fix" call.
|
|
237
|
+
const checks = await runChecks(args.fix ? { fix: true } : undefined);
|
|
162
238
|
if (useJson) {
|
|
163
239
|
const { buildDoctorDoc } = require('./utils/result-schema');
|
|
164
240
|
const VERSION = require('../package.json').version;
|
package/src/cli-handlers-run.js
CHANGED
|
@@ -173,6 +173,10 @@ async function handleFanout(args) {
|
|
|
173
173
|
contextTurns: args['context-turns'],
|
|
174
174
|
contextSince: args['context-since'],
|
|
175
175
|
contextMaxTokens: args['context-max-tokens'],
|
|
176
|
+
// #10: forward the Cowork parent so MCP-spawned fanout legs pin the right
|
|
177
|
+
// session (mirrors handleStart's coworkProcess plumbing). Without this the
|
|
178
|
+
// spawned `--cowork-process` flag is dropped and buildContext gets null.
|
|
179
|
+
coworkProcess: args['cowork-process'],
|
|
176
180
|
mcp: args.mcp,
|
|
177
181
|
mcpConfig: args['mcp-config'],
|
|
178
182
|
noMcp: args['no-mcp'],
|
package/src/cli.js
CHANGED
|
@@ -116,6 +116,7 @@ function isBooleanFlag(key) {
|
|
|
116
116
|
'no-ledger', // council tally: compute the record without appending to the reliability ledger
|
|
117
117
|
'html', // council report: emit a self-contained HTML page
|
|
118
118
|
'md', // council report: emit Markdown (default)
|
|
119
|
+
'fix', // doctor: self-heal fixable checks in place (#56)
|
|
119
120
|
];
|
|
120
121
|
return booleanFlags.includes(key);
|
|
121
122
|
}
|
|
@@ -438,6 +439,8 @@ Subcommands for 'council':
|
|
|
438
439
|
doctor: `
|
|
439
440
|
Options for 'doctor':
|
|
440
441
|
--json Machine-readable output
|
|
442
|
+
--fix Self-heal fixable checks in place (provisions the
|
|
443
|
+
Electron GUI binary; no global reinstall)
|
|
441
444
|
`,
|
|
442
445
|
setup: `
|
|
443
446
|
Options for 'setup':
|