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.
- package/.claude-plugin/marketplace.json +14 -0
- package/.claude-plugin/plugin.json +19 -0
- package/CHANGELOG.md +86 -0
- package/LICENSE +22 -1
- package/README.md +14 -3
- package/bin/amicus.js +17 -162
- package/electron/ipc-setup.js +30 -9
- package/electron/main.js +13 -5
- package/electron/preload.js +30 -10
- package/electron/setup-ui-keys.js +9 -0
- package/electron/setup-ui-model.js +33 -23
- package/electron/setup-ui-styles.js +6 -1
- package/electron/setup-ui.js +91 -38
- package/electron/toolbar.js +4 -5
- package/package.json +7 -5
- package/scripts/postinstall.js +16 -7
- package/skills/second-opinion/COUNCIL-DESIGN.md +36 -34
- package/skills/second-opinion/MODEL-NOTES.md +23 -17
- package/skills/second-opinion/SKILL.md +84 -51
- package/{skill → skills/sidecar}/SKILL.md +14 -4
- package/src/cli-handlers-council.js +59 -0
- package/src/cli-handlers-doctor.js +173 -0
- package/src/cli-handlers-run.js +196 -0
- package/src/cli-handlers.js +66 -1
- package/src/cli.js +16 -2
- package/src/council/findings.js +48 -0
- package/src/council/ledger.js +82 -0
- package/src/council/tally.js +108 -0
- package/src/council/verdict.js +48 -0
- package/src/headless.js +43 -149
- package/src/mcp-server.js +6 -0
- package/src/sidecar/budget.js +83 -0
- package/src/sidecar/conversation-mirror.js +128 -0
- package/src/sidecar/fanout-leg.js +4 -1
- package/src/sidecar/fanout.js +34 -7
- package/src/sidecar/interactive-mirror.js +66 -0
- package/src/sidecar/interactive.js +35 -21
- package/src/sidecar/models.js +41 -10
- package/src/sidecar/session-finalize.js +26 -0
- package/src/sidecar/session-utils.js +5 -5
- package/src/sidecar/setup.js +55 -42
- package/src/sidecar/start.js +19 -6
- package/src/utils/activity-poller.js +47 -0
- package/src/utils/alias-resolver.js +1 -1
- package/src/utils/config.js +4 -4
- package/src/utils/curated-models.js +88 -45
- package/src/utils/error-doc.js +55 -0
- package/src/utils/lifecycle.js +1 -1
- package/src/utils/model-catalog.js +1 -1
- package/src/utils/model-fetcher.js +16 -2
- package/src/utils/pricing.js +93 -0
- package/src/utils/quick-picks.js +81 -0
- package/src/utils/result-schema.js +21 -2
- package/src/utils/session-abort.js +40 -13
- 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
|

|
|
8
8
|
|
|
9
|
-
Hand Claude a
|
|
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
|
[](https://www.npmjs.com/package/amicus)
|
|
12
12
|
[](./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
|
-
| [
|
|
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
|
|
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,
|
|
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}`);
|
package/electron/ipc-setup.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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:
|
|
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(
|
|
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
|
}
|
package/electron/preload.js
CHANGED
|
@@ -7,16 +7,9 @@
|
|
|
7
7
|
|
|
8
8
|
const { contextBridge, ipcRenderer } = require('electron');
|
|
9
9
|
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
|
|
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
|
|