amicus 1.0.0 → 1.2.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.
Files changed (55) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/.claude-plugin/plugin.json +19 -0
  3. package/CHANGELOG.md +86 -0
  4. package/LICENSE +22 -1
  5. package/README.md +14 -3
  6. package/bin/amicus.js +17 -162
  7. package/electron/ipc-setup.js +30 -9
  8. package/electron/main.js +13 -5
  9. package/electron/preload.js +30 -10
  10. package/electron/setup-ui-keys.js +9 -0
  11. package/electron/setup-ui-model.js +33 -23
  12. package/electron/setup-ui-styles.js +6 -1
  13. package/electron/setup-ui.js +91 -38
  14. package/electron/toolbar.js +4 -5
  15. package/package.json +7 -5
  16. package/scripts/postinstall.js +16 -7
  17. package/skills/second-opinion/COUNCIL-DESIGN.md +36 -34
  18. package/skills/second-opinion/MODEL-NOTES.md +23 -17
  19. package/skills/second-opinion/SKILL.md +84 -51
  20. package/{skill → skills/sidecar}/SKILL.md +14 -4
  21. package/src/cli-handlers-council.js +59 -0
  22. package/src/cli-handlers-doctor.js +173 -0
  23. package/src/cli-handlers-run.js +196 -0
  24. package/src/cli-handlers.js +66 -1
  25. package/src/cli.js +16 -2
  26. package/src/council/findings.js +48 -0
  27. package/src/council/ledger.js +82 -0
  28. package/src/council/tally.js +108 -0
  29. package/src/council/verdict.js +48 -0
  30. package/src/headless.js +43 -149
  31. package/src/mcp-server.js +6 -0
  32. package/src/sidecar/budget.js +83 -0
  33. package/src/sidecar/conversation-mirror.js +128 -0
  34. package/src/sidecar/fanout-leg.js +4 -1
  35. package/src/sidecar/fanout.js +34 -7
  36. package/src/sidecar/interactive-mirror.js +66 -0
  37. package/src/sidecar/interactive.js +35 -21
  38. package/src/sidecar/models.js +41 -10
  39. package/src/sidecar/session-finalize.js +26 -0
  40. package/src/sidecar/session-utils.js +5 -5
  41. package/src/sidecar/setup.js +55 -42
  42. package/src/sidecar/start.js +19 -6
  43. package/src/utils/activity-poller.js +47 -0
  44. package/src/utils/alias-resolver.js +1 -1
  45. package/src/utils/config.js +4 -4
  46. package/src/utils/curated-models.js +88 -45
  47. package/src/utils/error-doc.js +55 -0
  48. package/src/utils/lifecycle.js +1 -1
  49. package/src/utils/model-catalog.js +1 -1
  50. package/src/utils/model-fetcher.js +16 -2
  51. package/src/utils/pricing.js +93 -0
  52. package/src/utils/quick-picks.js +81 -0
  53. package/src/utils/result-schema.js +21 -2
  54. package/src/utils/session-abort.js +40 -13
  55. package/src/utils/validators.js +17 -17
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "bourbondog-amicus",
3
+ "owner": { "name": "Christian Wagner", "url": "https://github.com/BourbonDog" },
4
+ "metadata": { "description": "Amicus — multi-model LLM Council + parallel AI window for Claude Code." },
5
+ "plugins": [
6
+ {
7
+ "name": "amicus",
8
+ "source": "./",
9
+ "description": "Multi-model LLM Council + parallel AI window. Run structured council reviews across Gemini, GPT, DeepSeek and more — or fork a conversation to any model and fold the results back.",
10
+ "author": { "name": "Christian Wagner" },
11
+ "keywords": ["claude-code", "multi-model", "llm", "council", "second-opinion"]
12
+ }
13
+ ]
14
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "amicus",
3
+ "version": "1.2.0",
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": { "name": "Christian Wagner" },
6
+ "homepage": "https://bourbondog.github.io/amicus/",
7
+ "repository": "https://github.com/BourbonDog/amicus",
8
+ "bugs": "https://github.com/BourbonDog/amicus/issues",
9
+ "license": "MIT",
10
+ "keywords": ["claude-code", "multi-model", "llm", "council", "second-opinion", "sidecar", "gemini", "gpt", "deepseek"],
11
+ "skills": ["./skills/sidecar", "./skills/second-opinion"],
12
+ "mcpServers": {
13
+ "amicus": {
14
+ "command": "npx",
15
+ "args": ["-y", "amicus@latest", "mcp"],
16
+ "env": { "AMICUS_SKIP_POSTINSTALL": "1" }
17
+ }
18
+ }
19
+ }
package/CHANGELOG.md CHANGED
@@ -5,6 +5,92 @@ All notable changes to Amicus are documented here. Format follows
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.2.0] - 2026-06-24
9
+
10
+ A post-launch enhancement program: reliability and cost made real, the council's
11
+ trust machinery turned from hand-math into deterministic code, plus first-run
12
+ diagnostics, a Claude Code plugin, and an observable interactive surface.
13
+
14
+ ### Added
15
+ - **`amicus doctor`**: a one-screen first-run health check — configured providers, default-model
16
+ resolution vs. the live catalog, catalog freshness, the OpenCode binary, Electron, installed
17
+ skills, and MCP registration. Each red line carries the exact fix command; `--json` lets skills
18
+ self-diagnose.
19
+ - **Claude Code plugin**: Amicus is now installable from the marketplace —
20
+ `/plugin marketplace add BourbonDog/amicus` then `/plugin install amicus`. The plugin ships both
21
+ skills and the MCP server; npm stays the engine/CLI. (The plugin channel skips the global
22
+ postinstall via `AMICUS_SKIP_POSTINSTALL` so it can't double-register.)
23
+ - **Per-leg cost & token telemetry**: the run/wave schema (now `schemaVersion: 2`) carries a
24
+ `usage` block — input/output/reasoning tokens and a `$` cost tagged by source (reported >
25
+ estimated > unknown). Surfaced in `fanout --json` and council run-stats.
26
+ - **Enforced budget gate**: a per-`$/Mtok` threshold (on by default — blocks o3-pro-class models
27
+ before a wave launches) plus an optional `--max-cost` total ceiling. `--no-cost-gate` is the
28
+ explicit escape hatch.
29
+ - **`amicus council tally|stats`**: deterministic council scoring — a structured findings
30
+ contract, a peers-only tier cascade with self-vote-corrected street-cred, a compounding
31
+ reviewer-reliability ledger, and a machine-readable `verdict.json`. The council stays a skill;
32
+ the engine owns only the arithmetic and schemas.
33
+ - **Structured `--json` error envelope**: pre-flight failures now emit a typed
34
+ `{ ok: false, error: { code, message, hint } }` document on stdout (stable codes like
35
+ `MISSING_KEY`, `BAD_MODEL`, `BUDGET_EXCEEDED`) instead of bare text on stderr.
36
+
37
+ ### Changed
38
+ - **Interactive GUI sessions now persist live**: `conversation.jsonl` and `progress.json` are
39
+ written as the session runs, so the CLI heartbeat, `amicus status`, and
40
+ `amicus read --conversation` work for GUI sessions — and **closing the window without folding no
41
+ longer loses the transcript**. Interactive runs also record token/cost usage. (Headless and
42
+ interactive now share one persistence transform.)
43
+ - **Reliability**: a single source of truth for terminal state (exit code and `metadata.status`
44
+ always agree; the idle backstop no longer exits 0 with `running` metadata), and an
45
+ activity-driven interactive watchdog that won't kill an actively-working-but-quiet GUI session.
46
+ - **CI**: a real matrix (Ubuntu / Windows / macOS × Node 18 / 20 / 22) plus lint, secret-scan, and
47
+ size-gate now gate every push and the publish.
48
+ - Repo layout: the chat skill moved to `skills/sidecar/` (both skills live under `skills/`); npm
49
+ `homepage` now points at the live site; README and the landing page gained a "Prerequisites &
50
+ cost" section.
51
+
52
+ ### Fixed
53
+ - **MCP stderr fd leak**: `spawnSidecarProcess` opened a `debug.log` descriptor for the child's
54
+ stderr but never closed the parent's copy — a descriptor leak that, on Windows, also held the
55
+ file open and blocked session-dir cleanup.
56
+ - Platform-correct missing-key guidance (PowerShell `$PROFILE`/`setx` on Windows; leads with
57
+ `amicus key`); the committed-secret scan now knows all five providers; `amicus models` marks
58
+ your **actual** aliases (not curated defaults); OpenRouter's `-1` "variable pricing" sentinel
59
+ renders as `—` instead of a nonsense negative price.
60
+
61
+ ## [1.1.0] - 2026-06-11
62
+
63
+ ### Added
64
+ - **DeepSeek as a direct API provider**: DeepSeek card and API key step in the setup wizard,
65
+ live model fetch from DeepSeek's `/models`, and a direct `deepseek/...` route used
66
+ automatically when no OpenRouter key is configured.
67
+ - **`amicus key`**: headless API key management — `amicus key` lists configured providers with
68
+ masked hints, `amicus key <provider> <key>` validates and saves, `--remove` deletes. No GUI
69
+ required.
70
+ - **Live quick picks in the setup wizard (Step 2)**: recommended models resolve per family
71
+ against the live catalog when the window opens (no stale pinned ids), with always-visible
72
+ labeled search and a write-preview showing exactly which alias will change.
73
+
74
+ ### Changed
75
+ - **Setup wizard finish is now read-modify-write**: picking a model sets the default and
76
+ upgrades only that one alias; untouched aliases are never rewritten and deleted aliases stay
77
+ deleted. (Previously, finishing setup could silently rewrite every card alias.)
78
+ - Readline (no-Electron) setup parity: free-form model ids and the same no-clobber behavior.
79
+ `amicus models --check` now also warns when a curated pinned fallback drifts from the live
80
+ catalog.
81
+ - Council skill (Stage 6): the proposed MODEL-NOTES diff is written to a run-folder file and
82
+ the approval prompt carries the file path — approval dialogs can hide chat text.
83
+ - Chat skill docs: single-model sidecars default to interactive (GUI) mode; headless remains
84
+ the default for fanouts and bulk runs.
85
+ - Attribution: npm package author is Christian Wagner; "Inspired by" fork wording in
86
+ CONTRIBUTING.
87
+
88
+ ### Fixed
89
+ - **Electron preload crash on every page**: `window.sidecar` (contextBridge) is now exposed
90
+ before DOM injection, and the injected CSS guards against a null `documentElement` — the
91
+ silent TypeError previously killed both the bridge and the anti-white-flash styling.
92
+ - DeepSeek provider pill showed `undefined` in the wizard model step.
93
+
8
94
  ## [1.0.0] - 2026-06-10
9
95
 
10
96
  Everything since the fork from upstream `claude-sidecar` v0.5.2 — the Amicus launch line.
package/LICENSE CHANGED
@@ -1,6 +1,27 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 John Renaldi
3
+ Copyright (c) 2025 John Renaldi Claude Sidecar
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
4
25
 
5
26
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
27
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  ![Amicus: an LLM Council and a parallel AI window for Claude](./docs/hero.png)
8
8
 
9
- Hand Claude a document and say *council review this*: Amicus routes it through several models from different families, has them anonymously cross-review each other, and a non-Claude chair synthesizes a verdict you turn into accept/deny edits. Or skip the ceremony and **fork** a single conversation to Gemini, GPT, DeepSeek, or any other model — it works in parallel with full context, and you **fold** the result back when you're ready. Claude orchestrates throughout; you stay in your editor.
9
+ Hand Claude a plan, a design, a diff, an architecture decision, a manuscript — anything — and say *council review this*: Amicus routes it through several models from different families, has them anonymously cross-review each other, and a non-Claude chair synthesizes a verdict you turn into accept/deny edits. Or skip the ceremony and **fork** a single conversation to Gemini, GPT, DeepSeek, or any other model — it works in parallel with full context, and you **fold** the result back when you're ready. Claude orchestrates throughout; you stay in your editor.
10
10
 
11
11
  [![npm version](https://img.shields.io/npm/v/amicus?color=D97757&labelColor=1A1C29)](https://www.npmjs.com/package/amicus)
12
12
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue?labelColor=1A1C29)](./LICENSE)
@@ -67,6 +67,16 @@ The postinstall step auto-configures everything — no manual registration:
67
67
  - Registers the **MCP server** in Claude Code and in Claude Desktop / Cowork, so the Amicus tools appear natively.
68
68
  - Installs **both skills** into `~/.claude/skills/` — `second-opinion` (the council) and `sidecar` (the chat skill).
69
69
 
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
+
70
80
  **Configure:**
71
81
 
72
82
  ```bash
@@ -420,6 +430,7 @@ Most Claude-adjacent tooling assumes macOS/Linux; Amicus doesn't.
420
430
  | Symptom | Likely cause | Fix |
421
431
  |---------|--------------|-----|
422
432
  | "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) |
433
+ | `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). |
423
434
  | `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. |
424
435
  | Session not found | No session matches the given ID | Run `amicus list`, or omit `--session-id` to use the most recent. |
425
436
  | No conversation history found | Project-path encoding | Check `~/.claude/projects/`; `/` and `_` in the project path are encoded as `-` in the directory name. |
@@ -451,7 +462,7 @@ LOG_LEVEL=debug amicus start --model gemini --prompt "test" --no-ui
451
462
  | [docs/publishing.md](./docs/publishing.md) | Release and publish process. |
452
463
  | [docs/SHIMS.md](./docs/SHIMS.md) | Legacy `SIDECAR_*` → `AMICUS_*` compatibility shims. |
453
464
  | [skills/second-opinion/SKILL.md](./skills/second-opinion/SKILL.md) | The LLM Council skill. |
454
- | [skill/SKILL.md](./skill/SKILL.md) | The `sidecar` chat skill. |
465
+ | [skills/sidecar/SKILL.md](./skills/sidecar/SKILL.md) | The `sidecar` chat skill. |
455
466
  | [evals/README.md](./evals/README.md) | End-to-end eval harness for LLM interactions. |
456
467
 
457
468
  ---
@@ -472,6 +483,6 @@ Amicus is a harness built on top of [**OpenCode**](https://opencode.ai), the ope
472
483
 
473
484
  ## Attribution & License
474
485
 
475
- Amicus is an independent fork of [**Claude Sidecar**](https://github.com/jrenaldi79/sidecar) by [John Renaldi](https://github.com/jrenaldi79), used under the MIT License. The original copyright (© 2025 John Renaldi) is preserved in full in [LICENSE](./LICENSE). The engine modifications, the multi-model council, and the skill bundling are © 2026 BourbonDog, also under the MIT License. See [LICENSE](./LICENSE) and [NOTICE](./NOTICE) for the complete attribution.
486
+ Amicus is inspired by [**Claude Sidecar**](https://github.com/jrenaldi79/sidecar) by [John Renaldi](https://github.com/jrenaldi79), used under the MIT License. The original copyright (© 2025 John Renaldi) is preserved in full in [LICENSE](./LICENSE). The engine modifications, the multi-model council, and the skill bundling are © 2026 Christian Wagner, also under the MIT License. See [LICENSE](./LICENSE) and [NOTICE](./NOTICE) for the complete attribution.
476
487
 
477
488
  **MIT.**
package/bin/amicus.js CHANGED
@@ -11,10 +11,11 @@
11
11
  const { loadCredentials } = require('../src/utils/env-loader');
12
12
  loadCredentials();
13
13
 
14
- const { parseArgs, validateStartArgs, getUsage } = require('../src/cli');
14
+ const { parseArgs, getUsage } = require('../src/cli');
15
15
  const { validateTaskId } = require('../src/utils/validators');
16
16
  const { resolveModelFromArgs, validateFallbackModel } = require('../src/utils/start-helpers');
17
- const { handleSetup, handleAbort, handleUpdate, handleMcp } = require('../src/cli-handlers');
17
+ const { handleSetup, handleAbort, handleUpdate, handleMcp, handleKey } = require('../src/cli-handlers');
18
+ const { handleStart, handleFanout, handleRead } = require('../src/cli-handlers-run');
18
19
  const { isOneShotCommand, armExitWatchdog } = require('../src/utils/lifecycle');
19
20
  const { logger } = require('../src/utils/logger');
20
21
 
@@ -73,7 +74,7 @@ async function main() {
73
74
  try {
74
75
  switch (command) {
75
76
  case 'start':
76
- await handleStart(args);
77
+ exitCode = await handleStart(args);
77
78
  break;
78
79
  case 'fanout':
79
80
  exitCode = await handleFanout(args);
@@ -95,9 +96,22 @@ async function main() {
95
96
  exitCode = await handleModels(args);
96
97
  break;
97
98
  }
99
+ case 'council': {
100
+ const { handleCouncil } = require('../src/cli-handlers-council');
101
+ exitCode = await handleCouncil(args);
102
+ break;
103
+ }
104
+ case 'doctor': {
105
+ const { handleDoctor } = require('../src/cli-handlers-doctor');
106
+ exitCode = await handleDoctor(args);
107
+ break;
108
+ }
98
109
  case 'setup':
99
110
  await handleSetup(args);
100
111
  break;
112
+ case 'key':
113
+ await handleKey(args);
114
+ break;
101
115
  case 'abort':
102
116
  await handleAbort(args);
103
117
  break;
@@ -126,135 +140,6 @@ async function main() {
126
140
  }
127
141
  }
128
142
 
129
- /**
130
- * Handle 'sidecar start' command
131
- * Spec Reference: §4.1
132
- */
133
- async function handleStart(args) {
134
- // F4: --prompt-file support (XOR --prompt) and --json gating
135
- if (args.prompt !== undefined || args['prompt-file'] !== undefined) {
136
- const { resolvePromptSource } = require('../src/utils/prompt-source');
137
- const promptRes = resolvePromptSource(args);
138
- if (promptRes.error) {
139
- console.error(promptRes.error);
140
- process.exit(1);
141
- }
142
- args.prompt = promptRes.prompt;
143
- }
144
- if (args.json && !args['no-ui']) {
145
- console.error('Error: --json requires --no-ui');
146
- process.exit(1);
147
- }
148
-
149
- const { model, alias } = resolveModelFromArgs(args);
150
- args.model = model;
151
- args.model = await validateFallbackModel(args, alias);
152
-
153
- // Normalize agent: --agent takes precedence, otherwise use --mode
154
- args.agent = args.agent || args.mode;
155
-
156
- const validation = validateStartArgs(args);
157
- if (!validation.valid) {
158
- console.error(validation.error);
159
- process.exit(1);
160
- }
161
-
162
- const { startSidecar } = require('../src/index');
163
-
164
- await startSidecar({
165
- taskId: args['task-id'],
166
- model: args.model,
167
- prompt: args.prompt,
168
- sessionId: args['session-id'],
169
- cwd: args.cwd,
170
- contextTurns: args['context-turns'],
171
- contextSince: args['context-since'],
172
- contextMaxTokens: args['context-max-tokens'],
173
- noUi: args['no-ui'],
174
- timeout: args.timeout,
175
- agent: args.agent,
176
- mcp: args.mcp,
177
- mcpConfig: args['mcp-config'],
178
- thinking: args.thinking,
179
- summaryLength: args['summary-length'],
180
- client: args.client,
181
- sessionDir: args['session-dir'],
182
- foldShortcut: args['fold-shortcut'],
183
- opencodePort: args['opencode-port'],
184
- noMcp: args['no-mcp'],
185
- excludeMcp: args['exclude-mcp'],
186
- coworkProcess: args['cowork-process'],
187
- position: args.position,
188
- json: !!args.json,
189
- modelInput: alias || null,
190
- });
191
- }
192
-
193
- /**
194
- * Handle 'amicus fanout' command (F4).
195
- * Returns the wave exit code: 0 all complete, 2 partial, 1 none/hard failure,
196
- * 130/143 when the wave was signal-aborted.
197
- */
198
- async function handleFanout(args) {
199
- const { resolvePromptSource } = require('../src/utils/prompt-source');
200
- const promptRes = resolvePromptSource(args);
201
- if (promptRes.error) {
202
- console.error(promptRes.error);
203
- process.exit(1);
204
- }
205
- if (typeof args.models !== 'string' || !args.models.trim()) {
206
- console.error('Error: --models is required (comma-separated aliases or provider/model IDs)');
207
- process.exit(1);
208
- }
209
- if (args['wave-id']) {
210
- const check = validateTaskId(String(args['wave-id']));
211
- if (!check.valid) {
212
- console.error(check.error);
213
- process.exit(1);
214
- }
215
- }
216
- if (args.agent && String(args.agent).toLowerCase() === 'chat') {
217
- console.error('Error: --agent chat is interactive-only; fanout is headless');
218
- process.exit(1);
219
- }
220
- if (args.timeout !== undefined && args.timeout <= 0) {
221
- console.error('Error: --timeout must be a positive number');
222
- process.exit(1);
223
- }
224
- const { parseModelsList } = require('../src/sidecar/fanout');
225
- if (parseModelsList(args.models).length === 0) {
226
- console.error('Error: --models must contain at least one non-empty entry');
227
- process.exit(1);
228
- }
229
-
230
- // Direct require — the src/index.js public re-export is added later (Task 13)
231
- const { runFanout } = require('../src/sidecar/fanout');
232
- const { exitCode } = await runFanout({
233
- models: args.models,
234
- prompt: promptRes.prompt,
235
- promptMeta: promptRes.promptMeta,
236
- waveId: args['wave-id'],
237
- project: args.cwd || process.cwd(),
238
- agent: args.agent || args.mode,
239
- thinking: args.thinking,
240
- timeout: args.timeout,
241
- summaryLength: args['summary-length'],
242
- includeContext: !args['no-context'],
243
- sessionId: args['session-id'],
244
- contextTurns: args['context-turns'],
245
- contextSince: args['context-since'],
246
- contextMaxTokens: args['context-max-tokens'],
247
- mcp: args.mcp,
248
- mcpConfig: args['mcp-config'],
249
- noMcp: args['no-mcp'],
250
- excludeMcp: args['exclude-mcp'],
251
- noValidateModel: args['no-validate-model'],
252
- json: !!args.json,
253
- client: args.client,
254
- });
255
- return exitCode;
256
- }
257
-
258
143
  /**
259
144
  * Handle 'sidecar list' command
260
145
  * Spec Reference: §4.2
@@ -345,36 +230,6 @@ async function handleContinue(args) {
345
230
  });
346
231
  }
347
232
 
348
- /**
349
- * Handle 'sidecar read' command
350
- * Spec Reference: §4.5
351
- */
352
- async function handleRead(args) {
353
- const taskId = args._[1];
354
-
355
- if (!taskId) {
356
- console.error('Error: task_id is required for read');
357
- console.error('Usage: sidecar read <task_id> [--summary|--conversation]');
358
- process.exit(1);
359
- }
360
-
361
- const taskIdCheck = validateTaskId(taskId);
362
- if (!taskIdCheck.valid) {
363
- console.error(taskIdCheck.error);
364
- process.exit(1);
365
- }
366
-
367
- const { readSidecar } = require('../src/index');
368
-
369
- await readSidecar({
370
- taskId,
371
- conversation: args.conversation,
372
- metadata: args.metadata,
373
- json: args.json,
374
- project: args.cwd
375
- });
376
- }
377
-
378
233
  // Run main
379
234
  main().catch(err => {
380
235
  console.error(`Fatal error: ${err.message}`);
@@ -7,14 +7,14 @@
7
7
  * fetch-models, get-catalog, and refresh-catalog.
8
8
  */
9
9
 
10
+ const { ipcMain } = require('electron');
10
11
  const { logger } = require('../src/utils/logger');
11
12
 
12
13
  /**
13
14
  * Register all setup-related IPC handlers
14
- * @param {Electron.IpcMain} ipcMain - Electron IPC main
15
15
  * @param {function} getMainWindow - Returns the current main BrowserWindow
16
16
  */
17
- function registerSetupHandlers(ipcMain, getMainWindow) {
17
+ function registerSetupHandlers(getMainWindow) {
18
18
  ipcMain.handle('sidecar:validate-key', async (_event, provider, key) => {
19
19
  try {
20
20
  const { validateApiKey } = require('../src/utils/api-key-store');
@@ -95,14 +95,35 @@ function registerSetupHandlers(ipcMain, getMainWindow) {
95
95
  }
96
96
  });
97
97
 
98
- ipcMain.handle('sidecar:save-config', (_event, defaultModel, aliasOverrides) => {
99
- const { saveConfig, getDefaultAliases } = require('../src/utils/config');
100
- const aliases = getDefaultAliases();
101
- if (aliasOverrides && typeof aliasOverrides === 'object') {
102
- Object.assign(aliases, aliasOverrides);
98
+ // Read-modify-write: never rewrite an alias the renderer didn't send.
99
+ // aliasWrites values: string = set, null = delete. First run seeds live.
100
+ ipcMain.handle('sidecar:save-config', async (_event, defaultModel, aliasWrites) => {
101
+ try {
102
+ const { loadConfig, saveConfig } = require('../src/utils/config');
103
+ let cfg = loadConfig();
104
+ if (!cfg) {
105
+ const { toLiveSeedAliases } = require('../src/utils/quick-picks');
106
+ let catalog = [];
107
+ try {
108
+ catalog = await require('../src/utils/model-catalog').getCatalog();
109
+ } catch (_err) { /* offline: pinned seeds */ }
110
+ cfg = { aliases: toLiveSeedAliases(catalog) };
111
+ }
112
+ if (!cfg.aliases) { cfg.aliases = {}; }
113
+ if (defaultModel) { cfg.default = defaultModel; }
114
+ if (aliasWrites && typeof aliasWrites === 'object') {
115
+ for (const [alias, model] of Object.entries(aliasWrites)) {
116
+ if (model === null) { delete cfg.aliases[alias]; }
117
+ // empty string: ignore (use null to delete)
118
+ else if (typeof model === 'string' && model) { cfg.aliases[alias] = model; }
119
+ }
120
+ }
121
+ saveConfig(cfg);
122
+ return { success: true };
123
+ } catch (err) {
124
+ logger.error('save-config handler error', { error: err.message });
125
+ throw err; // renderer invoke() rejects; its catch re-enables Finish
103
126
  }
104
- saveConfig({ default: defaultModel, aliases });
105
- return { success: true };
106
127
  });
107
128
 
108
129
  ipcMain.handle('sidecar:get-config', () => {
package/electron/main.js CHANGED
@@ -93,7 +93,7 @@ function createAmicusWindow() {
93
93
  x: winX, y: winY,
94
94
  show: false,
95
95
  frame: true, backgroundColor: '#2D2B2A',
96
- title: CLIENT === 'cowork' ? 'Openwork Amicus' : 'Amicus',
96
+ title: 'Amicus',
97
97
  icon: ICON_PATH,
98
98
  webPreferences: {
99
99
  preload: path.join(__dirname, 'preload.js'),
@@ -270,9 +270,17 @@ function createAmicusWindow() {
270
270
  // Setup Window (API Key Form)
271
271
  // ============================================================================
272
272
 
273
- function createSetupWindow() {
273
+ async function createSetupWindow() {
274
274
  // Lazy-load setup UI to avoid loading it for sidecar mode
275
275
  const { buildSetupHTML } = require('./setup-ui');
276
+ const { resolveQuickPicks } = require('../src/utils/quick-picks');
277
+ let quickPicks;
278
+ try {
279
+ const catalog = await require('../src/utils/model-catalog').getCatalog();
280
+ quickPicks = resolveQuickPicks(catalog);
281
+ } catch (_err) {
282
+ quickPicks = undefined; // buildSetupHTML falls back to pinned
283
+ }
276
284
 
277
285
  mainWindow = new BrowserWindow({
278
286
  width: 560, height: 680, minWidth: 480, minHeight: 580,
@@ -286,7 +294,7 @@ function createSetupWindow() {
286
294
  }
287
295
  });
288
296
 
289
- const html = buildSetupHTML({ client: CLIENT });
297
+ const html = buildSetupHTML({ client: CLIENT, quickPicks });
290
298
  mainWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`);
