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
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input Validators
|
|
3
|
+
*
|
|
4
|
+
* Validation helper functions for CLI argument validation.
|
|
5
|
+
* These validators run before sidecar launch to fail fast with clear errors.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { isValidAgent, isHeadlessSafe, OPENCODE_AGENTS } = require('./agent-mapping');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Valid agent modes for --agent option
|
|
14
|
+
* These are OpenCode's native agents.
|
|
15
|
+
* Custom agents defined in ~/.config/opencode/agents/ are also accepted.
|
|
16
|
+
*/
|
|
17
|
+
const VALID_AGENT_MODES = OPENCODE_AGENTS;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Provider to API key mapping
|
|
21
|
+
*/
|
|
22
|
+
const PROVIDER_KEY_MAP = {
|
|
23
|
+
'openrouter': { key: 'OPENROUTER_API_KEY', name: 'OpenRouter' },
|
|
24
|
+
'google': { key: 'GOOGLE_GENERATIVE_AI_API_KEY', name: 'Google Gemini' },
|
|
25
|
+
'openai': { key: 'OPENAI_API_KEY', name: 'OpenAI' },
|
|
26
|
+
'anthropic': { key: 'ANTHROPIC_API_KEY', name: 'Anthropic' },
|
|
27
|
+
'deepseek': { key: 'DEEPSEEK_API_KEY', name: 'DeepSeek' },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Task ID format: alphanumeric, hyphens, underscores, 1-64 chars */
|
|
31
|
+
const TASK_ID_PATTERN = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Validate a task ID format (safe for use in file paths)
|
|
35
|
+
* @param {string} taskId
|
|
36
|
+
* @returns {{valid: boolean, error?: string}}
|
|
37
|
+
*/
|
|
38
|
+
function validateTaskId(taskId) {
|
|
39
|
+
if (!taskId) {
|
|
40
|
+
return { valid: false, error: 'Task ID is required' };
|
|
41
|
+
}
|
|
42
|
+
if (!TASK_ID_PATTERN.test(taskId)) {
|
|
43
|
+
return { valid: false, error: 'Invalid task ID format. Must be 1-64 alphanumeric, hyphen, or underscore characters.' };
|
|
44
|
+
}
|
|
45
|
+
return { valid: true };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { SESSIONS_DIR, LEGACY_SESSIONS_DIR } = require('../session-manager');
|
|
49
|
+
|
|
50
|
+
/** Resolve a session path under a single root, throwing on path traversal. */
|
|
51
|
+
function safeSessionDirUnder(project, root, taskId) {
|
|
52
|
+
// Resolve so both sides share the same drive/separator form; on Windows path.join
|
|
53
|
+
// yields a driveless root (\tmp\...) while path.resolve(taskId) adds the drive.
|
|
54
|
+
const sessionsDir = path.resolve(path.join(project, '.claude', root));
|
|
55
|
+
const resolved = path.resolve(sessionsDir, taskId);
|
|
56
|
+
if (!resolved.startsWith(sessionsDir + path.sep)) {
|
|
57
|
+
throw new Error('Invalid task ID: path traversal detected');
|
|
58
|
+
}
|
|
59
|
+
return resolved;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Resolve an EXISTING session path: prefer canonical amicus, fall back to the
|
|
64
|
+
* legacy sidecar_sessions dir (shim). The traversal guard runs against BOTH
|
|
65
|
+
* roots, so a malicious taskId is rejected regardless of root.
|
|
66
|
+
* @throws {Error} If resolved path escapes the sessions directory
|
|
67
|
+
*/
|
|
68
|
+
function safeSessionDir(project, taskId) {
|
|
69
|
+
const canonical = safeSessionDirUnder(project, SESSIONS_DIR, taskId);
|
|
70
|
+
if (fs.existsSync(canonical)) { return canonical; }
|
|
71
|
+
const legacy = safeSessionDirUnder(project, LEGACY_SESSIONS_DIR, taskId);
|
|
72
|
+
if (fs.existsSync(legacy)) { return legacy; }
|
|
73
|
+
return canonical;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Validate prompt content is not empty or whitespace-only
|
|
78
|
+
* @param {string} prompt
|
|
79
|
+
* @returns {{valid: boolean, error?: string}}
|
|
80
|
+
*/
|
|
81
|
+
function validatePromptContent(prompt) {
|
|
82
|
+
if (!prompt || prompt.trim().length === 0) {
|
|
83
|
+
return { valid: false, error: 'Error: --prompt cannot be empty or whitespace-only' };
|
|
84
|
+
}
|
|
85
|
+
return { valid: true };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** @deprecated Use validatePromptContent instead */
|
|
89
|
+
const validateBriefingContent = validatePromptContent;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Validate cwd directory exists
|
|
93
|
+
* @param {string} cwdPath
|
|
94
|
+
* @returns {{valid: boolean, error?: string}}
|
|
95
|
+
*/
|
|
96
|
+
function validateCwdPath(cwdPath) {
|
|
97
|
+
// Skip validation if not provided (will use default)
|
|
98
|
+
if (!cwdPath) {
|
|
99
|
+
return { valid: true };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!fs.existsSync(cwdPath)) {
|
|
103
|
+
return { valid: false, error: `Error: --cwd path does not exist: ${cwdPath}` };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const stat = fs.statSync(cwdPath);
|
|
108
|
+
if (!stat.isDirectory()) {
|
|
109
|
+
return { valid: false, error: `Error: --cwd path is not a directory: ${cwdPath}` };
|
|
110
|
+
}
|
|
111
|
+
} catch (e) {
|
|
112
|
+
return { valid: false, error: `Error: --cwd path is not accessible: ${cwdPath}` };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { valid: true };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** @deprecated Use validateCwdPath instead */
|
|
119
|
+
const validateProjectPath = validateCwdPath;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Find a session file in Claude's project directories
|
|
123
|
+
* Claude stores sessions in subdirectories named after the hashed project path
|
|
124
|
+
* @param {string} baseDir - The base ~/.claude/projects directory
|
|
125
|
+
* @param {string} sessionId - The session ID to find
|
|
126
|
+
* @returns {boolean} Whether the session was found
|
|
127
|
+
*/
|
|
128
|
+
function findSessionInProjectDirs(baseDir, sessionId) {
|
|
129
|
+
if (!fs.existsSync(baseDir)) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const entries = fs.readdirSync(baseDir, { withFileTypes: true });
|
|
135
|
+
for (const entry of entries) {
|
|
136
|
+
if (entry.isDirectory()) {
|
|
137
|
+
const sessionFile = path.join(baseDir, entry.name, `${sessionId}.jsonl`);
|
|
138
|
+
if (fs.existsSync(sessionFile)) {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} catch (e) {
|
|
144
|
+
// Ignore errors reading directories
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Validate explicit session ID exists
|
|
152
|
+
* @param {string} session
|
|
153
|
+
* @param {string} _projectPath - Project path (unused, for future use)
|
|
154
|
+
* @returns {{valid: boolean, error?: string}}
|
|
155
|
+
*/
|
|
156
|
+
function validateExplicitSession(session, _projectPath) {
|
|
157
|
+
// Skip validation for 'current' or undefined (deferred resolution)
|
|
158
|
+
if (!session || session === 'current') {
|
|
159
|
+
return { valid: true };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Check in ~/.claude/projects subdirectories
|
|
163
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
164
|
+
const claudeProjectsDir = path.join(homeDir, '.claude', 'projects');
|
|
165
|
+
|
|
166
|
+
// Check if explicit session file exists in any project subdirectory
|
|
167
|
+
const found = findSessionInProjectDirs(claudeProjectsDir, session);
|
|
168
|
+
if (!found) {
|
|
169
|
+
return {
|
|
170
|
+
valid: false,
|
|
171
|
+
error: `Error: --session '${session}' not found. Use 'sidecar list' to see available sessions or omit --session for most recent.`
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { valid: true };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Validate agent mode
|
|
180
|
+
*
|
|
181
|
+
* Accepts:
|
|
182
|
+
* - OpenCode native agents: Build, Plan, General, Explore
|
|
183
|
+
* - Custom agents: any non-empty string (for user-defined OpenCode agents)
|
|
184
|
+
*
|
|
185
|
+
* @param {string} agent
|
|
186
|
+
* @returns {{valid: boolean, error?: string}}
|
|
187
|
+
*/
|
|
188
|
+
function validateAgentMode(agent) {
|
|
189
|
+
// Allow undefined/null - will default to Build
|
|
190
|
+
if (!agent) {
|
|
191
|
+
return { valid: true };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Use isValidAgent which accepts all non-empty strings
|
|
195
|
+
// This allows custom agents defined in user's OpenCode agent directory
|
|
196
|
+
if (!isValidAgent(agent)) {
|
|
197
|
+
return {
|
|
198
|
+
valid: false,
|
|
199
|
+
error: `Error: --agent cannot be empty. Examples: ${VALID_AGENT_MODES.join(', ')}`
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return { valid: true };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Validate agent is compatible with headless (--no-ui) mode.
|
|
208
|
+
* 'chat' agent requires user permission for writes/bash and stalls headless.
|
|
209
|
+
*
|
|
210
|
+
* @param {string} agent - Agent name
|
|
211
|
+
* @returns {{valid: boolean, error?: string, warning?: string}}
|
|
212
|
+
*/
|
|
213
|
+
function validateHeadlessAgent(agent) {
|
|
214
|
+
if (!agent) {
|
|
215
|
+
return { valid: true };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const safe = isHeadlessSafe(agent);
|
|
219
|
+
|
|
220
|
+
if (safe === false) {
|
|
221
|
+
return {
|
|
222
|
+
valid: false,
|
|
223
|
+
error: 'Error: --agent chat requires interactive mode (remove --no-ui or use --agent build)'
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (safe === null) {
|
|
228
|
+
return {
|
|
229
|
+
valid: true,
|
|
230
|
+
warning: `Warning: Custom agent '${agent}' may not be headless-safe. Ensure it does not require user interaction.`
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return { valid: true };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const { validateMcpSpec, validateMcpConfigFile } = require('./mcp-validators');
|
|
238
|
+
const { MODEL_THINKING_SUPPORT, getSupportedThinkingLevels, validateThinkingLevel } = require('./thinking-validators');
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Validate API key is present for the given model's provider.
|
|
242
|
+
*
|
|
243
|
+
* Assumes loadCredentials() has already run, projecting all credential
|
|
244
|
+
* sources (sidecar .env, auth.json) into process.env.
|
|
245
|
+
*
|
|
246
|
+
* @param {string} model - The model string (e.g., 'openrouter/google/gemini-2.5-flash')
|
|
247
|
+
* @returns {{valid: boolean, error?: string}}
|
|
248
|
+
*/
|
|
249
|
+
function validateApiKey(model) {
|
|
250
|
+
if (!model) {
|
|
251
|
+
return { valid: true };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const provider = model.split('/')[0].toLowerCase();
|
|
255
|
+
const providerInfo = PROVIDER_KEY_MAP[provider];
|
|
256
|
+
|
|
257
|
+
if (!providerInfo) {
|
|
258
|
+
return { valid: true };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!process.env[providerInfo.key]) {
|
|
262
|
+
return {
|
|
263
|
+
valid: false,
|
|
264
|
+
error: `Error: ${providerInfo.key} not found.\n\n` +
|
|
265
|
+
'In non-interactive shells (Claude Code, CI), ~/.zshrc is not sourced.\n' +
|
|
266
|
+
'Fix with one of:\n' +
|
|
267
|
+
' - Run `sidecar setup` to store keys in sidecar\'s config\n' +
|
|
268
|
+
' - Move your export to ~/.zshenv (sourced by all zsh shells)\n' +
|
|
269
|
+
' - Add key to ~/.local/share/opencode/auth.json'
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return { valid: true };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
module.exports = {
|
|
277
|
+
VALID_AGENT_MODES,
|
|
278
|
+
PROVIDER_KEY_MAP,
|
|
279
|
+
MODEL_THINKING_SUPPORT,
|
|
280
|
+
TASK_ID_PATTERN,
|
|
281
|
+
validateTaskId,
|
|
282
|
+
safeSessionDir,
|
|
283
|
+
validatePromptContent,
|
|
284
|
+
validateCwdPath,
|
|
285
|
+
// Backward-compatible aliases
|
|
286
|
+
validateBriefingContent,
|
|
287
|
+
validateProjectPath,
|
|
288
|
+
validateExplicitSession,
|
|
289
|
+
validateAgentMode,
|
|
290
|
+
validateHeadlessAgent,
|
|
291
|
+
validateMcpSpec,
|
|
292
|
+
validateMcpConfigFile,
|
|
293
|
+
validateApiKey,
|
|
294
|
+
validateThinkingLevel,
|
|
295
|
+
getSupportedThinkingLevels,
|
|
296
|
+
findSessionInProjectDirs,
|
|
297
|
+
// Re-exported from input-validators.js
|
|
298
|
+
validateStartInputs: require('./input-validators').validateStartInputs,
|
|
299
|
+
findSimilar: require('./input-validators').findSimilar,
|
|
300
|
+
};
|