amicus 3.1.0 → 3.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/plugin.json +1 -1
- package/CHANGELOG.md +44 -0
- package/README.md +2 -2
- package/electron/ipc-setup.js +33 -0
- package/electron/preload-setup.js +2 -1
- package/electron/setup-ui-aliases.js +1 -1
- package/electron/setup-ui-keys-script.js +8 -1
- package/electron/setup-ui-provider-default.js +101 -0
- package/electron/setup-ui-styles.js +13 -1
- package/electron/setup-ui.js +5 -1
- package/package.json +2 -1
- package/src/cli-handlers-run.js +9 -1
- package/src/cli-handlers.js +40 -0
- package/src/cli.js +4 -0
- package/src/sidecar/models.js +39 -17
- package/src/sidecar/setup.js +80 -11
- package/src/utils/config.js +59 -0
- package/src/utils/curated-models.js +69 -5
- package/src/utils/gateway-route-audit.js +103 -0
- package/src/utils/gateway-route-catalog.js +92 -0
- package/src/utils/gateway-router.js +21 -5
- package/src/utils/model-fetcher.js +11 -5
- package/src/utils/model-tiers.js +120 -0
- package/src/utils/provider-default-picker.js +234 -0
- package/src/utils/provider-default-prompt.js +118 -0
- package/src/utils/result-schema.js +9 -2
- package/src/utils/route-error.js +26 -9
- package/src/utils/route-launch.js +147 -27
- package/src/utils/start-helpers.js +53 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amicus",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.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": {
|
|
6
6
|
"name": "Christian Wagner"
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,50 @@ All notable changes to Amicus are documented here. Format follows
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [3.2.0] - 2026-07-16
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Cost-aware per-provider default model picker at key-add.** Adding a provider API key (`amicus key
|
|
13
|
+
<provider>`, the setup wizard, or the Electron key step) now offers a picker that pre-selects a
|
|
14
|
+
**balanced**-tier model instead of the priciest flagship, shows live `$/M` input pricing, and writes
|
|
15
|
+
your choice as a vendor-named alias (e.g. `--model anthropic`) — seeding your overall default
|
|
16
|
+
(`config.default`) on your first key. Applies to direct model vendors (OpenAI, Anthropic, Google,
|
|
17
|
+
DeepSeek).
|
|
18
|
+
- **`routing.tier` preference** (`frontier` | `balanced` | `economy`, default `balanced`) biasing the
|
|
19
|
+
picker's per-vendor pre-selection.
|
|
20
|
+
- A one-time onboarding tip on `amicus start` pointing existing users to the new picker.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Model aliases now resolve their per-gateway executable ids **by model**, so user-defined and
|
|
25
|
+
vendor-named aliases route correctly across the direct and OpenRouter gateways (extends the v3.1.1
|
|
26
|
+
gateway-correct-ids fix beyond the curated defaults). An alias whose value is an explicit
|
|
27
|
+
`openrouter/…` id still forces OpenRouter.
|
|
28
|
+
|
|
29
|
+
## [3.1.1] - 2026-07-16
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **Anthropic model aliases now route correctly for direct-Anthropic-key users.** `--model opus` /
|
|
34
|
+
`haiku` / `claude` / `sonnet` previously resolved to OpenRouter's dot-form id (e.g.
|
|
35
|
+
`anthropic/claude-opus-4.8`), which the direct Anthropic API rejects with `model_not_found` (it uses
|
|
36
|
+
dashes/date suffixes: `claude-opus-4-8`, `claude-haiku-4-5-20251001`). Aliases now carry per-gateway
|
|
37
|
+
executable ids and the router emits the selected gateway's native id. OpenRouter-only users were
|
|
38
|
+
unaffected.
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- `--model claude` / `--model sonnet` default target moves from Claude Sonnet 4.6 to **Claude Sonnet
|
|
43
|
+
5**; the offline model floor was refreshed to the current Anthropic family.
|
|
44
|
+
- Availability-aware routing: a model not served on the selected gateway (e.g. Fable, which is
|
|
45
|
+
OpenRouter-only) routes to the gateway that has it, or errors clearly under an explicit `--gateway`.
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- `amicus models --check --strict` exits non-zero on curated default-alias drift; a scheduled
|
|
50
|
+
`model-drift` CI workflow audits the per-gateway ids against the live (keyless) OpenRouter catalog.
|
|
51
|
+
|
|
8
52
|
## [3.1.0] - 2026-07-15
|
|
9
53
|
|
|
10
54
|
### Added
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
**A multi-model LLM Council for Claude — with a parallel AI window underneath.**
|
|
6
6
|
|
|
7
|
-

