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.
- package/CHANGELOG.md +46 -0
- package/LICENSE +21 -0
- package/README.md +477 -0
- package/bin/amicus.js +382 -0
- package/electron/assets/icon.png +0 -0
- package/electron/assets/icon.svg +5 -0
- package/electron/fold.js +163 -0
- package/electron/ipc-setup.js +176 -0
- package/electron/load-failsafe.js +85 -0
- package/electron/main.js +468 -0
- package/electron/preload-setup.js +38 -0
- package/electron/preload.js +33 -0
- package/electron/setup-ui-alias-script.js +218 -0
- package/electron/setup-ui-aliases.js +85 -0
- package/electron/setup-ui-keys-script.js +115 -0
- package/electron/setup-ui-keys.js +97 -0
- package/electron/setup-ui-model.js +138 -0
- package/electron/setup-ui-styles.js +327 -0
- package/electron/setup-ui.js +465 -0
- package/electron/summary.js +118 -0
- package/electron/toolbar.js +229 -0
- package/electron/window-position.js +35 -0
- package/package.json +98 -0
- package/scripts/postinstall.js +193 -0
- package/scripts/setup-hooks.js +42 -0
- package/skill/SKILL.md +976 -0
- package/skills/second-opinion/COUNCIL-DESIGN.md +227 -0
- package/skills/second-opinion/MODEL-NOTES.md +104 -0
- package/skills/second-opinion/SKILL.md +389 -0
- package/src/cli-handlers.js +188 -0
- package/src/cli.js +400 -0
- package/src/conflict.js +144 -0
- package/src/context-compression.js +102 -0
- package/src/context.js +199 -0
- package/src/drift.js +144 -0
- package/src/environment.js +157 -0
- package/src/headless.js +742 -0
- package/src/index.js +106 -0
- package/src/jsonl-parser.js +180 -0
- package/src/mcp-server.js +625 -0
- package/src/mcp-tools.js +407 -0
- package/src/opencode-client.js +615 -0
- package/src/prompt-builder.js +355 -0
- package/src/prompts/cowork-agent-prompt.js +118 -0
- package/src/session-manager.js +414 -0
- package/src/session.js +180 -0
- package/src/sidecar/context-builder.js +297 -0
- package/src/sidecar/continue.js +212 -0
- package/src/sidecar/crash-handler.js +56 -0
- package/src/sidecar/fanout-leg.js +107 -0
- package/src/sidecar/fanout-output.js +46 -0
- package/src/sidecar/fanout.js +236 -0
- package/src/sidecar/interactive.js +217 -0
- package/src/sidecar/models.js +135 -0
- package/src/sidecar/progress.js +218 -0
- package/src/sidecar/read.js +183 -0
- package/src/sidecar/resume.js +221 -0
- package/src/sidecar/session-utils.js +288 -0
- package/src/sidecar/setup-window.js +79 -0
- package/src/sidecar/setup.js +280 -0
- package/src/sidecar/start.js +251 -0
- package/src/utils/agent-mapping.js +138 -0
- package/src/utils/alias-audit.js +98 -0
- package/src/utils/alias-resolver.js +77 -0
- package/src/utils/api-key-store.js +259 -0
- package/src/utils/api-key-validation.js +97 -0
- package/src/utils/auth-json.js +109 -0
- package/src/utils/config.js +291 -0
- package/src/utils/curated-models.js +82 -0
- package/src/utils/env-compat.js +38 -0
- package/src/utils/env-loader.js +54 -0
- package/src/utils/idle-watchdog.js +225 -0
- package/src/utils/input-validators.js +127 -0
- package/src/utils/lifecycle.js +43 -0
- package/src/utils/logger.js +84 -0
- package/src/utils/mcp-discovery.js +194 -0
- package/src/utils/mcp-validators.js +78 -0
- package/src/utils/model-catalog.js +103 -0
- package/src/utils/model-fetcher.js +179 -0
- package/src/utils/model-validator.js +207 -0
- package/src/utils/path-setup.js +41 -0
- package/src/utils/port-pid.js +39 -0
- package/src/utils/prompt-source.js +53 -0
- package/src/utils/result-schema.js +261 -0
- package/src/utils/server-setup.js +93 -0
- package/src/utils/session-abort.js +53 -0
- package/src/utils/session-lock.js +95 -0
- package/src/utils/shared-server.js +216 -0
- package/src/utils/start-helpers.js +76 -0
- package/src/utils/thinking-validators.js +92 -0
- package/src/utils/update-notifier-loader.js +18 -0
- package/src/utils/updater.js +157 -0
- package/src/utils/validators.js +300 -0
package/bin/amicus.js
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Amicus CLI Entry Point
|
|
5
|
+
*
|
|
6
|
+
* Spec Reference: §4 CLI Interface
|
|
7
|
+
* Routes commands to appropriate handlers.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Load API keys from all sources: process.env > sidecar .env > auth.json
|
|
11
|
+
const { loadCredentials } = require('../src/utils/env-loader');
|
|
12
|
+
loadCredentials();
|
|
13
|
+
|
|
14
|
+
const { parseArgs, validateStartArgs, getUsage } = require('../src/cli');
|
|
15
|
+
const { validateTaskId } = require('../src/utils/validators');
|
|
16
|
+
const { resolveModelFromArgs, validateFallbackModel } = require('../src/utils/start-helpers');
|
|
17
|
+
const { handleSetup, handleAbort, handleUpdate, handleMcp } = require('../src/cli-handlers');
|
|
18
|
+
const { isOneShotCommand, armExitWatchdog } = require('../src/utils/lifecycle');
|
|
19
|
+
const { logger } = require('../src/utils/logger');
|
|
20
|
+
|
|
21
|
+
const VERSION = require('../package.json').version;
|
|
22
|
+
|
|
23
|
+
async function main() {
|
|
24
|
+
const args = parseArgs(process.argv.slice(2));
|
|
25
|
+
const command = args._[0];
|
|
26
|
+
|
|
27
|
+
// Install crash handler for MCP-spawned processes (have --task-id)
|
|
28
|
+
if (args['task-id'] && (command === 'start' || command === 'continue')) {
|
|
29
|
+
const { installCrashHandler } = require('../src/sidecar/crash-handler');
|
|
30
|
+
const project = args.cwd || process.cwd();
|
|
31
|
+
const handler = installCrashHandler(args['task-id'], project);
|
|
32
|
+
process.on('uncaughtException', (err) => {
|
|
33
|
+
handler(err);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
});
|
|
36
|
+
process.on('unhandledRejection', (reason) => {
|
|
37
|
+
handler(reason instanceof Error ? reason : new Error(String(reason)));
|
|
38
|
+
process.exit(1);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Non-interactive update check (skip for mcp, --version, --help)
|
|
43
|
+
if (command !== 'mcp' && !args.version && !args.help) {
|
|
44
|
+
const { initUpdateCheck, getUpdateInfo, notifyUpdate } = require('../src/utils/updater');
|
|
45
|
+
await initUpdateCheck();
|
|
46
|
+
// Pass update info to Electron child process via env var,
|
|
47
|
+
// because update-notifier deletes the cache entry after reading it.
|
|
48
|
+
const cliUpdateInfo = getUpdateInfo();
|
|
49
|
+
if (cliUpdateInfo) {
|
|
50
|
+
process.env.AMICUS_UPDATE_INFO = JSON.stringify(cliUpdateInfo);
|
|
51
|
+
process.on('exit', () => {
|
|
52
|
+
process.stderr.write(
|
|
53
|
+
`\n Update available: v${cliUpdateInfo.current} → v${cliUpdateInfo.latest}\n` +
|
|
54
|
+
' Run `npm install -g amicus` to upgrade.\n\n'
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Handle --version
|
|
61
|
+
if (args.version) {
|
|
62
|
+
console.log(`amicus v${VERSION}`);
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Handle --help or no command
|
|
67
|
+
if (args.help || args._.length === 0) {
|
|
68
|
+
console.log(getUsage());
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let exitCode = 0;
|
|
73
|
+
try {
|
|
74
|
+
switch (command) {
|
|
75
|
+
case 'start':
|
|
76
|
+
await handleStart(args);
|
|
77
|
+
break;
|
|
78
|
+
case 'fanout':
|
|
79
|
+
exitCode = await handleFanout(args);
|
|
80
|
+
break;
|
|
81
|
+
case 'list':
|
|
82
|
+
await handleList(args);
|
|
83
|
+
break;
|
|
84
|
+
case 'resume':
|
|
85
|
+
await handleResume(args);
|
|
86
|
+
break;
|
|
87
|
+
case 'continue':
|
|
88
|
+
await handleContinue(args);
|
|
89
|
+
break;
|
|
90
|
+
case 'read':
|
|
91
|
+
await handleRead(args);
|
|
92
|
+
break;
|
|
93
|
+
case 'models': {
|
|
94
|
+
const { handleModels } = require('../src/sidecar/models');
|
|
95
|
+
exitCode = await handleModels(args);
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
case 'setup':
|
|
99
|
+
await handleSetup(args);
|
|
100
|
+
break;
|
|
101
|
+
case 'abort':
|
|
102
|
+
await handleAbort(args);
|
|
103
|
+
break;
|
|
104
|
+
case 'mcp':
|
|
105
|
+
await handleMcp();
|
|
106
|
+
break;
|
|
107
|
+
case 'update':
|
|
108
|
+
await handleUpdate();
|
|
109
|
+
break;
|
|
110
|
+
default:
|
|
111
|
+
console.error(`Unknown command: ${command}`);
|
|
112
|
+
console.log(getUsage());
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
} catch (err) {
|
|
116
|
+
console.error(`Error: ${err.message}`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// F3 #15: one-shot commands must not hang on a lingering handle. The work is
|
|
121
|
+
// done here; give natural drain a brief grace, then force-exit as a net.
|
|
122
|
+
// (mcp is long-lived and never reaches this point.)
|
|
123
|
+
if (exitCode) { process.exitCode = exitCode; }
|
|
124
|
+
if (isOneShotCommand(command)) {
|
|
125
|
+
armExitWatchdog(exitCode, 1500, { log: (m, meta) => logger.debug(m, meta) });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
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
|
+
/**
|
|
259
|
+
* Handle 'sidecar list' command
|
|
260
|
+
* Spec Reference: §4.2
|
|
261
|
+
*/
|
|
262
|
+
async function handleList(args) {
|
|
263
|
+
const { listSidecars } = require('../src/index');
|
|
264
|
+
|
|
265
|
+
await listSidecars({
|
|
266
|
+
status: args.status,
|
|
267
|
+
all: args.all,
|
|
268
|
+
json: args.json,
|
|
269
|
+
project: args.cwd
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Handle 'sidecar resume' command
|
|
275
|
+
* Spec Reference: §4.3
|
|
276
|
+
*/
|
|
277
|
+
async function handleResume(args) {
|
|
278
|
+
const taskId = args._[1];
|
|
279
|
+
|
|
280
|
+
if (!taskId) {
|
|
281
|
+
console.error('Error: task_id is required for resume');
|
|
282
|
+
console.error('Usage: sidecar resume <task_id>');
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const taskIdCheck = validateTaskId(taskId);
|
|
287
|
+
if (!taskIdCheck.valid) {
|
|
288
|
+
console.error(taskIdCheck.error);
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const { resumeSidecar } = require('../src/index');
|
|
293
|
+
|
|
294
|
+
await resumeSidecar({
|
|
295
|
+
taskId,
|
|
296
|
+
project: args.cwd,
|
|
297
|
+
headless: args['no-ui'],
|
|
298
|
+
timeout: args.timeout
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Handle 'sidecar continue' command
|
|
304
|
+
* Spec Reference: §4.4
|
|
305
|
+
*/
|
|
306
|
+
async function handleContinue(args) {
|
|
307
|
+
const taskId = args._[1];
|
|
308
|
+
|
|
309
|
+
if (!taskId) {
|
|
310
|
+
console.error('Error: task_id is required for continue');
|
|
311
|
+
console.error('Usage: sidecar continue <task_id> --prompt "..."');
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const taskIdCheck = validateTaskId(taskId);
|
|
316
|
+
if (!taskIdCheck.valid) {
|
|
317
|
+
console.error(taskIdCheck.error);
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (!args.prompt && !args.briefing) {
|
|
322
|
+
console.error('Error: --prompt is required for continue');
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// F5: an explicitly passed --model gets the same resolution+validation as start.
|
|
327
|
+
if (args.model !== undefined) {
|
|
328
|
+
const { model, alias } = resolveModelFromArgs(args);
|
|
329
|
+
args.model = model;
|
|
330
|
+
args.model = await validateFallbackModel(args, alias);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const { continueSidecar } = require('../src/index');
|
|
334
|
+
|
|
335
|
+
await continueSidecar({
|
|
336
|
+
taskId,
|
|
337
|
+
newTaskId: args['task-id'],
|
|
338
|
+
briefing: args.prompt || args.briefing,
|
|
339
|
+
model: args.model,
|
|
340
|
+
project: args.cwd,
|
|
341
|
+
contextTurns: args['context-turns'],
|
|
342
|
+
contextMaxTokens: args['context-max-tokens'],
|
|
343
|
+
headless: args['no-ui'],
|
|
344
|
+
timeout: args.timeout
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
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
|
+
// Run main
|
|
379
|
+
main().catch(err => {
|
|
380
|
+
console.error(`Fatal error: ${err.message}`);
|
|
381
|
+
process.exit(1);
|
|
382
|
+
});
|
|
Binary file
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg width="1024" height="1024" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<rect width="16" height="16" rx="3" fill="#2D2B2A"/>
|
|
3
|
+
<path d="M3 2v12" stroke="#D97757" stroke-width="2" stroke-linecap="round"/>
|
|
4
|
+
<path d="M10 2v5c0 2-3 3-7 5" stroke="#D97757" stroke-width="2" stroke-linecap="round" stroke-opacity="0.6"/>
|
|
5
|
+
</svg>
|
package/electron/fold.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fold Logic
|
|
3
|
+
*
|
|
4
|
+
* Handles the fold action: showing overlay, requesting summary,
|
|
5
|
+
* outputting fold data to stdout, and closing the window.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { logger } = require('../src/utils/logger');
|
|
9
|
+
const { requestSummaryFromModel } = require('./summary');
|
|
10
|
+
const { getSummaryTemplate } = require('../src/prompt-builder');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a fold handler bound to the window state
|
|
14
|
+
* @param {object} state - Shared state object
|
|
15
|
+
* @param {string} state.model - Model name
|
|
16
|
+
* @param {string} state.client - Client type
|
|
17
|
+
* @param {string} state.cwd - Working directory
|
|
18
|
+
* @param {string} state.sessionId - OpenCode session ID
|
|
19
|
+
* @param {string} state.taskId - Sidecar task ID
|
|
20
|
+
* @param {number} state.port - OpenCode server port
|
|
21
|
+
* @returns {{ triggerFold: Function, hasFolded: Function }}
|
|
22
|
+
*/
|
|
23
|
+
function createFoldHandler(state) {
|
|
24
|
+
let folded = false;
|
|
25
|
+
|
|
26
|
+
async function triggerFold(mainWindow, contentView) {
|
|
27
|
+
if (folded) { return; }
|
|
28
|
+
folded = true;
|
|
29
|
+
|
|
30
|
+
// Show fold progress in toolbar and content overlay
|
|
31
|
+
showFoldOverlay(mainWindow, contentView);
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// Ask the model to generate a structured summary
|
|
35
|
+
let summary = '';
|
|
36
|
+
try {
|
|
37
|
+
summary = await requestSummaryFromModel(
|
|
38
|
+
state.sessionId, state.port, getSummaryTemplate
|
|
39
|
+
);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
logger.warn('Failed to get model summary', { error: err.message });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const output = [
|
|
45
|
+
'[SIDECAR_FOLD]',
|
|
46
|
+
`Model: ${state.model}`,
|
|
47
|
+
`Session: ${state.sessionId || state.taskId}`,
|
|
48
|
+
`Client: ${state.client}`,
|
|
49
|
+
`CWD: ${state.cwd}`,
|
|
50
|
+
`Mode: interactive`,
|
|
51
|
+
'---',
|
|
52
|
+
summary || 'Session ended without summary.'
|
|
53
|
+
].join('\n');
|
|
54
|
+
|
|
55
|
+
process.stdout.write(output + '\n');
|
|
56
|
+
logger.info('Fold completed', { taskId: state.taskId });
|
|
57
|
+
|
|
58
|
+
// Show nudge overlay before closing
|
|
59
|
+
if (contentView) {
|
|
60
|
+
await contentView.webContents.executeJavaScript(`
|
|
61
|
+
(function() {
|
|
62
|
+
var overlay = document.getElementById('sidecar-fold-overlay');
|
|
63
|
+
if (overlay) {
|
|
64
|
+
while (overlay.firstChild) { overlay.removeChild(overlay.firstChild); }
|
|
65
|
+
var msg = document.createElement('div');
|
|
66
|
+
msg.style.cssText = 'color:#E8E0D8;font-family:-apple-system,BlinkMacSystemFont,sans-serif;font-size:15px;font-weight:500;text-align:center;max-width:320px;';
|
|
67
|
+
msg.textContent = 'Summary saved. Tell Claude you\\u2019re done with the Amicus session so it can read the results.';
|
|
68
|
+
overlay.appendChild(msg);
|
|
69
|
+
}
|
|
70
|
+
})();
|
|
71
|
+
`).catch(() => {});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Wait 2.5s for user to read the nudge, then close
|
|
75
|
+
await new Promise(resolve => setTimeout(resolve, 2500));
|
|
76
|
+
} catch (err) {
|
|
77
|
+
logger.error('Fold failed', { error: err.message });
|
|
78
|
+
folded = false;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Close the window after fold
|
|
83
|
+
const { app } = require('electron');
|
|
84
|
+
if (mainWindow) {
|
|
85
|
+
mainWindow.close();
|
|
86
|
+
} else {
|
|
87
|
+
app.quit();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function hasFolded() {
|
|
92
|
+
return folded;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { triggerFold, hasFolded };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Show fold progress overlay in both the toolbar and the content view.
|
|
100
|
+
* Note: The JS strings below contain only hardcoded markup (no user input),
|
|
101
|
+
* so there is no XSS risk from DOM manipulation.
|
|
102
|
+
*/
|
|
103
|
+
function showFoldOverlay(mainWindow, contentView) {
|
|
104
|
+
if (mainWindow) {
|
|
105
|
+
mainWindow.webContents.executeJavaScript(`
|
|
106
|
+
(function() {
|
|
107
|
+
var btn = document.getElementById('fold-btn');
|
|
108
|
+
if (btn) {
|
|
109
|
+
btn.textContent = '';
|
|
110
|
+
var span = document.createElement('span');
|
|
111
|
+
span.style.cssText = 'display:inline-flex;align-items:center;gap:6px;';
|
|
112
|
+
var spinner = document.createElement('span');
|
|
113
|
+
spinner.style.cssText = 'width:12px;height:12px;border:2px solid rgba(255,255,255,0.3);border-top-color:#fff;border-radius:50%;animation:spin 0.8s linear infinite;display:inline-block;';
|
|
114
|
+
span.appendChild(spinner);
|
|
115
|
+
span.appendChild(document.createTextNode('Generating summary\\u2026'));
|
|
116
|
+
btn.appendChild(span);
|
|
117
|
+
btn.disabled = true;
|
|
118
|
+
btn.style.opacity = '0.85';
|
|
119
|
+
btn.style.cursor = 'default';
|
|
120
|
+
}
|
|
121
|
+
if (!document.getElementById('fold-spin-style')) {
|
|
122
|
+
var style = document.createElement('style');
|
|
123
|
+
style.id = 'fold-spin-style';
|
|
124
|
+
style.textContent = '@keyframes spin { to { transform: rotate(360deg); } }';
|
|
125
|
+
document.head.appendChild(style);
|
|
126
|
+
}
|
|
127
|
+
})();
|
|
128
|
+
`).catch(() => {});
|
|
129
|
+
}
|
|
130
|
+
if (contentView) {
|
|
131
|
+
contentView.webContents.executeJavaScript(`
|
|
132
|
+
(function() {
|
|
133
|
+
if (!document.getElementById('fold-spin-style')) {
|
|
134
|
+
var style = document.createElement('style');
|
|
135
|
+
style.id = 'fold-spin-style';
|
|
136
|
+
style.textContent = '@keyframes spin { to { transform: rotate(360deg); } }';
|
|
137
|
+
document.head.appendChild(style);
|
|
138
|
+
}
|
|
139
|
+
var overlay = document.createElement('div');
|
|
140
|
+
overlay.id = 'sidecar-fold-overlay';
|
|
141
|
+
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.55);display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:99999;';
|
|
142
|
+
|
|
143
|
+
var spinDiv = document.createElement('div');
|
|
144
|
+
spinDiv.style.cssText = 'width:32px;height:32px;border:3px solid rgba(217,119,87,0.3);border-top-color:#D97757;border-radius:50%;animation:spin 0.8s linear infinite;margin-bottom:16px;';
|
|
145
|
+
overlay.appendChild(spinDiv);
|
|
146
|
+
|
|
147
|
+
var titleDiv = document.createElement('div');
|
|
148
|
+
titleDiv.style.cssText = 'color:#E8E0D8;font-family:-apple-system,BlinkMacSystemFont,sans-serif;font-size:15px;font-weight:500;';
|
|
149
|
+
titleDiv.textContent = 'Generating summary\\u2026';
|
|
150
|
+
overlay.appendChild(titleDiv);
|
|
151
|
+
|
|
152
|
+
var subtitleDiv = document.createElement('div');
|
|
153
|
+
subtitleDiv.style.cssText = 'color:#7A756F;font-family:-apple-system,BlinkMacSystemFont,sans-serif;font-size:12px;margin-top:6px;';
|
|
154
|
+
subtitleDiv.textContent = 'Folding session back to Claude Code';
|
|
155
|
+
overlay.appendChild(subtitleDiv);
|
|
156
|
+
|
|
157
|
+
document.body.appendChild(overlay);
|
|
158
|
+
})();
|
|
159
|
+
`).catch(() => {});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = { createFoldHandler, showFoldOverlay };
|