amicus 1.0.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 (93) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/LICENSE +21 -0
  3. package/README.md +477 -0
  4. package/bin/amicus.js +382 -0
  5. package/electron/assets/icon.png +0 -0
  6. package/electron/assets/icon.svg +5 -0
  7. package/electron/fold.js +163 -0
  8. package/electron/ipc-setup.js +176 -0
  9. package/electron/load-failsafe.js +85 -0
  10. package/electron/main.js +468 -0
  11. package/electron/preload-setup.js +38 -0
  12. package/electron/preload.js +33 -0
  13. package/electron/setup-ui-alias-script.js +218 -0
  14. package/electron/setup-ui-aliases.js +85 -0
  15. package/electron/setup-ui-keys-script.js +115 -0
  16. package/electron/setup-ui-keys.js +97 -0
  17. package/electron/setup-ui-model.js +138 -0
  18. package/electron/setup-ui-styles.js +327 -0
  19. package/electron/setup-ui.js +465 -0
  20. package/electron/summary.js +118 -0
  21. package/electron/toolbar.js +229 -0
  22. package/electron/window-position.js +35 -0
  23. package/package.json +98 -0
  24. package/scripts/postinstall.js +193 -0
  25. package/scripts/setup-hooks.js +42 -0
  26. package/skill/SKILL.md +976 -0
  27. package/skills/second-opinion/COUNCIL-DESIGN.md +227 -0
  28. package/skills/second-opinion/MODEL-NOTES.md +104 -0
  29. package/skills/second-opinion/SKILL.md +389 -0
  30. package/src/cli-handlers.js +188 -0
  31. package/src/cli.js +400 -0
  32. package/src/conflict.js +144 -0
  33. package/src/context-compression.js +102 -0
  34. package/src/context.js +199 -0
  35. package/src/drift.js +144 -0
  36. package/src/environment.js +157 -0
  37. package/src/headless.js +742 -0
  38. package/src/index.js +106 -0
  39. package/src/jsonl-parser.js +180 -0
  40. package/src/mcp-server.js +625 -0
  41. package/src/mcp-tools.js +407 -0
  42. package/src/opencode-client.js +615 -0
  43. package/src/prompt-builder.js +355 -0
  44. package/src/prompts/cowork-agent-prompt.js +118 -0
  45. package/src/session-manager.js +414 -0
  46. package/src/session.js +180 -0
  47. package/src/sidecar/context-builder.js +297 -0
  48. package/src/sidecar/continue.js +212 -0
  49. package/src/sidecar/crash-handler.js +56 -0
  50. package/src/sidecar/fanout-leg.js +107 -0
  51. package/src/sidecar/fanout-output.js +46 -0
  52. package/src/sidecar/fanout.js +236 -0
  53. package/src/sidecar/interactive.js +217 -0
  54. package/src/sidecar/models.js +135 -0
  55. package/src/sidecar/progress.js +218 -0
  56. package/src/sidecar/read.js +183 -0
  57. package/src/sidecar/resume.js +221 -0
  58. package/src/sidecar/session-utils.js +288 -0
  59. package/src/sidecar/setup-window.js +79 -0
  60. package/src/sidecar/setup.js +280 -0
  61. package/src/sidecar/start.js +251 -0
  62. package/src/utils/agent-mapping.js +138 -0
  63. package/src/utils/alias-audit.js +98 -0
  64. package/src/utils/alias-resolver.js +77 -0
  65. package/src/utils/api-key-store.js +259 -0
  66. package/src/utils/api-key-validation.js +97 -0
  67. package/src/utils/auth-json.js +109 -0
  68. package/src/utils/config.js +291 -0
  69. package/src/utils/curated-models.js +82 -0
  70. package/src/utils/env-compat.js +38 -0
  71. package/src/utils/env-loader.js +54 -0
  72. package/src/utils/idle-watchdog.js +225 -0
  73. package/src/utils/input-validators.js +127 -0
  74. package/src/utils/lifecycle.js +43 -0
  75. package/src/utils/logger.js +84 -0
  76. package/src/utils/mcp-discovery.js +194 -0
  77. package/src/utils/mcp-validators.js +78 -0
  78. package/src/utils/model-catalog.js +103 -0
  79. package/src/utils/model-fetcher.js +179 -0
  80. package/src/utils/model-validator.js +207 -0
  81. package/src/utils/path-setup.js +41 -0
  82. package/src/utils/port-pid.js +39 -0
  83. package/src/utils/prompt-source.js +53 -0
  84. package/src/utils/result-schema.js +261 -0
  85. package/src/utils/server-setup.js +93 -0
  86. package/src/utils/session-abort.js +53 -0
  87. package/src/utils/session-lock.js +95 -0
  88. package/src/utils/shared-server.js +216 -0
  89. package/src/utils/start-helpers.js +76 -0
  90. package/src/utils/thinking-validators.js +92 -0
  91. package/src/utils/update-notifier-loader.js +18 -0
  92. package/src/utils/updater.js +157 -0
  93. package/src/utils/validators.js +300 -0