|
|
8
8
|
|
|
9
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
|
|
|
@@ -319,7 +319,7 @@ $ amicus status demo123 --json
|
|
|
319
319
|
"taskId": "demo123",
|
|
320
320
|
"status": "complete",
|
|
321
321
|
"elapsed": "5m 0s",
|
|
322
|
-
"version": "3.
|
|
322
|
+
"version": "3.2.0",
|
|
323
323
|
"model": "google/gemini-2.5-flash",
|
|
324
324
|
"phase": "terminal"
|
|
325
325
|
}
|
package/electron/ipc-setup.js
CHANGED
|
@@ -42,6 +42,26 @@ function registerSetupHandlers(getMainWindow) {
|
|
|
42
42
|
require('../src/utils/model-catalog').refreshCatalog().catch(() => {});
|
|
43
43
|
} catch { /* best-effort */ }
|
|
44
44
|
});
|
|
45
|
+
// Task 8: per-provider default picker choices for the key step.
|
|
46
|
+
// Per-provider defaults only make sense for DIRECT model vendors --
|
|
47
|
+
// openrouter is the GATEWAY, not a vendor, so it's skipped entirely
|
|
48
|
+
// (mirrors provider-default-prompt.js's runProviderDefaultFlow gate;
|
|
49
|
+
// this path calls the picker core directly instead of that
|
|
50
|
+
// readline-oriented helper, so it re-checks isDirectProvider itself).
|
|
51
|
+
const { isDirectProvider } = require('../src/utils/provider-registry');
|
|
52
|
+
if (isDirectProvider(provider)) {
|
|
53
|
+
try {
|
|
54
|
+
const { getCatalog } = require('../src/utils/model-catalog');
|
|
55
|
+
const { buildProviderDefaultChoices } = require('../src/utils/provider-default-picker');
|
|
56
|
+
const catalog = await getCatalog();
|
|
57
|
+
result.providerDefault = buildProviderDefaultChoices(provider, { catalog });
|
|
58
|
+
} catch (err) {
|
|
59
|
+
logger.error('save-key providerDefault error', { error: err.message });
|
|
60
|
+
result.providerDefault = null;
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
result.providerDefault = null;
|
|
64
|
+
}
|
|
45
65
|
}
|
|
46
66
|
return result;
|
|
47
67
|
} catch (err) {
|
|
@@ -50,6 +70,19 @@ function registerSetupHandlers(getMainWindow) {
|
|
|
50
70
|
}
|
|
51
71
|
});
|
|
52
72
|
|
|
73
|
+
// Task 8: apply a per-provider default picker choice. Read-modify-write,
|
|
74
|
+
// no-clobber -- applyProviderDefault only ever writes aliases[vendor] and
|
|
75
|
+
// seeds config.default when absent (see provider-default-picker.js).
|
|
76
|
+
ipcMain.handle('sidecar:set-provider-default', (_event, provider, chosenId) => {
|
|
77
|
+
try {
|
|
78
|
+
const { applyProviderDefault } = require('../src/utils/provider-default-picker');
|
|
79
|
+
return applyProviderDefault(provider, chosenId, { seedDefaultIfAbsent: true });
|
|
80
|
+
} catch (err) {
|
|
81
|
+
logger.error('set-provider-default handler error', { error: err.message });
|
|
82
|
+
return { success: false, error: err.message };
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
53
86
|
ipcMain.handle('sidecar:remove-key', async (_event, provider) => {
|
|
54
87
|
try {
|
|
55
88
|
const { removeApiKey } = require('../src/utils/api-key-store');
|
|
@@ -22,7 +22,8 @@ contextBridge.exposeInMainWorld('sidecarSetup', {
|
|
|
22
22
|
'sidecar:get-api-keys',
|
|
23
23
|
'sidecar:get-catalog',
|
|
24
24
|
'sidecar:refresh-catalog',
|
|
25
|
-
'sidecar:fetch-free-models'
|
|
25
|
+
'sidecar:fetch-free-models',
|
|
26
|
+
'sidecar:set-provider-default'
|
|
26
27
|
];
|
|
27
28
|
if (!allowedChannels.includes(channel)) {
|
|
28
29
|
throw new Error(`IPC channel not allowed: ${channel}`);
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
const ALIAS_GROUPS = [
|
|
10
10
|
{ name: 'Gemini', keys: ['gemini', 'gemini-pro'] },
|
|
11
11
|
{ name: 'GPT', keys: ['gpt', 'gpt-pro', 'codex'] },
|
|
12
|
-
{ name: 'Claude', keys: ['claude', 'sonnet', 'opus', 'haiku'] },
|
|
12
|
+
{ name: 'Claude', keys: ['claude', 'sonnet', 'opus', 'haiku', 'fable'] },
|
|
13
13
|
{ name: 'DeepSeek', keys: ['deepseek'] },
|
|
14
14
|
{ name: 'Qwen', keys: ['qwen', 'qwen-coder', 'qwen-flash'] },
|
|
15
15
|
{ name: 'Mistral', keys: ['mistral', 'devstral'] },
|
|
@@ -19,6 +19,8 @@ function buildKeysScript() {
|
|
|
19
19
|
var prov = providers.find(function(p) { return p.id === id; });
|
|
20
20
|
if (!prov) { return; }
|
|
21
21
|
selectedProvider = prov;
|
|
22
|
+
// Task 8: a picker rendered for the previously-selected provider is stale here.
|
|
23
|
+
if (window.hideProviderDefaultPicker) { window.hideProviderDefaultPicker(); }
|
|
22
24
|
document.querySelectorAll('.provider-btn').forEach(function(b) { b.classList.remove('selected'); });
|
|
23
25
|
this.classList.add('selected');
|
|
24
26
|
keySection.classList.add('visible');
|
|
@@ -64,7 +66,7 @@ function buildKeysScript() {
|
|
|
64
66
|
try {
|
|
65
67
|
var res = await window.sidecarSetup.invoke('sidecar:validate-key', selectedProvider.id, key);
|
|
66
68
|
if (res.valid) {
|
|
67
|
-
await window.sidecarSetup.invoke('sidecar:save-key', selectedProvider.id, key);
|
|
69
|
+
var saveResult = await window.sidecarSetup.invoke('sidecar:save-key', selectedProvider.id, key);
|
|
68
70
|
configuredKeys[selectedProvider.id] = true;
|
|
69
71
|
var c = document.getElementById('check-' + selectedProvider.id);
|
|
70
72
|
if (c) { c.textContent = '\\u2713'; }
|
|
@@ -72,6 +74,10 @@ function buildKeysScript() {
|
|
|
72
74
|
setInputState('valid'); keyValid = true; validatedKey = key;
|
|
73
75
|
keyHints[selectedProvider.id] = key.slice(0, 8) + '\\u2022'.repeat(Math.min(key.length - 8, 12));
|
|
74
76
|
removeBtn.style.display = ''; updateNextState();
|
|
77
|
+
// Task 8: inline per-provider default picker beneath the just-saved key.
|
|
78
|
+
if (window.renderProviderDefaultPicker) {
|
|
79
|
+
window.renderProviderDefaultPicker(selectedProvider.id, selectedProvider.name, saveResult && saveResult.providerDefault);
|
|
80
|
+
}
|
|
75
81
|
} else {
|
|
76
82
|
statusMsg.textContent = res.error || 'Invalid key'; statusMsg.className = 'status-invalid';
|
|
77
83
|
setInputState('invalid'); keyValid = false;
|
|
@@ -107,6 +113,7 @@ function buildKeysScript() {
|
|
|
107
113
|
keyInput.value = ''; keyInput.type = 'password'; keyValid = false; setInputState(null);
|
|
108
114
|
statusMsg.textContent = 'Key removed'; statusMsg.className = 'status-testing';
|
|
109
115
|
removeBtn.style.display = 'none'; updateNextState();
|
|
116
|
+
if (window.hideProviderDefaultPicker) { window.hideProviderDefaultPicker(); }
|
|
110
117
|
} catch (_e) { statusMsg.textContent = 'Failed to remove'; statusMsg.className = 'status-invalid'; }
|
|
111
118
|
removeBtn.disabled = false;
|
|
112
119
|
});`;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Setup UI — Per-provider default model picker (Part 2, Task 8).
|
|
3
|
+
*
|
|
4
|
+
* Rendered inline in the key step (Step 1), beneath the just-saved key, once
|
|
5
|
+
* `sidecar:save-key` (ipc-setup.js) returns a non-null `providerDefault` for a
|
|
6
|
+
* DIRECT provider (google/openai/anthropic/deepseek — never the openrouter
|
|
7
|
+
* gateway; see ipc-setup.js's own isDirectProvider guard, mirroring the skip
|
|
8
|
+
* rule in provider-default-prompt.js's runProviderDefaultFlow).
|
|
9
|
+
*
|
|
10
|
+
* Split into its own file (rather than growing setup-ui-keys.js /
|
|
11
|
+
* setup-ui-keys-script.js) per the existing setup-ui-* convention (see
|
|
12
|
+
* setup-ui-council.js, another Step-content add-on with its own HTML +
|
|
13
|
+
* script pair).
|
|
14
|
+
*
|
|
15
|
+
* Row data (from src/utils/provider-default-picker.js's
|
|
16
|
+
* buildProviderDefaultChoices): { preselectedId, rows: [{id, name,
|
|
17
|
+
* contextLength, pricePerMInput, isPreselected}] }. Row name/pricing come
|
|
18
|
+
* straight from the live model catalog, so the runtime script below builds
|
|
19
|
+
* rows via createElement/textContent (never innerHTML string interpolation)
|
|
20
|
+
* — the same convention Step 2's renderSearchResults uses for catalog rows.
|
|
21
|
+
*/
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
/** Static (initially hidden) container the key step's picker renders into. */
|
|
25
|
+
function buildProviderDefaultSectionHTML() {
|
|
26
|
+
return `<div class="provider-default-section" id="provider-default-section" style="display:none">
|
|
27
|
+
<div class="provider-default-label" id="provider-default-label">Default model</div>
|
|
28
|
+
<div class="provider-default-list" id="provider-default-list"></div>
|
|
29
|
+
</div>`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Browser-only JS: exposes window.renderProviderDefaultPicker(provider,
|
|
34
|
+
* providerName, providerDefault) and window.hideProviderDefaultPicker(),
|
|
35
|
+
* wired to apply the pick via sidecar:set-provider-default
|
|
36
|
+
* (src/utils/provider-default-picker.js's applyProviderDefault,
|
|
37
|
+
* read-modify-write, no-clobber — see ipc-setup.js). Selecting a row (or
|
|
38
|
+
* leaving the preselection untouched) both apply the same way: the
|
|
39
|
+
* preselected id is applied immediately on render, and a change event
|
|
40
|
+
* re-applies whichever row the user picks instead.
|
|
41
|
+
* @returns {string} JavaScript source (no <script> tags)
|
|
42
|
+
*/
|
|
43
|
+
function buildProviderDefaultScript() {
|
|
44
|
+
return `
|
|
45
|
+
// Task 8: per-provider default picker, inline in the key step.
|
|
46
|
+
(function() {
|
|
47
|
+
var section = document.getElementById('provider-default-section');
|
|
48
|
+
var label = document.getElementById('provider-default-label');
|
|
49
|
+
var list = document.getElementById('provider-default-list');
|
|
50
|
+
|
|
51
|
+
function fmtPrice(p) { return (p === null || p === undefined) ? 'n/a' : '$' + Number(p).toFixed(2) + '/M in'; }
|
|
52
|
+
function fmtCtx(n) { return (n === null || n === undefined) ? '' : ' \\u00b7 ctx ' + n; }
|
|
53
|
+
|
|
54
|
+
function applyChoice(provider, chosenId) {
|
|
55
|
+
if (!provider || !chosenId) { return; }
|
|
56
|
+
window.sidecarSetup.invoke('sidecar:set-provider-default', provider, chosenId).catch(function() {});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function buildRow(provider, row) {
|
|
60
|
+
var rowEl = document.createElement('label');
|
|
61
|
+
rowEl.className = 'provider-default-row';
|
|
62
|
+
var radio = document.createElement('input');
|
|
63
|
+
radio.type = 'radio'; radio.name = 'provider-default-' + provider;
|
|
64
|
+
radio.value = row.id; radio.checked = !!row.isPreselected;
|
|
65
|
+
var text = document.createElement('span');
|
|
66
|
+
text.className = 'provider-default-text';
|
|
67
|
+
text.textContent = row.name + fmtCtx(row.contextLength) + ' \\u00b7 ' + fmtPrice(row.pricePerMInput);
|
|
68
|
+
rowEl.appendChild(radio); rowEl.appendChild(text);
|
|
69
|
+
if (row.isPreselected) {
|
|
70
|
+
var badge = document.createElement('span');
|
|
71
|
+
badge.className = 'pick-badge'; badge.textContent = 'recommended';
|
|
72
|
+
rowEl.appendChild(badge);
|
|
73
|
+
}
|
|
74
|
+
radio.addEventListener('change', function() { if (radio.checked) { applyChoice(provider, row.id); } });
|
|
75
|
+
return rowEl;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
window.hideProviderDefaultPicker = function() {
|
|
79
|
+
if (!section) { return; }
|
|
80
|
+
section.style.display = 'none';
|
|
81
|
+
if (list) { list.innerHTML = ''; }
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
window.renderProviderDefaultPicker = function(provider, providerName, providerDefault) {
|
|
85
|
+
if (!section || !list) { return; }
|
|
86
|
+
if (!providerDefault || !Array.isArray(providerDefault.rows) || providerDefault.rows.length === 0) {
|
|
87
|
+
window.hideProviderDefaultPicker(); return;
|
|
88
|
+
}
|
|
89
|
+
if (label) { label.textContent = 'Default model for ' + (providerName || provider); }
|
|
90
|
+
list.innerHTML = '';
|
|
91
|
+
providerDefault.rows.forEach(function(row) { list.appendChild(buildRow(provider, row)); });
|
|
92
|
+
section.style.display = '';
|
|
93
|
+
// No-selection case: the preselected row is already checked above, so
|
|
94
|
+
// applying it immediately covers "advance without touching the list".
|
|
95
|
+
applyChoice(provider, providerDefault.preselectedId);
|
|
96
|
+
};
|
|
97
|
+
})();
|
|
98
|
+
`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = { buildProviderDefaultSectionHTML, buildProviderDefaultScript };
|
|
@@ -415,7 +415,19 @@ function __rawWizardCSS() {
|
|
|
415
415
|
content: ''; position: absolute; inset: 0;
|
|
416
416
|
border-radius: 50%; background: var(--ok);
|
|
417
417
|
animation: amicusPulse 1.6s var(--ease-out) infinite;
|
|
418
|
-
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/* Per-provider default picker (Step 1, Task 8) */
|
|
421
|
+
.provider-default-section { margin-top: 14px; }
|
|
422
|
+
.provider-default-label { font-size: 12px; color: var(--text-muted); margin-bottom: 6px; }
|
|
423
|
+
.provider-default-list { display: flex; flex-direction: column; gap: 2px; }
|
|
424
|
+
.provider-default-row {
|
|
425
|
+
display: flex; align-items: center; gap: 8px;
|
|
426
|
+
padding: 6px 10px; font-size: 12px; color: var(--text); cursor: pointer;
|
|
427
|
+
border-radius: var(--r-6);
|
|
428
|
+
}
|
|
429
|
+
.provider-default-row:hover { background: var(--surface-hover); }
|
|
430
|
+
.provider-default-row input[type="radio"] { accent-color: var(--accent); flex-shrink: 0; }`;
|
|
419
431
|
}
|
|
420
432
|
|
|
421
433
|
function buildWizardCSS() {
|
package/electron/setup-ui.js
CHANGED
|
@@ -6,6 +6,7 @@ const { buildWizardCSS } = require('./setup-ui-styles');
|
|
|
6
6
|
const { buildKeysScript } = require('./setup-ui-keys-script');
|
|
7
7
|
const { buildAliasScript } = require('./setup-ui-alias-script');
|
|
8
8
|
const { buildCouncilSectionHTML, buildCouncilScript } = require('./setup-ui-council');
|
|
9
|
+
const { buildProviderDefaultSectionHTML, buildProviderDefaultScript } = require('./setup-ui-provider-default');
|
|
9
10
|
const { getDefaultAliases } = require('../src/utils/config');
|
|
10
11
|
const { getBrandName } = require('./toolbar');
|
|
11
12
|
const { resolveQuickPicks } = require('../src/utils/quick-picks');
|
|
@@ -40,7 +41,7 @@ function buildSetupHTML(options = {}) {
|
|
|
40
41
|
<div class="header"><svg width="18" height="18" viewBox="0 0 32 32" fill="none"><path d="M4 8H19"/><path d="M4 11H14L19 8"/><path d="M4 14H13L19 8"/><path d="M4 17H12L19 8"/><path d="M4 20H11L19 8"/><path d="M4 23H10L19 8"/><path class="brand-main" d="M19 8H28"/></svg><span class="header-title">${brandName} Setup</span></div>
|
|
41
42
|
<div class="progress-bar"><div class="progress-step active" id="step-1"><span class="progress-dot">1</span><span>API Keys</span></div><div class="progress-connector"></div><div class="progress-step" id="step-2"><span class="progress-dot">2</span><span>Models</span></div><div class="progress-connector"></div><div class="progress-step" id="step-3"><span class="progress-dot">3</span><span>Routing</span></div><div class="progress-connector"></div><div class="progress-step" id="step-4"><span class="progress-dot">4</span><span>Review</span></div></div>
|
|
42
43
|
<div class="content">
|
|
43
|
-
<div class="wizard-step visible" id="wizard-step-1"><div id="import-notice"></div>${keysHtml}</div>
|
|
44
|
+
<div class="wizard-step visible" id="wizard-step-1"><div id="import-notice"></div>${keysHtml}${buildProviderDefaultSectionHTML()}</div>
|
|
44
45
|
<div class="wizard-step" id="wizard-step-2">${modelHtml}${buildCouncilSectionHTML()}</div>
|
|
45
46
|
<div class="wizard-step" id="wizard-step-3">${aliasHtml}</div>
|
|
46
47
|
<div class="wizard-step" id="wizard-step-4">
|
|
@@ -63,6 +64,7 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
63
64
|
const keysJs = buildKeysScript();
|
|
64
65
|
const aliasJs = buildAliasScript();
|
|
65
66
|
const councilJs = buildCouncilScript();
|
|
67
|
+
const providerDefaultJs = buildProviderDefaultScript();
|
|
66
68
|
return `<script>
|
|
67
69
|
window.onerror = function(msg, src, line, col, err) { console.error('WIZARD ERROR:', msg, 'at', src, line, col, err); };
|
|
68
70
|
window.onunhandledrejection = function(e) { console.error('WIZARD UNHANDLED REJECTION:', e.reason); };
|
|
@@ -574,6 +576,8 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
574
576
|
${keysJs}
|
|
575
577
|
|
|
576
578
|
${councilJs}
|
|
579
|
+
|
|
580
|
+
${providerDefaultJs}
|
|
577
581
|
</script>`;
|
|
578
582
|
}
|
|
579
583
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amicus",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"mcpName": "io.github.BourbonDog/amicus",
|
|
5
5
|
"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.",
|
|
6
6
|
"keywords": [
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"refresh-models": "node bin/amicus.js models --refresh",
|
|
60
60
|
"models:info": "node bin/amicus.js models",
|
|
61
61
|
"models:check": "node bin/amicus.js models --check",
|
|
62
|
+
"models:check:strict": "node bin/amicus.js models --check --strict",
|
|
62
63
|
"generate-icon": "node scripts/generate-icon.js",
|
|
63
64
|
"generate-docs": "node scripts/generate-docs.js",
|
|
64
65
|
"generate-docs:check": "node scripts/generate-docs.js --check",
|
package/src/cli-handlers-run.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
const { validateStartArgs } = require('./cli');
|
|
16
16
|
const { validateTaskId } = require('./utils/validators');
|
|
17
|
-
const { resolveLaunchModel } = require('./utils/start-helpers');
|
|
17
|
+
const { resolveLaunchModel, maybeOfferProviderDefaults } = require('./utils/start-helpers');
|
|
18
18
|
const { failJson, ERROR_CODES } = require('./utils/error-doc');
|
|
19
19
|
const { requireNoUiForJson } = require('./utils/cli-preflight');
|
|
20
20
|
const { GATEWAY_MODES } = require('./utils/model-descriptor');
|
|
@@ -55,6 +55,14 @@ async function handleStart(args) {
|
|
|
55
55
|
process.exit(failJson(useJson, { code: validation.code || ERROR_CODES.BAD_ARGS, message: validation.error }));
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// Existing-user one-time onboarding offer (Part 2, Task 9): a non-blocking
|
|
59
|
+
// notice, printed at most once ever, pointing users who already have direct
|
|
60
|
+
// provider keys at the per-provider cost-aware default picker. No-ops on
|
|
61
|
+
// any non-interactive/--json run (see maybeOfferProviderDefaults). Fired
|
|
62
|
+
// only after validateStartArgs succeeds so a failed first `amicus start`
|
|
63
|
+
// doesn't burn the one-time flag.
|
|
64
|
+
maybeOfferProviderDefaults(args);
|
|
65
|
+
|
|
58
66
|
// Budget gate for solo start
|
|
59
67
|
if (!args['no-cost-gate']) {
|
|
60
68
|
const { lookupPricing } = require('./utils/pricing');
|
package/src/cli-handlers.js
CHANGED
|
@@ -159,6 +159,46 @@ async function handleKey(args) {
|
|
|
159
159
|
process.exit(1);
|
|
160
160
|
}
|
|
161
161
|
console.log(`${provider} key validated and saved.`);
|
|
162
|
+
|
|
163
|
+
await offerProviderDefault(provider, args);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* After a successful key save, run the per-provider cost-aware default
|
|
168
|
+
* picker (Part 2, Task 6) and print its one-line summary. Wrapped end-to-end
|
|
169
|
+
* in try/catch: the key is ALREADY saved by the time this runs, so a picker
|
|
170
|
+
* failure (offline catalog, bad input, etc.) must never abort `amicus key`
|
|
171
|
+
* -- it just prints a soft notice instead.
|
|
172
|
+
* @param {string} provider
|
|
173
|
+
* @param {object} args parsed CLI args (checked for --json/--quiet to gate interactivity)
|
|
174
|
+
*/
|
|
175
|
+
async function offerProviderDefault(provider, args) {
|
|
176
|
+
let rl = null;
|
|
177
|
+
try {
|
|
178
|
+
const { runProviderDefaultFlow } = require('./utils/provider-default-prompt');
|
|
179
|
+
const { getCatalog } = require('./utils/model-catalog');
|
|
180
|
+
|
|
181
|
+
const interactive = !!process.stdin.isTTY && !args.json && !args.quiet;
|
|
182
|
+
|
|
183
|
+
let catalog = [];
|
|
184
|
+
try { catalog = await getCatalog(); } catch { /* offline/empty: handled gracefully by the picker */ }
|
|
185
|
+
|
|
186
|
+
let ask;
|
|
187
|
+
if (interactive) {
|
|
188
|
+
const readline = require('readline');
|
|
189
|
+
rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
190
|
+
ask = (q) => new Promise((resolve) => rl.question(q, (answer) => resolve((answer || '').trim())));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const { summaryLine } = await runProviderDefaultFlow(provider, {
|
|
194
|
+
interactive, ask, catalog, print: console.log,
|
|
195
|
+
});
|
|
196
|
+
console.log(summaryLine);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
console.log(`Note: couldn't set a per-provider default (${err.message}). Run 'amicus key ${provider}' again later.`);
|
|
199
|
+
} finally {
|
|
200
|
+
if (rl) { rl.close(); }
|
|
201
|
+
}
|
|
162
202
|
}
|
|
163
203
|
|
|
164
204
|
module.exports = {
|
package/src/cli.js
CHANGED
|
@@ -138,6 +138,7 @@ function isBooleanFlag(key) {
|
|
|
138
138
|
'html', // council report: emit a self-contained HTML page
|
|
139
139
|
'md', // council report: emit Markdown (default)
|
|
140
140
|
'fix', // doctor: self-heal fixable checks in place (#56)
|
|
141
|
+
'strict', // models --check: exit non-zero on curated per-gateway drift (#gwid Task 6)
|
|
141
142
|
];
|
|
142
143
|
return booleanFlags.includes(key);
|
|
143
144
|
}
|
|
@@ -436,6 +437,9 @@ Options for 'models':
|
|
|
436
437
|
--search <q> Filter by substring over model id and name
|
|
437
438
|
--refresh Force-refresh the catalog from provider APIs
|
|
438
439
|
--check Audit aliases against the catalog (exit = stale count)
|
|
440
|
+
--strict With --check: also exit non-zero on curated
|
|
441
|
+
per-gateway drift (stale/divergent direct or
|
|
442
|
+
openrouter forms). Informational without it.
|
|
439
443
|
--json Machine-readable output
|
|
440
444
|
`,
|
|
441
445
|
list: `
|
package/src/sidecar/models.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
const { getCatalogInfo, refreshCatalog, catalogPath } = require('../utils/model-catalog');
|
|
16
16
|
const { collectAliasSources, findStaleAliases, suggestReplacements } = require('../utils/alias-audit');
|
|
17
|
+
const { auditGatewayRoutes } = require('../utils/gateway-route-audit');
|
|
17
18
|
const { buildCatalogDoc, buildAuditDoc } = require('../utils/result-schema');
|
|
18
19
|
const { getFamilies } = require('../utils/curated-models');
|
|
19
20
|
const { pickCurrent } = require('../utils/quick-picks');
|
|
@@ -116,8 +117,20 @@ async function runRefresh(args) {
|
|
|
116
117
|
return 0;
|
|
117
118
|
}
|
|
118
119
|
|
|
120
|
+
/** One readable line per gateway-route finding (Task 6, #gwid). @param {object} f @returns {string} */
|
|
121
|
+
function fmtGatewayFinding(f) {
|
|
122
|
+
if (f.kind === 'stale') {
|
|
123
|
+
return ` GATEWAY STALE (${f.gateway}): ${f.alias} -> ${f.model}`;
|
|
124
|
+
}
|
|
125
|
+
if (f.kind === 'divergent-missing') {
|
|
126
|
+
return ` GATEWAY DIVERGENT: ${f.alias} has no direct form; catalog confirms ${f.model}`;
|
|
127
|
+
}
|
|
128
|
+
return ` GATEWAY DIVERGENT: ${f.alias} direct form ${f.model} no longer matches catalog (now ${f.expected})`;
|
|
129
|
+
}
|
|
130
|
+
|
|
119
131
|
async function runCheck(args) {
|
|
120
|
-
const
|
|
132
|
+
const catalogInfo = await getCatalogInfo();
|
|
133
|
+
const catalog = catalogInfo.models;
|
|
121
134
|
if (!catalog || catalog.length === 0) {
|
|
122
135
|
if (args.json) {
|
|
123
136
|
process.stdout.write(JSON.stringify(buildAuditDoc({
|
|
@@ -131,35 +144,44 @@ async function runCheck(args) {
|
|
|
131
144
|
const sources = collectAliasSources();
|
|
132
145
|
const stale = findStaleAliases(sources, catalog)
|
|
133
146
|
.map(s => ({ ...s, suggestions: suggestReplacements(s.model, catalog) }));
|
|
147
|
+
// Task 6 (#gwid): per-gateway-form audit of the curated DEFAULTS
|
|
148
|
+
// (toGatewayRoutes()) — additive to the flat audit above. Informational by
|
|
149
|
+
// default; --strict promotes it to a build-breaking exit code (CI gate).
|
|
150
|
+
const gatewayFindings = auditGatewayRoutes(catalogInfo);
|
|
151
|
+
const legacyExitCode = Math.min(stale.length, CHECK_EXIT_CAP);
|
|
152
|
+
const exitCode = args.strict
|
|
153
|
+
? Math.max(legacyExitCode, Math.min(gatewayFindings.length, CHECK_EXIT_CAP))
|
|
154
|
+
: legacyExitCode;
|
|
155
|
+
|
|
134
156
|
if (args.json) {
|
|
135
157
|
process.stdout.write(JSON.stringify(buildAuditDoc({
|
|
136
|
-
stale, catalogAvailable: true
|
|
158
|
+
stale, catalogAvailable: true, gatewayFindings
|
|
137
159
|
}), null, 2) + '\n');
|
|
138
|
-
return
|
|
160
|
+
return exitCode;
|
|
139
161
|
}
|
|
140
162
|
const driftLines = buildFallbackDriftReport(catalog);
|
|
141
163
|
if (stale.length === 0) {
|
|
142
164
|
process.stdout.write(`All aliases resolve to catalog models (${sources.length} checked).\n`);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
process.stdout.write(` candidates: ${s.suggestions.join(', ')}\n`);
|
|
153
|
-
process.stdout.write(` fix: amicus setup --add-alias ${s.alias}=${s.suggestions[0]}\n`);
|
|
154
|
-
} else {
|
|
155
|
-
process.stdout.write(' no same-vendor candidates in catalog\n');
|
|
165
|
+
} else {
|
|
166
|
+
for (const s of stale) {
|
|
167
|
+
process.stdout.write(`STALE: ${s.alias} -> ${s.model} (${s.source})\n`);
|
|
168
|
+
if (s.suggestions.length > 0) {
|
|
169
|
+
process.stdout.write(` candidates: ${s.suggestions.join(', ')}\n`);
|
|
170
|
+
process.stdout.write(` fix: amicus setup --add-alias ${s.alias}=${s.suggestions[0]}\n`);
|
|
171
|
+
} else {
|
|
172
|
+
process.stdout.write(' no same-vendor candidates in catalog\n');
|
|
173
|
+
}
|
|
156
174
|
}
|
|
157
175
|
}
|
|
158
176
|
if (driftLines.length > 0) {
|
|
159
177
|
process.stdout.write('Pinned fallback drift:\n');
|
|
160
178
|
for (const l of driftLines) { process.stdout.write(l + '\n'); }
|
|
161
179
|
}
|
|
162
|
-
|
|
180
|
+
if (gatewayFindings.length > 0) {
|
|
181
|
+
process.stdout.write('Per-gateway route audit (curated defaults):\n');
|
|
182
|
+
for (const f of gatewayFindings) { process.stdout.write(fmtGatewayFinding(f) + '\n'); }
|
|
183
|
+
}
|
|
184
|
+
return exitCode;
|
|
163
185
|
}
|
|
164
186
|
|
|
165
187
|
/**
|