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,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
+ };