291
299
  mainWindow.webContents.on('page-title-updated', (e) => e.preventDefault());
292
300
 
@@ -417,7 +425,7 @@ ipcMain.handle('sidecar:resize-toolbar', (_event, height) => {
417
425
  });
418
426
 
419
427
  // Setup mode: all setup IPC handlers (extracted to ipc-setup.js)
420
- registerSetupHandlers(ipcMain, () => mainWindow);
428
+ registerSetupHandlers(() => mainWindow);
421
429
 
422
430
  // ============================================================================
423
431
  // Settings Child Window (opened from sidecar toolbar gear button)
@@ -456,7 +464,7 @@ app.whenReady().then(() => {
456
464
  }
457
465
 
458
466
  if (MODE === 'setup') {
459
- createSetupWindow();
467
+ createSetupWindow().catch((err) => { logger.error('createSetupWindow failed', err); });
460
468
  } else {
461
469
  createAmicusWindow();
462
470
  }
@@ -7,16 +7,9 @@
7
7
 
8
8
  const { contextBridge, ipcRenderer } = require('electron');
9
9
 
10
- // Inject CSS before page scripts run to hide OpenCode branding
11
- // and match window background color to prevent white flash on load
12
- const style = document.createElement('style');
13
- style.textContent = [
14
- 'html, body { background-color: #2D2B2A !important; }',
15
- '#root > div > header { display: none !important; }',
16
- 'svg[viewBox="0 0 234 42"] { display: none !important; }',
17
- ].join('\n');
18
- document.documentElement.appendChild(style);
19
-
10
+ // Bridge exposure needs no DOM and must run before any cosmetic step:
11
+ // the CSS injection below once threw at preload-evaluation time and killed
12
+ // the script before the bridge was exposed.
20
13
  contextBridge.exposeInMainWorld('sidecar', {
21
14
  /** Trigger fold: summarize and return to Claude Code */
22
15
  fold: () => ipcRenderer.invoke('sidecar:fold'),
@@ -31,3 +24,30 @@ contextBridge.exposeInMainWorld('sidecar', {
31
24
  /** Notify main process to resize toolbar area */
32
25
  resizeToolbar: (height) => ipcRenderer.invoke('sidecar:resize-toolbar', height),
33
26
  });
27
+
28
+ /**
29
+ * Inject CSS to hide OpenCode branding and match the window background color
30
+ * to prevent a white flash on load. Cosmetic only — must never throw, or it
31
+ * would kill the rest of the preload.
32
+ */
33
+ function injectBrandingCss() {
34
+ try {
35
+ const style = document.createElement('style');
36
+ style.textContent = [
37
+ 'html, body { background-color: #2D2B2A !important; }',
38
+ '#root > div > header { display: none !important; }',
39
+ 'svg[viewBox="0 0 234 42"] { display: none !important; }',
40
+ ].join('\n');
41
+ document.documentElement.appendChild(style);
42
+ } catch {
43
+ // cosmetic — a failed injection must not break the preload
44
+ }
45
+ }
46
+
47
+ // documentElement is still null while the preload evaluates; defer until the
48
+ // DOM exists so the injection cannot null-deref.
49
+ if (document.documentElement) {
50
+ injectBrandingCss();
51
+ } else {
52
+ document.addEventListener('DOMContentLoaded', injectBrandingCss);
53
+ }
@@ -43,6 +43,15 @@ const PROVIDERS = [
43
43
  helpUrl: 'https://console.anthropic.com/settings/keys',
44
44
  helpLabel: 'console.anthropic.com/settings/keys',
45
45
  recommended: false
46
+ },
47
+ {
48
+ id: 'deepseek',
49
+ name: 'DeepSeek',
50
+ description: 'Direct access to DeepSeek-V3 and DeepSeek-R1 models',
51
+ placeholder: 'sk-...',
52
+ helpUrl: 'https://platform.deepseek.com/api_keys',
53
+ helpLabel: 'platform.deepseek.com/api_keys',
54
+ recommended: false
46
55
  }
47
56
  ];
48
57