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/src/cli.js
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Argument Parser
|
|
3
|
+
*
|
|
4
|
+
* Spec Reference: §4 CLI Interface
|
|
5
|
+
* Parses command line arguments for all sidecar commands.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
validatePromptContent,
|
|
10
|
+
validateCwdPath,
|
|
11
|
+
validateExplicitSession,
|
|
12
|
+
validateAgentMode,
|
|
13
|
+
validateHeadlessAgent,
|
|
14
|
+
validateMcpSpec,
|
|
15
|
+
validateMcpConfigFile,
|
|
16
|
+
validateApiKey,
|
|
17
|
+
validateThinkingLevel
|
|
18
|
+
} = require('./utils/validators');
|
|
19
|
+
const { logger } = require('./utils/logger');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Default values per spec §4.1
|
|
23
|
+
*/
|
|
24
|
+
const DEFAULTS = {
|
|
25
|
+
'session-id': 'current',
|
|
26
|
+
cwd: process.cwd(),
|
|
27
|
+
'context-turns': 50,
|
|
28
|
+
'context-max-tokens': 80000,
|
|
29
|
+
timeout: 15,
|
|
30
|
+
'no-ui': false,
|
|
31
|
+
'summary-length': 'normal', // Default summary length
|
|
32
|
+
position: 'right' // Default window position
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse command line arguments
|
|
37
|
+
* @param {string[]} argv - Command line arguments (without node and script name)
|
|
38
|
+
* @returns {object} Parsed arguments
|
|
39
|
+
*/
|
|
40
|
+
function parseArgs(argv) {
|
|
41
|
+
const result = {
|
|
42
|
+
_: [],
|
|
43
|
+
...DEFAULTS
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < argv.length; i++) {
|
|
47
|
+
const arg = argv[i];
|
|
48
|
+
|
|
49
|
+
if (arg.startsWith('--')) {
|
|
50
|
+
let key = arg.slice(2);
|
|
51
|
+
const next = argv[i + 1];
|
|
52
|
+
|
|
53
|
+
// Handle --key=value syntax
|
|
54
|
+
let inlineValue;
|
|
55
|
+
const eqIdx = key.indexOf('=');
|
|
56
|
+
if (eqIdx !== -1) {
|
|
57
|
+
inlineValue = key.slice(eqIdx + 1);
|
|
58
|
+
key = key.slice(0, eqIdx);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Boolean flags (no value expected)
|
|
62
|
+
if (isBooleanFlag(key)) {
|
|
63
|
+
result[key] = true;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// If --key=value was used, use the inline value directly
|
|
68
|
+
if (inlineValue !== undefined) {
|
|
69
|
+
result[key] = parseValue(key, inlineValue);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Array accumulation flags
|
|
74
|
+
if (key === 'exclude-mcp' && next && !next.startsWith('--')) {
|
|
75
|
+
result['exclude-mcp'] = result['exclude-mcp'] || [];
|
|
76
|
+
result['exclude-mcp'].push(next);
|
|
77
|
+
i++;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Options with values
|
|
82
|
+
if (next && !next.startsWith('--')) {
|
|
83
|
+
result[key] = parseValue(key, next);
|
|
84
|
+
i++;
|
|
85
|
+
} else {
|
|
86
|
+
result[key] = true;
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
result._.push(arg);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Check if a flag is boolean (doesn't take a value)
|
|
98
|
+
*/
|
|
99
|
+
function isBooleanFlag(key) {
|
|
100
|
+
const booleanFlags = [
|
|
101
|
+
'no-ui',
|
|
102
|
+
'no-mcp',
|
|
103
|
+
'no-context',
|
|
104
|
+
'setup',
|
|
105
|
+
'all',
|
|
106
|
+
// 'summary', // summary is now an option with a value
|
|
107
|
+
'conversation',
|
|
108
|
+
'json',
|
|
109
|
+
'version',
|
|
110
|
+
'help',
|
|
111
|
+
'api-keys',
|
|
112
|
+
'validate-model',
|
|
113
|
+
'no-validate-model'
|
|
114
|
+
];
|
|
115
|
+
return booleanFlags.includes(key);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Parse a value to the appropriate type
|
|
120
|
+
*/
|
|
121
|
+
function parseValue(key, value) {
|
|
122
|
+
// Numeric options
|
|
123
|
+
const numericOptions = ['context-turns', 'context-max-tokens', 'timeout', 'opencode-port'];
|
|
124
|
+
if (numericOptions.includes(key)) {
|
|
125
|
+
return parseInt(value, 10);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Specific string options
|
|
129
|
+
if (key === 'summary-length') {
|
|
130
|
+
const validLengths = ['brief', 'normal', 'verbose'];
|
|
131
|
+
if (!validLengths.includes(value.toLowerCase())) {
|
|
132
|
+
logger.warn('Invalid summary-length value, using default', { value, default: 'normal' });
|
|
133
|
+
return DEFAULTS['summary-length'];
|
|
134
|
+
}
|
|
135
|
+
return value.toLowerCase();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Validate arguments for the 'start' command
|
|
143
|
+
* @param {object} args - Parsed arguments
|
|
144
|
+
* @returns {{ valid: boolean, error?: string }}
|
|
145
|
+
*/
|
|
146
|
+
function validateStartArgs(args) {
|
|
147
|
+
// Required: --prompt (presence check)
|
|
148
|
+
if (!args.prompt) {
|
|
149
|
+
return { valid: false, error: 'Error: --prompt is required' };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Validate prompt content (not empty/whitespace-only)
|
|
153
|
+
const promptCheck = validatePromptContent(args.prompt);
|
|
154
|
+
if (!promptCheck.valid) {
|
|
155
|
+
return promptCheck;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Validate model format if model is present (model is resolved externally via resolveModel)
|
|
159
|
+
if (args.model && !isValidModelFormat(args.model)) {
|
|
160
|
+
return { valid: false, error: 'Error: --model must be in format provider/model (e.g., google/gemini-2.5-flash) or openrouter/provider/model' };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Validate cwd path exists (if provided)
|
|
164
|
+
const cwdCheck = validateCwdPath(args.cwd);
|
|
165
|
+
if (!cwdCheck.valid) {
|
|
166
|
+
return cwdCheck;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Validate explicit session ID exists (if not 'current')
|
|
170
|
+
const sessionCheck = validateExplicitSession(args['session-id'], args.cwd);
|
|
171
|
+
if (!sessionCheck.valid) {
|
|
172
|
+
return sessionCheck;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Validate agent mode (if provided)
|
|
176
|
+
const agentCheck = validateAgentMode(args.agent);
|
|
177
|
+
if (!agentCheck.valid) {
|
|
178
|
+
return agentCheck;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Validate agent is headless-safe when --no-ui is set
|
|
182
|
+
let headlessWarning;
|
|
183
|
+
if (args['no-ui']) {
|
|
184
|
+
const headlessCheck = validateHeadlessAgent(args.agent);
|
|
185
|
+
if (!headlessCheck.valid) {
|
|
186
|
+
return headlessCheck;
|
|
187
|
+
}
|
|
188
|
+
if (headlessCheck.warning) {
|
|
189
|
+
logger.warn('Custom agent headless warning', { warning: headlessCheck.warning });
|
|
190
|
+
headlessWarning = headlessCheck.warning;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Validate --client (if provided)
|
|
195
|
+
if (args.client) {
|
|
196
|
+
const validClients = ['code-local', 'code-web', 'cowork'];
|
|
197
|
+
if (!validClients.includes(args.client)) {
|
|
198
|
+
return { valid: false, error: `Error: --client must be one of: ${validClients.join(', ')}` };
|
|
199
|
+
}
|
|
200
|
+
// Require --session-dir when client is code-web
|
|
201
|
+
if (args.client === 'code-web' && !args['session-dir']) {
|
|
202
|
+
return { valid: false, error: 'Error: --session-dir is required when --client is code-web' };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Validate MCP spec format (if provided)
|
|
207
|
+
const mcpCheck = validateMcpSpec(args.mcp);
|
|
208
|
+
if (!mcpCheck.valid) {
|
|
209
|
+
return mcpCheck;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Validate MCP config file (if provided)
|
|
213
|
+
const mcpConfigCheck = validateMcpConfigFile(args['mcp-config']);
|
|
214
|
+
if (!mcpConfigCheck.valid) {
|
|
215
|
+
return mcpConfigCheck;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Validate timeout is positive
|
|
219
|
+
if (args.timeout !== undefined && args.timeout <= 0) {
|
|
220
|
+
return { valid: false, error: 'Error: --timeout must be a positive number' };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Validate context-turns is positive
|
|
224
|
+
if (args['context-turns'] !== undefined && args['context-turns'] <= 0) {
|
|
225
|
+
return { valid: false, error: 'Error: --context-turns must be a positive number' };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Validate context-since format if provided
|
|
229
|
+
if (args['context-since'] && !isValidDurationFormat(args['context-since'])) {
|
|
230
|
+
return { valid: false, error: 'Error: --context-since must be in format like 30m, 2h, or 1d' };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Validate summary-length
|
|
234
|
+
const validSummaryLengths = ['brief', 'normal', 'verbose'];
|
|
235
|
+
if (args['summary-length'] && !validSummaryLengths.includes(args['summary-length'])) {
|
|
236
|
+
return { valid: false, error: `Error: --summary-length must be one of: ${validSummaryLengths.join(', ')}` };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Validate thinking effort level (if provided), with model-specific support check
|
|
240
|
+
const thinkingCheck = validateThinkingLevel(args.thinking, args.model);
|
|
241
|
+
if (!thinkingCheck.valid) {
|
|
242
|
+
return thinkingCheck;
|
|
243
|
+
}
|
|
244
|
+
// If model doesn't support the level, adjust it and warn
|
|
245
|
+
if (thinkingCheck.warning) {
|
|
246
|
+
logger.warn('Thinking level adjusted', { warning: thinkingCheck.warning, adjustedLevel: thinkingCheck.adjustedLevel });
|
|
247
|
+
args.thinking = thinkingCheck.adjustedLevel;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Validate API key is present for the model's provider
|
|
251
|
+
const apiKeyCheck = validateApiKey(args.model);
|
|
252
|
+
if (!apiKeyCheck.valid) {
|
|
253
|
+
return apiKeyCheck;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const result = { valid: true };
|
|
257
|
+
if (headlessWarning) {
|
|
258
|
+
result.warning = headlessWarning;
|
|
259
|
+
}
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Check if model format is valid
|
|
265
|
+
* Supports:
|
|
266
|
+
* - Direct API: provider/model (e.g., google/gemini-2.5-flash)
|
|
267
|
+
* - OpenRouter: openrouter/provider/model (e.g., openrouter/google/gemini-2.5-flash)
|
|
268
|
+
*/
|
|
269
|
+
function isValidModelFormat(model) {
|
|
270
|
+
const parts = model.split('/');
|
|
271
|
+
// Must have at least 2 parts (provider/model) and at most 3 (openrouter/provider/model)
|
|
272
|
+
if (parts.length < 2 || parts.length > 3) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
// All parts must be non-empty
|
|
276
|
+
return parts.every(part => part.length > 0);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Check if duration format is valid (e.g., 30m, 2h, 1d)
|
|
281
|
+
*/
|
|
282
|
+
function isValidDurationFormat(duration) {
|
|
283
|
+
return /^\d+[mhd]$/.test(duration);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Get usage text
|
|
288
|
+
*/
|
|
289
|
+
function getUsage() {
|
|
290
|
+
return `
|
|
291
|
+
Usage: amicus <command> [options]
|
|
292
|
+
|
|
293
|
+
Commands:
|
|
294
|
+
start Launch a new amicus session
|
|
295
|
+
fanout Run N models on the same prompt in parallel (headless)
|
|
296
|
+
list Show previous sessions
|
|
297
|
+
resume Reopen a previous session
|
|
298
|
+
continue New session building on previous
|
|
299
|
+
read Output session summary/conversation
|
|
300
|
+
models List/search the model catalog, refresh it, audit aliases
|
|
301
|
+
abort Abort a running session (or --all)
|
|
302
|
+
setup Configure default model and aliases
|
|
303
|
+
--api-keys Open API key setup window
|
|
304
|
+
--add-alias <name=model> Add a model alias without the full wizard
|
|
305
|
+
update Update to latest version
|
|
306
|
+
mcp Start MCP server (stdio transport)
|
|
307
|
+
|
|
308
|
+
Options for 'start':
|
|
309
|
+
--model <model> Optional (uses config default). Model to use:
|
|
310
|
+
- Short aliases: gemini, opus, gpt (see 'amicus setup')
|
|
311
|
+
- Direct API: google/gemini-2.5-flash
|
|
312
|
+
- OpenRouter: openrouter/google/gemini-2.5-flash
|
|
313
|
+
--prompt <text> Required. Task description
|
|
314
|
+
--prompt-file <path> Read the prompt from a UTF-8 file (XOR --prompt)
|
|
315
|
+
--json With --no-ui: emit the run result as stable JSON
|
|
316
|
+
--agent <agent> OpenCode agent to use (see Agent Types below)
|
|
317
|
+
--session-id <id|"current"> Session ID to pull context from (default: current)
|
|
318
|
+
--cwd <path> Project directory (default: cwd)
|
|
319
|
+
--no-ui Run without GUI (autonomous mode)
|
|
320
|
+
--no-context Skip parent conversation history context
|
|
321
|
+
--timeout <minutes> Headless timeout (default: 15)
|
|
322
|
+
--client <type> Client type: code-local, code-web, cowork
|
|
323
|
+
--session-dir <path> Explicit session data directory
|
|
324
|
+
--setup Force open configuration
|
|
325
|
+
--fold-shortcut <key> Customize fold shortcut
|
|
326
|
+
--opencode-port <port> Port override for OpenCode server
|
|
327
|
+
--context-turns <N> Max conversation turns (default: 50)
|
|
328
|
+
--context-since <duration> Time filter (e.g., 2h). Overrides turns.
|
|
329
|
+
--context-max-tokens <N> Max context tokens (default: 80000)
|
|
330
|
+
--summary-length <length> Summary verbosity: brief, normal (default), verbose
|
|
331
|
+
--thinking <level> Reasoning effort: none, minimal, low, medium, high, xhigh
|
|
332
|
+
--mcp <spec> Add MCP server. Formats:
|
|
333
|
+
- name=url (remote server)
|
|
334
|
+
- name=command (local server)
|
|
335
|
+
--mcp-config <path> Path to opencode.json with MCP config
|
|
336
|
+
--no-mcp Don't inherit MCP servers from parent LLM
|
|
337
|
+
--exclude-mcp <name> Exclude specific MCP server (repeatable)
|
|
338
|
+
--validate-model (Deprecated: validation is on by default)
|
|
339
|
+
--no-validate-model Skip model-catalog validation before launch
|
|
340
|
+
--position <pos> Window position: right (default), left, center
|
|
341
|
+
|
|
342
|
+
Options for 'fanout':
|
|
343
|
+
--models <a,b,c> Required. Comma-separated aliases or provider/model IDs
|
|
344
|
+
--prompt <text> Task briefing (or use --prompt-file)
|
|
345
|
+
--prompt-file <path> Read the briefing from a UTF-8 file (avoids the
|
|
346
|
+
~32KB Windows argument cap). Mutually exclusive
|
|
347
|
+
with --prompt. Also works with 'start'.
|
|
348
|
+
--wave-id <id> Explicit wave ID (leg IDs become <id>-1..N)
|
|
349
|
+
--json Emit the wave result as stable JSON on stdout
|
|
350
|
+
Shared per-leg knobs: --agent, --thinking, --timeout, --summary-length,
|
|
351
|
+
--no-context, --context-*, --mcp*, --no-validate-model, --cwd
|
|
352
|
+
Exit codes: 0 all legs complete, 2 partial, 1 none complete / hard failure
|
|
353
|
+
|
|
354
|
+
Options for 'models':
|
|
355
|
+
--search <q> Filter by substring over model id and name
|
|
356
|
+
--refresh Force-refresh the catalog from provider APIs
|
|
357
|
+
--check Audit aliases against the catalog (exit = stale count)
|
|
358
|
+
--json Machine-readable output
|
|
359
|
+
|
|
360
|
+
Options for 'list':
|
|
361
|
+
--status <filter> Filter by status (running, complete)
|
|
362
|
+
--all Show all projects
|
|
363
|
+
--json Output as JSON
|
|
364
|
+
|
|
365
|
+
Options for 'abort':
|
|
366
|
+
--all Abort all running sessions in this project
|
|
367
|
+
|
|
368
|
+
Options for 'read':
|
|
369
|
+
--summary Show summary (default)
|
|
370
|
+
--conversation Show full conversation
|
|
371
|
+
--metadata Show session metadata
|
|
372
|
+
--json Emit the run/wave result as stable JSON
|
|
373
|
+
|
|
374
|
+
OpenCode Agent Types:
|
|
375
|
+
Chat Reads auto, writes/bash ask permission (interactive default)
|
|
376
|
+
Build Full tool access (headless default)
|
|
377
|
+
Plan Read-only analysis and planning
|
|
378
|
+
|
|
379
|
+
NOTE: --agent chat is interactive-only (incompatible with --no-ui).
|
|
380
|
+
Headless mode defaults to build agent.
|
|
381
|
+
|
|
382
|
+
Custom agents defined in ~/.config/opencode/agents/ or
|
|
383
|
+
.opencode/agents/ are also supported.
|
|
384
|
+
|
|
385
|
+
Examples:
|
|
386
|
+
amicus start --model google/gemini-2.5 --prompt "Debug auth issue"
|
|
387
|
+
amicus start --model openai/o3 --prompt "Generate tests" --no-ui
|
|
388
|
+
amicus start --model gemini --prompt "Review code" --agent Plan
|
|
389
|
+
amicus list
|
|
390
|
+
amicus resume abc123
|
|
391
|
+
amicus read abc123 --conversation
|
|
392
|
+
`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
module.exports = {
|
|
396
|
+
parseArgs,
|
|
397
|
+
validateStartArgs,
|
|
398
|
+
getUsage,
|
|
399
|
+
DEFAULTS
|
|
400
|
+
};
|
package/src/conflict.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Conflict Detection Module
|
|
3
|
+
*
|
|
4
|
+
* Spec Reference: Section 7.2 File Conflict Detection
|
|
5
|
+
* Detects conflicts between sidecar file modifications and external changes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Format a relative time string (e.g., "5 min ago")
|
|
13
|
+
*
|
|
14
|
+
* @param {Date|string} date - Date to format
|
|
15
|
+
* @returns {string} Relative time string
|
|
16
|
+
*/
|
|
17
|
+
function formatRelativeTime(date) {
|
|
18
|
+
const dateObj = date instanceof Date ? date : new Date(date);
|
|
19
|
+
const diffMs = Date.now() - dateObj.getTime();
|
|
20
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
21
|
+
|
|
22
|
+
if (diffMins < 1) {
|
|
23
|
+
return 'just now';
|
|
24
|
+
}
|
|
25
|
+
if (diffMins < 60) {
|
|
26
|
+
return `${diffMins} min ago`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
30
|
+
if (diffHours < 24) {
|
|
31
|
+
return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
35
|
+
return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Detect file conflicts between sidecar and external modifications
|
|
40
|
+
* Spec Reference: §7.2 File Conflict Detection
|
|
41
|
+
*
|
|
42
|
+
* Compares files written by the sidecar against their current modification times.
|
|
43
|
+
* A conflict exists when a file was modified externally after the sidecar session started.
|
|
44
|
+
*
|
|
45
|
+
* @param {object} sidecarFiles - Object containing file tracking info
|
|
46
|
+
* @param {string[]} sidecarFiles.written - Array of relative file paths written by sidecar
|
|
47
|
+
* @param {string} projectDir - Project directory path
|
|
48
|
+
* @param {Date|string} sessionStartTime - When the sidecar session started
|
|
49
|
+
* @returns {object[]} Array of conflict objects
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* const conflicts = detectConflicts(
|
|
53
|
+
* { written: ['src/auth/TokenManager.ts'] },
|
|
54
|
+
* '/path/to/project',
|
|
55
|
+
* new Date(Date.now() - 5 * 60 * 1000)
|
|
56
|
+
* );
|
|
57
|
+
* // Returns: [{ file: 'src/auth/TokenManager.ts', sidecarAction: 'write', externalMtime: Date }]
|
|
58
|
+
*/
|
|
59
|
+
function detectConflicts(sidecarFiles, projectDir, sessionStartTime) {
|
|
60
|
+
const conflicts = [];
|
|
61
|
+
|
|
62
|
+
// Handle missing or empty written files list
|
|
63
|
+
const writtenFiles = sidecarFiles?.written || [];
|
|
64
|
+
if (writtenFiles.length === 0) {
|
|
65
|
+
return conflicts;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Normalize session start time to Date object
|
|
69
|
+
const startTime = sessionStartTime instanceof Date
|
|
70
|
+
? sessionStartTime
|
|
71
|
+
: new Date(sessionStartTime);
|
|
72
|
+
|
|
73
|
+
for (const file of writtenFiles) {
|
|
74
|
+
const filePath = path.join(projectDir, file);
|
|
75
|
+
|
|
76
|
+
// Check if file exists
|
|
77
|
+
if (!fs.existsSync(filePath)) {
|
|
78
|
+
// File doesn't exist - no conflict (sidecar created it or it was deleted)
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const stat = fs.statSync(filePath);
|
|
84
|
+
|
|
85
|
+
// Check if file was modified after session started
|
|
86
|
+
if (stat.mtime > startTime) {
|
|
87
|
+
conflicts.push({
|
|
88
|
+
file,
|
|
89
|
+
sidecarAction: 'write',
|
|
90
|
+
externalMtime: stat.mtime
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
} catch (error) {
|
|
94
|
+
// Skip files we can't stat
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return conflicts;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Format conflict warning for summary output
|
|
104
|
+
* Spec Reference: §7.2 Conflict Warning in Summary
|
|
105
|
+
*
|
|
106
|
+
* @param {object[]} conflicts - Array of conflict objects
|
|
107
|
+
* @returns {string} Formatted warning string or empty string if no conflicts
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* formatConflictWarning([
|
|
111
|
+
* { file: 'src/auth/TokenManager.ts', sidecarAction: 'write', externalMtime: new Date() }
|
|
112
|
+
* ]);
|
|
113
|
+
* // Returns:
|
|
114
|
+
* // ⚠️ **FILE CONFLICT WARNING**
|
|
115
|
+
* // The following files were modified by both this sidecar AND externally:
|
|
116
|
+
* // - src/auth/TokenManager.ts (external change: 5 min ago)
|
|
117
|
+
* //
|
|
118
|
+
* // **Review these changes carefully before accepting.**
|
|
119
|
+
*/
|
|
120
|
+
function formatConflictWarning(conflicts) {
|
|
121
|
+
if (!conflicts || conflicts.length === 0) {
|
|
122
|
+
return '';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const lines = [
|
|
126
|
+
'\u26A0\uFE0F **FILE CONFLICT WARNING**',
|
|
127
|
+
'The following files were modified by both this sidecar AND externally:'
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
for (const conflict of conflicts) {
|
|
131
|
+
const relativeTime = formatRelativeTime(conflict.externalMtime);
|
|
132
|
+
lines.push(`- ${conflict.file} (external change: ${relativeTime})`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
lines.push('');
|
|
136
|
+
lines.push('**Review these changes carefully before accepting.**');
|
|
137
|
+
|
|
138
|
+
return lines.join('\n');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = {
|
|
142
|
+
detectConflicts,
|
|
143
|
+
formatConflictWarning
|
|
144
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Compression Module
|
|
3
|
+
*
|
|
4
|
+
* Provides token estimation and context compression utilities for
|
|
5
|
+
* sidecar sessions. Determines whether context needs model-based
|
|
6
|
+
* compression (delegated to the caller).
|
|
7
|
+
*
|
|
8
|
+
* @module context-compression
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { logger } = require('./utils/logger');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Default token limit for context compression
|
|
15
|
+
* @type {number}
|
|
16
|
+
*/
|
|
17
|
+
const DEFAULT_TOKEN_LIMIT = 30000;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Estimate the number of tokens in a text string.
|
|
21
|
+
* Uses a simple heuristic: ceil(length / 4).
|
|
22
|
+
*
|
|
23
|
+
* @param {string|null|undefined} text - The text to estimate tokens for
|
|
24
|
+
* @returns {number} Estimated token count (0 for empty/null/undefined)
|
|
25
|
+
*/
|
|
26
|
+
function estimateTokenCount(text) {
|
|
27
|
+
if (!text) {
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return Math.ceil(text.length / 4);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build a preamble string that identifies the working directory.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} cwd - The current working directory path
|
|
38
|
+
* @returns {string} Preamble string ending with double newline
|
|
39
|
+
*/
|
|
40
|
+
function buildPreamble(cwd) {
|
|
41
|
+
return `You are working in ${cwd}. Here is the conversation:\n\n`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Compress context text for sidecar sessions.
|
|
46
|
+
*
|
|
47
|
+
* If the estimated token count is within the limit, returns the text
|
|
48
|
+
* with a preamble prepended and compressed=false. If it exceeds the
|
|
49
|
+
* limit, returns the full text with compressed=true and
|
|
50
|
+
* needsModelCompression=true, signaling the caller to perform
|
|
51
|
+
* model-based compression.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} contextText - The raw context text to compress
|
|
54
|
+
* @param {Object} [options] - Compression options
|
|
55
|
+
* @param {string} [options.cwd=process.cwd()] - Working directory for preamble
|
|
56
|
+
* @param {number} [options.tokenLimit=30000] - Maximum token threshold
|
|
57
|
+
* @returns {Object} Compression result
|
|
58
|
+
* @returns {string} result.text - Preamble + context text
|
|
59
|
+
* @returns {boolean} result.compressed - Whether compression was triggered
|
|
60
|
+
* @returns {boolean} result.needsModelCompression - Whether caller should run model compression
|
|
61
|
+
* @returns {number} result.estimatedTokens - Estimated token count of the full text
|
|
62
|
+
*/
|
|
63
|
+
function compressContext(contextText, options = {}) {
|
|
64
|
+
const { cwd = process.cwd(), tokenLimit = DEFAULT_TOKEN_LIMIT } = options;
|
|
65
|
+
|
|
66
|
+
const preamble = buildPreamble(cwd);
|
|
67
|
+
const fullText = preamble + contextText;
|
|
68
|
+
const estimatedTokens = estimateTokenCount(fullText);
|
|
69
|
+
|
|
70
|
+
if (estimatedTokens <= tokenLimit) {
|
|
71
|
+
logger.debug('Context within token limit', {
|
|
72
|
+
estimatedTokens,
|
|
73
|
+
tokenLimit
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
text: fullText,
|
|
78
|
+
compressed: false,
|
|
79
|
+
needsModelCompression: false,
|
|
80
|
+
estimatedTokens
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
logger.info('Context exceeds token limit, model compression needed', {
|
|
85
|
+
estimatedTokens,
|
|
86
|
+
tokenLimit
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
text: fullText,
|
|
91
|
+
compressed: true,
|
|
92
|
+
needsModelCompression: true,
|
|
93
|
+
estimatedTokens
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = {
|
|
98
|
+
compressContext,
|
|
99
|
+
estimateTokenCount,
|
|
100
|
+
buildPreamble,
|
|
101
|
+
DEFAULT_TOKEN_LIMIT
|
|
102
|
+
};
|