@@ -0,0 +1,176 @@
1
+ /**
2
+ * IPC Setup Handlers
3
+ *
4
+ * Extracted from main.js to keep file sizes under 300 lines.
5
+ * Registers all setup-mode IPC handlers: validate-key, save-key,
6
+ * remove-key, setup-done, save-config, get-config, get-api-keys,
7
+ * fetch-models, get-catalog, and refresh-catalog.
8
+ */
9
+
10
+ const { logger } = require('../src/utils/logger');
11
+
12
+ /**
13
+ * Register all setup-related IPC handlers
14
+ * @param {Electron.IpcMain} ipcMain - Electron IPC main
15
+ * @param {function} getMainWindow - Returns the current main BrowserWindow
16
+ */
17
+ function registerSetupHandlers(ipcMain, getMainWindow) {
18
+ ipcMain.handle('sidecar:validate-key', async (_event, provider, key) => {
19
+ try {
20
+ const { validateApiKey } = require('../src/utils/api-key-store');
21
+ return await validateApiKey(provider, key);
22
+ } catch (err) {
23
+ logger.error('validate-key handler error', { error: err.message });
24
+ return { valid: false, error: err.message };
25
+ }
26
+ });
27
+
28
+ ipcMain.handle('sidecar:save-key', async (_event, provider, key) => {
29
+ try {
30
+ const { saveApiKey } = require('../src/utils/api-key-store');
31
+ const result = saveApiKey(provider, key);
32
+ // F5: warm the model catalog as soon as a key lands so the Step 2
33
+ // picker renders instantly. Fire-and-forget; failures are silent
34
+ // (a failed warm-up never clobbers the cache; Step 2's get-catalog or
35
+ // the refresh button retry it).
36
+ if (result && result.success !== false) {
37
+ setImmediate(() => {
38
+ try {
39
+ require('../src/utils/model-catalog').refreshCatalog().catch(() => {});
40
+ } catch { /* best-effort */ }
41
+ });
42
+ }
43
+ return result;
44
+ } catch (err) {
45
+ logger.error('save-key handler error', { error: err.message });
46
+ return { success: false, error: err.message };
47
+ }
48
+ });
49
+
50
+ ipcMain.handle('sidecar:remove-key', async (_event, provider) => {
51
+ try {
52
+ const { removeApiKey } = require('../src/utils/api-key-store');
53
+ const { removeFromAuthJson } = require('../src/utils/auth-json');
54
+ const result = removeApiKey(provider);
55
+ // Always clean auth.json too — prevents auto-import from re-adding the key
56
+ if (result.alsoInAuthJson) {
57
+ removeFromAuthJson(provider);
58
+ result.alsoInAuthJson = false;
59
+ }
60
+ return result;
61
+ } catch (err) {
62
+ logger.error('remove-key handler error', { error: err.message });
63
+ return { success: false, error: err.message };
64
+ }
65
+ });
66
+
67
+ ipcMain.handle('sidecar:remove-from-opencode', async (_event, provider) => {
68
+ try {
69
+ const { removeFromAuthJson } = require('../src/utils/auth-json');
70
+ removeFromAuthJson(provider);
71
+ return { success: true };
72
+ } catch (err) {
73
+ logger.error('remove-from-opencode handler error', { error: err.message });
74
+ return { success: false, error: err.message };
75
+ }
76
+ });
77
+
78
+ ipcMain.handle('sidecar:setup-done', (_event, defaultModel, keyCount) => {
79
+ const { BrowserWindow } = require('electron');
80
+ const senderWindow = BrowserWindow.fromWebContents(_event.sender);
81
+ const mainWin = getMainWindow();
82
+
83
+ // If sent from main window → stdout + close (CLI setup flow)
84
+ // If sent from child window → just close it (settings flow)
85
+ if (senderWindow === mainWin) {
86
+ const result = JSON.stringify({
87
+ status: 'complete',
88
+ default: defaultModel || undefined,
89
+ keyCount: keyCount || undefined
90
+ });
91
+ process.stdout.write(result + '\n');
92
+ if (mainWin) { mainWin.close(); }
93
+ } else if (senderWindow) {
94
+ senderWindow.close();
95
+ }
96
+ });
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);
103
+ }
104
+ saveConfig({ default: defaultModel, aliases });
105
+ return { success: true };
106
+ });
107
+
108
+ ipcMain.handle('sidecar:get-config', () => {
109
+ const { loadConfig } = require('../src/utils/config');
110
+ return loadConfig();
111
+ });
112
+
113
+ ipcMain.handle('sidecar:get-api-keys', () => {
114
+ try {
115
+ const { readApiKeys, readApiKeyHints, saveApiKey } = require('../src/utils/api-key-store');
116
+ const { importFromAuthJson } = require('../src/utils/auth-json');
117
+ const status = readApiKeys();
118
+ const hints = readApiKeyHints();
119
+
120
+ // Auto-import keys from auth.json that sidecar doesn't have yet
121
+ const { imported } = importFromAuthJson(status);
122
+ for (const entry of imported) {
123
+ const result = saveApiKey(entry.provider, entry.key);
124
+ if (result && result.success !== false) {
125
+ status[entry.provider] = true;
126
+ const visible = entry.key.slice(0, 8);
127
+ hints[entry.provider] = visible + '\u2022'.repeat(Math.max(0, Math.min(entry.key.length - 8, 12)));
128
+ }
129
+ }
130
+
131
+ return { status, hints, imported: imported.map(e => e.provider) };
132
+ } catch (err) {
133
+ logger.error('get-api-keys handler error', { error: err.message });
134
+ return { status: {}, hints: {}, imported: [] };
135
+ }
136
+ });
137
+
138
+ ipcMain.handle('sidecar:fetch-models', async () => {
139
+ try {
140
+ const { readApiKeyValues } = require('../src/utils/api-key-store');
141
+ const { fetchAllModels, groupModelsByFamily } = require('../src/utils/model-fetcher');
142
+ const keys = readApiKeyValues();
143
+ const models = await fetchAllModels(keys);
144
+ return groupModelsByFamily(models);
145
+ } catch (err) {
146
+ logger.error('fetch-models handler error', { error: err.message });
147
+ return [];
148
+ }
149
+ });
150
+
151
+ // F5: wizard Step 2 reads the catalog CACHE (self-refreshing when stale).
152
+ ipcMain.handle('sidecar:get-catalog', async () => {
153
+ try {
154
+ const { getCatalogInfo } = require('../src/utils/model-catalog');
155
+ return await getCatalogInfo();
156
+ } catch (err) {
157
+ logger.error('get-catalog handler error', { error: err.message });
158
+ return { models: [], fetchedAt: null };
159
+ }
160
+ });
161
+
162
+ ipcMain.handle('sidecar:refresh-catalog', async () => {
163
+ try {
164
+ const { refreshCatalog, getCatalogInfo } = require('../src/utils/model-catalog');
165
+ await refreshCatalog();
166
+ // maxAgeMs: Infinity — report whatever the cache now holds without
167
+ // re-triggering a second fetch when the refresh just failed.
168
+ return await getCatalogInfo({ maxAgeMs: Number.POSITIVE_INFINITY });
169
+ } catch (err) {
170
+ logger.error('refresh-catalog handler error', { error: err.message });
171
+ return { models: [], fetchedAt: null };
172
+ }
173
+ });
174
+ }
175
+
176
+ module.exports = { registerSetupHandlers };
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Load Failsafe
3
+ *
4
+ * The Amicus window is created hidden and only shown after the OpenCode UI
5
+ * finishes loading (main.js did-finish-load -> rebrand -> show). If that load
6
+ * fails or stalls, nothing would ever show the window: the process sits as an
7
+ * invisible, silent hang (the historical "Starting up... | 0 messages" bug).
8
+ *
9
+ * attachLoadFailsafe watches a webContents and fires onFail exactly once when
10
+ * the main frame definitively fails to load, or when nothing has resolved
11
+ * within timeoutMs. The caller decides how to surface it (show the window,
12
+ * render an error page, log). cancel() disarms it on the success path.
13
+ */
14
+
15
+ 'use strict';
16
+
17
+ const DEFAULT_TIMEOUT_MS = 15000;
18
+ const ERR_ABORTED = -3; // benign: fires on in-page redirects/navigation replacement
19
+
20
+ /**
21
+ * Watch a webContents for startup load failure or stall.
22
+ * @param {{ webContents: object, timeoutMs?: number, onFail: Function }} opts
23
+ * @returns {{ cancel: Function }} call cancel() once the window is shown
24
+ */
25
+ function attachLoadFailsafe({ webContents, timeoutMs = DEFAULT_TIMEOUT_MS, onFail }) {
26
+ let settled = false;
27
+
28
+ const fire = (detail) => {
29
+ if (settled) { return; }
30
+ settled = true;
31
+ clearTimeout(timer);
32
+ onFail(detail);
33
+ };
34
+
35
+ const timer = setTimeout(() => fire({ reason: 'timeout' }), timeoutMs);
36
+
37
+ webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL, isMainFrame) => {
38
+ if (!isMainFrame || errorCode === ERR_ABORTED) { return; }
39
+ fire({ reason: 'load-failed', errorCode, errorDescription, validatedURL });
40
+ });
41
+
42
+ return {
43
+ cancel() {
44
+ settled = true;
45
+ clearTimeout(timer);
46
+ }
47
+ };
48
+ }
49
+
50
+ /** @param {*} value @returns {string} HTML-escaped text */
51
+ function escapeHTML(value) {
52
+ return String(value)
53
+ .replace(/&/g, '&')
54
+ .replace(/</g, '&lt;')
55
+ .replace(/>/g, '&gt;')
56
+ .replace(/"/g, '&quot;');
57
+ }
58
+
59
+ /**
60
+ * Error page shown in the content area when the OpenCode UI fails to load.
61
+ * @param {{ url: string, errorCode: number|string, errorDescription: string }} info
62
+ * @returns {string} full HTML document
63
+ */
64
+ function buildLoadErrorHTML({ url, errorCode, errorDescription }) {
65
+ return `<!DOCTYPE html>
66
+ <html><head><style>
67
+ body { background: #2D2B2A; color: #D4D0CC; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
68
+ display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
69
+ .box { max-width: 440px; padding: 24px; }
70
+ h1 { color: #D97757; font-size: 16px; margin: 0 0 12px; }
71
+ p { font-size: 13px; line-height: 1.5; color: #A09B96; margin: 0 0 10px; }
72
+ code { font-family: 'SF Mono', Menlo, Consolas, monospace; font-size: 11px; color: #D4D0CC;
73
+ word-break: break-all; }
74
+ </style></head><body>
75
+ <div class="box">
76
+ <h1>Amicus could not load the conversation UI</h1>
77
+ <p><code>${escapeHTML(errorDescription)} (${escapeHTML(errorCode)})</code></p>
78
+ <p>Failed URL: <code>${escapeHTML(url)}</code></p>
79
+ <p>The OpenCode server may have stopped. Close this window and rerun the
80
+ command, or use <code>--no-ui</code> for headless mode.</p>
81
+ </div>
82
+ </body></html>`;
83
+ }
84
+
85
+ module.exports = { attachLoadFailsafe, buildLoadErrorHTML, DEFAULT_TIMEOUT_MS };