@link-assistant/hive-mind 0.39.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 (63) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSE +24 -0
  3. package/README.md +769 -0
  4. package/package.json +58 -0
  5. package/src/agent.lib.mjs +705 -0
  6. package/src/agent.prompts.lib.mjs +196 -0
  7. package/src/buildUserMention.lib.mjs +71 -0
  8. package/src/claude-limits.lib.mjs +389 -0
  9. package/src/claude.lib.mjs +1445 -0
  10. package/src/claude.prompts.lib.mjs +203 -0
  11. package/src/codex.lib.mjs +552 -0
  12. package/src/codex.prompts.lib.mjs +194 -0
  13. package/src/config.lib.mjs +207 -0
  14. package/src/contributing-guidelines.lib.mjs +268 -0
  15. package/src/exit-handler.lib.mjs +205 -0
  16. package/src/git.lib.mjs +145 -0
  17. package/src/github-issue-creator.lib.mjs +246 -0
  18. package/src/github-linking.lib.mjs +152 -0
  19. package/src/github.batch.lib.mjs +272 -0
  20. package/src/github.graphql.lib.mjs +258 -0
  21. package/src/github.lib.mjs +1479 -0
  22. package/src/hive.config.lib.mjs +254 -0
  23. package/src/hive.mjs +1500 -0
  24. package/src/instrument.mjs +191 -0
  25. package/src/interactive-mode.lib.mjs +1000 -0
  26. package/src/lenv-reader.lib.mjs +206 -0
  27. package/src/lib.mjs +490 -0
  28. package/src/lino.lib.mjs +176 -0
  29. package/src/local-ci-checks.lib.mjs +324 -0
  30. package/src/memory-check.mjs +419 -0
  31. package/src/model-mapping.lib.mjs +145 -0
  32. package/src/model-validation.lib.mjs +278 -0
  33. package/src/opencode.lib.mjs +479 -0
  34. package/src/opencode.prompts.lib.mjs +194 -0
  35. package/src/protect-branch.mjs +159 -0
  36. package/src/review.mjs +433 -0
  37. package/src/reviewers-hive.mjs +643 -0
  38. package/src/sentry.lib.mjs +284 -0
  39. package/src/solve.auto-continue.lib.mjs +568 -0
  40. package/src/solve.auto-pr.lib.mjs +1374 -0
  41. package/src/solve.branch-errors.lib.mjs +341 -0
  42. package/src/solve.branch.lib.mjs +230 -0
  43. package/src/solve.config.lib.mjs +342 -0
  44. package/src/solve.error-handlers.lib.mjs +256 -0
  45. package/src/solve.execution.lib.mjs +291 -0
  46. package/src/solve.feedback.lib.mjs +436 -0
  47. package/src/solve.mjs +1128 -0
  48. package/src/solve.preparation.lib.mjs +210 -0
  49. package/src/solve.repo-setup.lib.mjs +114 -0
  50. package/src/solve.repository.lib.mjs +961 -0
  51. package/src/solve.results.lib.mjs +558 -0
  52. package/src/solve.session.lib.mjs +135 -0
  53. package/src/solve.validation.lib.mjs +325 -0
  54. package/src/solve.watch.lib.mjs +572 -0
  55. package/src/start-screen.mjs +324 -0
  56. package/src/task.mjs +308 -0
  57. package/src/telegram-bot.mjs +1481 -0
  58. package/src/telegram-markdown.lib.mjs +64 -0
  59. package/src/usage-limit.lib.mjs +218 -0
  60. package/src/version.lib.mjs +41 -0
  61. package/src/youtrack/solve.youtrack.lib.mjs +116 -0
  62. package/src/youtrack/youtrack-sync.mjs +219 -0
  63. package/src/youtrack/youtrack.lib.mjs +425 -0
@@ -0,0 +1,419 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Check if use is already defined (when imported from solve.mjs)
4
+ // If not, fetch it (when running standalone)
5
+ if (typeof globalThis.use === 'undefined') {
6
+ globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
7
+ }
8
+ const use = globalThis.use;
9
+
10
+ // Temporarily unset CI to avoid command-stream trace logs
11
+ const originalCI = process.env.CI;
12
+ delete process.env.CI;
13
+
14
+ const { $ } = await use('command-stream');
15
+ // Create a silent version of $ that doesn't mirror output to stdout
16
+ // Note: command-stream may still emit trace logs to stderr in some environments
17
+ // These are filtered out by consuming code when parsing JSON
18
+ const $silent = $({ mirror: false, capture: true });
19
+
20
+ const yargsModule = await use('yargs@17.7.2');
21
+ const yargs = yargsModule.default || yargsModule;
22
+ const { hideBin } = await use('yargs@17.7.2/helpers');
23
+ const fs = (await use('fs')).promises;
24
+
25
+ // Import log function from lib.mjs
26
+ const lib = await import('./lib.mjs');
27
+ const { log: libLog, setLogFile } = lib;
28
+
29
+ // Function to check available disk space
30
+ export const checkDiskSpace = async (minSpaceMB = 500, options = {}) => {
31
+ const log = options.log || libLog;
32
+
33
+ try {
34
+ let availableMB;
35
+
36
+ if (process.platform === 'darwin') {
37
+ // macOS: use df -m (megabytes) and get the 4th column
38
+ const { stdout } = await $silent`df -m . 2>/dev/null | tail -1 | awk '{print $4}'`;
39
+ availableMB = parseInt(stdout.toString().trim());
40
+ } else if (process.platform === 'win32') {
41
+ // Windows: use PowerShell to get free space
42
+ const { stdout } = await $silent`powershell -Command "(Get-PSDrive -Name (Get-Location).Drive.Name).Free / 1MB"`;
43
+ availableMB = Math.floor(parseFloat(stdout.toString().trim()));
44
+ } else {
45
+ // Linux: use df -BM and get the 4th column
46
+ const { stdout } = await $silent`df -BM . 2>/dev/null | tail -1 | awk '{print $4}'`;
47
+ availableMB = parseInt(stdout.toString().replace('M', ''));
48
+ }
49
+
50
+ if (isNaN(availableMB)) {
51
+ await log('❌ Failed to parse disk space information');
52
+ return { success: false, availableMB: 0, error: 'Failed to parse disk space' };
53
+ }
54
+
55
+ if (availableMB < minSpaceMB) {
56
+ await log(`❌ Insufficient disk space: ${availableMB}MB available, ${minSpaceMB}MB required`);
57
+ await log(' This may prevent successful operations.');
58
+ await log(' Please free up disk space and try again.');
59
+ return { success: false, availableMB, required: minSpaceMB };
60
+ }
61
+
62
+ await log(`💾 Disk space check: ${availableMB}MB available (${minSpaceMB}MB required) ✅`);
63
+ return { success: true, availableMB, required: minSpaceMB };
64
+ } catch (error) {
65
+ await log(`❌ Could not check disk space: ${error.message}`);
66
+ return { success: false, availableMB: 0, error: error.message };
67
+ }
68
+ };
69
+
70
+ // Function to check available RAM (volatile memory)
71
+ export const checkRAM = async (minMemoryMB = 256, options = {}) => {
72
+ const log = options.log || libLog;
73
+
74
+ // Check platform first
75
+ if (process.platform === 'darwin') {
76
+ // macOS RAM check using vm_stat
77
+ try {
78
+ const { stdout: vmStatOutput } = await $silent`vm_stat 2>/dev/null`;
79
+
80
+ // Parse page size
81
+ const pageSizeMatch = vmStatOutput.toString().match(/page size of (\d+) bytes/);
82
+ const pageSize = pageSizeMatch ? parseInt(pageSizeMatch[1]) : 16384; // Default to 16KB
83
+
84
+ // Parse free pages (handle dots in numbers like "6009.")
85
+ const freeMatch = vmStatOutput.toString().match(/Pages free:\s+([\d.]+)/);
86
+ const freePages = freeMatch ? parseInt(freeMatch[1].replace(/\./g, '')) : 0;
87
+
88
+ // Parse inactive pages (can be reclaimed)
89
+ const inactiveMatch = vmStatOutput.toString().match(/Pages inactive:\s+([\d.]+)/);
90
+ const inactivePages = inactiveMatch ? parseInt(inactiveMatch[1].replace(/\./g, '')) : 0;
91
+
92
+ // Parse purgeable pages (can be freed)
93
+ const purgeableMatch = vmStatOutput.toString().match(/Pages purgeable:\s+([\d.]+)/);
94
+ const purgeablePages = purgeableMatch ? parseInt(purgeableMatch[1].replace(/\./g, '')) : 0;
95
+
96
+ // Calculate available memory (free + inactive + purgeable)
97
+ const availablePages = freePages + inactivePages + purgeablePages;
98
+ const availableBytes = availablePages * pageSize;
99
+ const availableMB = Math.floor(availableBytes / (1024 * 1024));
100
+
101
+ // Check swap status
102
+ const { stdout: swapEnabledOutput } = await $silent`sysctl vm.swap_enabled 2>/dev/null`;
103
+ const swapEnabled = swapEnabledOutput.toString().includes('1');
104
+
105
+ // Get swap usage details
106
+ const { stdout: swapUsageOutput } = await $silent`sysctl vm.swapusage 2>/dev/null`;
107
+
108
+ // Parse swap info
109
+ const swapMatch = swapUsageOutput.toString().match(/total = ([\d.]+)M\s+used = ([\d.]+)M/);
110
+ const swapTotal = swapMatch ? parseFloat(swapMatch[1]) : 0;
111
+ const swapUsed = swapMatch ? parseFloat(swapMatch[2]) : 0;
112
+ const swapAvailable = swapTotal - swapUsed;
113
+
114
+ let swapInfo;
115
+ if (swapEnabled) {
116
+ if (swapTotal > 0) {
117
+ swapInfo = `${Math.round(swapTotal)}MB (${Math.round(swapUsed)}MB used)`;
118
+ } else {
119
+ swapInfo = 'enabled (dynamic allocation)';
120
+ }
121
+ } else {
122
+ swapInfo = 'disabled';
123
+ }
124
+
125
+ // Calculate total available memory (RAM + swap)
126
+ const totalAvailable = availableMB + (swapEnabled && swapTotal > 0 ? Math.round(swapAvailable) : 0);
127
+
128
+ // Check only total memory against the requirement
129
+ const success = totalAvailable >= minMemoryMB;
130
+
131
+ if (!success) {
132
+ await log(`❌ Insufficient memory: ${availableMB}MB available, swap: ${swapInfo}, total: ${totalAvailable}MB (${minMemoryMB}MB required)`);
133
+
134
+ if (!swapEnabled) {
135
+ await log(' Swap is disabled. Consider enabling swap:');
136
+ await log(' sudo launchctl load -w /System/Library/LaunchDaemons/com.apple.dynamic_pager.plist');
137
+ }
138
+
139
+ return { success: false, availableMB, required: minMemoryMB, swap: swapInfo, totalAvailable };
140
+ }
141
+
142
+ await log(`🧠 Memory check: ${availableMB}MB available, swap: ${swapInfo}, total: ${totalAvailable}MB (${minMemoryMB}MB required) ✅`);
143
+ return { success: true, availableMB, required: minMemoryMB, swap: swapInfo, totalAvailable };
144
+
145
+ } catch (error) {
146
+ await log(`❌ macOS memory check failed: ${error.message}`);
147
+ return { success: false, availableMB: 0, error: error.message };
148
+ }
149
+ } else if (process.platform === 'win32') {
150
+ // Windows memory check using PowerShell
151
+ try {
152
+ const { stdout: memOutput } = await $silent`powershell -Command "Get-CimInstance Win32_OperatingSystem | Select-Object @{Name='AvailableMB';Expression={[math]::Round($_.FreePhysicalMemory/1024)}}, @{Name='TotalPageFileMB';Expression={[math]::Round($_.TotalVirtualMemorySize/1024)}}, @{Name='FreePageFileMB';Expression={[math]::Round($_.FreeVirtualMemory/1024)}} | ConvertTo-Json"`;
153
+
154
+ const memInfo = JSON.parse(memOutput.toString());
155
+ const availableMB = memInfo.AvailableMB;
156
+ const pageFileTotalMB = memInfo.TotalPageFileMB || 0;
157
+ const pageFileFreeMB = memInfo.FreePageFileMB || 0;
158
+ const pageFileUsedMB = pageFileTotalMB - pageFileFreeMB;
159
+
160
+ let swapInfo;
161
+ if (pageFileTotalMB > 0) {
162
+ swapInfo = `${pageFileTotalMB}MB (${pageFileUsedMB}MB used)`;
163
+ } else {
164
+ swapInfo = 'none';
165
+ }
166
+
167
+ // Calculate total available memory (RAM + page file)
168
+ const totalAvailable = availableMB + (pageFileFreeMB || 0);
169
+
170
+ // Check only total memory against the requirement
171
+ const success = totalAvailable >= minMemoryMB;
172
+
173
+ if (!success) {
174
+ await log(`❌ Insufficient memory: ${availableMB}MB available, page file: ${swapInfo}, total: ${totalAvailable}MB (${minMemoryMB}MB required)`);
175
+ await log(' Consider closing some applications or increasing virtual memory.');
176
+ return { success: false, availableMB, required: minMemoryMB, swap: swapInfo, totalAvailable };
177
+ }
178
+
179
+ await log(`🧠 Memory check: ${availableMB}MB available, page file: ${swapInfo}, total: ${totalAvailable}MB (${minMemoryMB}MB required) ✅`);
180
+ return { success: true, availableMB, required: minMemoryMB, swap: swapInfo, totalAvailable };
181
+
182
+ } catch (error) {
183
+ await log(`❌ Windows memory check failed: ${error.message}`);
184
+ return { success: false, availableMB: 0, error: error.message };
185
+ }
186
+ } else {
187
+ // Linux memory check using /proc/meminfo
188
+ try {
189
+ const meminfoContent = await fs.readFile('/proc/meminfo', 'utf8');
190
+ const lines = meminfoContent.split('\n');
191
+
192
+ const getValue = (key) => {
193
+ const line = lines.find(l => l.startsWith(key));
194
+ if (!line) return 0;
195
+ const match = line.match(/(\d+)/);
196
+ return match ? parseInt(match[1]) : 0;
197
+ };
198
+
199
+ // Get memory values in KB
200
+ const memFree = getValue('MemFree:');
201
+ const buffers = getValue('Buffers:');
202
+ const cached = getValue('Cached:');
203
+ const sReclaimable = getValue('SReclaimable:');
204
+ const swapTotal = getValue('SwapTotal:');
205
+ const swapFree = getValue('SwapFree:');
206
+
207
+ // Calculate available memory (similar to 'free' command)
208
+ const availableKB = memFree + buffers + cached + sReclaimable;
209
+ const availableMB = Math.floor(availableKB / 1024);
210
+
211
+ // Calculate swap info
212
+ const swapUsedKB = swapTotal - swapFree;
213
+ const swapMB = Math.floor(swapTotal / 1024);
214
+ const swapUsedMB = Math.floor(swapUsedKB / 1024);
215
+ const swapAvailableMB = Math.floor(swapFree / 1024);
216
+
217
+ let swapInfo;
218
+ if (swapTotal > 0) {
219
+ swapInfo = `${swapMB}MB (${swapUsedMB}MB used)`;
220
+ } else {
221
+ swapInfo = 'none';
222
+ }
223
+
224
+ // Calculate total available memory (RAM + swap)
225
+ const totalAvailable = availableMB + swapAvailableMB;
226
+
227
+ // Check only total memory against the requirement
228
+ const success = totalAvailable >= minMemoryMB;
229
+
230
+ if (!success) {
231
+ await log(`❌ Insufficient memory: ${availableMB}MB available, swap: ${swapInfo}, total: ${totalAvailable}MB (${minMemoryMB}MB required)`);
232
+
233
+ if (swapTotal === 0) {
234
+ await log(' No swap configured. Consider adding swap:');
235
+ await log(' sudo fallocate -l 2G /swapfile');
236
+ await log(' sudo chmod 600 /swapfile');
237
+ await log(' sudo mkswap /swapfile');
238
+ await log(' sudo swapon /swapfile');
239
+ await log(' echo "/swapfile none swap sw 0 0" | sudo tee -a /etc/fstab');
240
+ }
241
+
242
+ return { success: false, availableMB, required: minMemoryMB, swap: swapInfo, totalAvailable };
243
+ }
244
+
245
+ await log(`🧠 Memory check: ${availableMB}MB available, swap: ${swapInfo}, total: ${totalAvailable}MB (${minMemoryMB}MB required) ✅`);
246
+ return { success: true, availableMB, required: minMemoryMB, swap: swapInfo, totalAvailable };
247
+
248
+ } catch (error) {
249
+ await log(`❌ Linux memory check failed: ${error.message}`);
250
+ return { success: false, availableMB: 0, error: error.message };
251
+ }
252
+ }
253
+ };
254
+
255
+ // Keep checkMemory as an alias for checkRAM for backward compatibility
256
+ export const checkMemory = checkRAM;
257
+
258
+ // Function to get system resource snapshot
259
+ export const getResourceSnapshot = async () => {
260
+ try {
261
+ if (process.platform === 'darwin') {
262
+ // macOS resource snapshot
263
+ const vmStat = await $silent`vm_stat 2>/dev/null | head -10`;
264
+ const uptime = await $silent`uptime 2>/dev/null`;
265
+ const swap = await $silent`sysctl vm.swapusage 2>/dev/null`;
266
+
267
+ return {
268
+ timestamp: new Date().toISOString(),
269
+ memory: vmStat.stdout.toString().trim(),
270
+ swap: swap.stdout.toString().trim(),
271
+ uptime: uptime.stdout.toString().trim()
272
+ };
273
+ } else {
274
+ // Linux resource snapshot
275
+ const memInfo = await $silent`grep -E "MemTotal|MemAvailable|MemFree|SwapTotal|SwapFree" /proc/meminfo 2>/dev/null`;
276
+ const loadAvg = await $silent`cat /proc/loadavg`;
277
+ const uptime = await $silent`uptime`;
278
+
279
+ return {
280
+ timestamp: new Date().toISOString(),
281
+ memory: memInfo.stdout.toString().trim(),
282
+ load: loadAvg.stdout.toString().trim(),
283
+ uptime: uptime.stdout.toString().trim()
284
+ };
285
+ }
286
+ } catch (error) {
287
+ return {
288
+ timestamp: new Date().toISOString(),
289
+ error: `Failed to get resource snapshot: ${error.message}`
290
+ };
291
+ }
292
+ };
293
+
294
+ // Combined system check function
295
+ export const checkSystem = async (requirements = {}, options = {}) => {
296
+ const {
297
+ minMemoryMB = 256,
298
+ minDiskSpaceMB = 500,
299
+ exitOnFailure = false
300
+ } = requirements;
301
+
302
+ // Note: log is passed through options to checkDiskSpace and checkRAM
303
+ const results = {
304
+ ram: null,
305
+ disk: null,
306
+ success: true
307
+ };
308
+
309
+ // Check disk space (persistent memory)
310
+ results.disk = await checkDiskSpace(minDiskSpaceMB, options);
311
+ if (!results.disk.success) {
312
+ results.success = false;
313
+ if (exitOnFailure) {
314
+ process.exit(1);
315
+ }
316
+ }
317
+
318
+ // Check RAM (volatile memory)
319
+ results.ram = await checkRAM(minMemoryMB, options);
320
+ if (!results.ram.success) {
321
+ results.success = false;
322
+ if (exitOnFailure) {
323
+ process.exit(1);
324
+ }
325
+ }
326
+
327
+ return results;
328
+ };
329
+
330
+ // CLI interface when run directly
331
+ if (import.meta.url === `file://${process.argv[1]}`) {
332
+
333
+ // Create yargs instance with all options
334
+ const yargsInstance = yargs(hideBin(process.argv))
335
+ .scriptName('memory-check.mjs')
336
+ .usage('Usage: $0 [options]')
337
+ .option('min-memory', {
338
+ alias: 'm',
339
+ type: 'number',
340
+ description: 'Minimum required memory in MB',
341
+ default: 256
342
+ })
343
+ .option('min-disk-space', {
344
+ alias: 'd',
345
+ type: 'number',
346
+ description: 'Minimum required disk space in MB',
347
+ default: 500
348
+ })
349
+ .option('exit-on-failure', {
350
+ alias: 'e',
351
+ type: 'boolean',
352
+ description: 'Exit with code 1 if any check fails',
353
+ default: false
354
+ })
355
+ .option('json', {
356
+ alias: 'j',
357
+ type: 'boolean',
358
+ description: 'Output results as JSON',
359
+ default: false
360
+ })
361
+ .option('quiet', {
362
+ alias: 'q',
363
+ type: 'boolean',
364
+ description: 'Suppress detailed output (only show final status)',
365
+ default: false
366
+ })
367
+ .option('log-file', {
368
+ alias: 'l',
369
+ type: 'string',
370
+ description: 'Path to log file for output'
371
+ })
372
+ .help('h')
373
+ .alias('h', 'help');
374
+
375
+ // Check for help before parsing
376
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
377
+ yargsInstance.showHelp();
378
+ process.exit(0);
379
+ }
380
+
381
+ const argv = await yargsInstance.parseAsync();
382
+
383
+ // If we get here, help wasn't requested or yargs didn't handle it
384
+ // Set up logging based on options
385
+ if (argv.logFile) {
386
+ setLogFile(argv.logFile);
387
+ }
388
+
389
+ // Create appropriate log function based on quiet mode
390
+ const log = argv.quiet ? async () => {} : libLog;
391
+
392
+ const results = await checkSystem(
393
+ {
394
+ minMemoryMB: argv.minMemory,
395
+ minDiskSpaceMB: argv.minDiskSpace,
396
+ exitOnFailure: argv.exitOnFailure
397
+ },
398
+ { log }
399
+ );
400
+
401
+ if (argv.json) {
402
+ console.log(JSON.stringify(results, null, 2));
403
+ } else if (!argv.quiet) {
404
+ console.log('\n📊 System Check Summary:');
405
+ console.log('─'.repeat(40));
406
+ console.log(`RAM: ${results.ram.success ? '✅' : '❌'} ${results.ram.availableMB}MB available (${results.ram.required}MB required)`);
407
+ console.log(`Disk: ${results.disk.success ? '✅' : '❌'} ${results.disk.availableMB}MB available (${results.disk.required}MB required)`);
408
+ console.log(`Overall: ${results.success ? '✅ All checks passed' : '❌ Some checks failed'}`);
409
+ }
410
+
411
+ if (!results.success && argv.exitOnFailure) {
412
+ process.exit(1);
413
+ }
414
+ }
415
+
416
+ // Restore CI if it was set (at the very end, after yargs has processed everything)
417
+ if (originalCI !== undefined) {
418
+ process.env.CI = originalCI;
419
+ }
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Unified model mapping module
5
+ * Provides a single source of truth for model name mapping across all tools
6
+ */
7
+
8
+ // Claude models (Anthropic API)
9
+ export const claudeModels = {
10
+ 'sonnet': 'claude-sonnet-4-5-20250929', // Sonnet 4.5
11
+ 'opus': 'claude-opus-4-5-20251101', // Opus 4.5
12
+ 'haiku': 'claude-haiku-4-5-20251001', // Haiku 4.5
13
+ 'haiku-3-5': 'claude-3-5-haiku-20241022', // Haiku 3.5
14
+ 'haiku-3': 'claude-3-haiku-20240307', // Haiku 3
15
+ };
16
+
17
+ // Agent models (OpenCode API via agent CLI)
18
+ export const agentModels = {
19
+ 'grok': 'opencode/grok-code',
20
+ 'grok-code': 'opencode/grok-code',
21
+ 'grok-code-fast-1': 'opencode/grok-code',
22
+ 'big-pickle': 'opencode/big-pickle',
23
+ 'gpt-5-nano': 'openai/gpt-5-nano',
24
+ 'sonnet': 'anthropic/claude-3-5-sonnet',
25
+ 'haiku': 'anthropic/claude-3-5-haiku',
26
+ 'opus': 'anthropic/claude-3-opus',
27
+ 'gemini-3-pro': 'google/gemini-3-pro',
28
+ };
29
+
30
+ // OpenCode models (OpenCode API)
31
+ export const opencodeModels = {
32
+ 'gpt4': 'openai/gpt-4',
33
+ 'gpt4o': 'openai/gpt-4o',
34
+ 'claude': 'anthropic/claude-3-5-sonnet',
35
+ 'sonnet': 'anthropic/claude-3-5-sonnet',
36
+ 'opus': 'anthropic/claude-3-opus',
37
+ 'gemini': 'google/gemini-pro',
38
+ 'grok': 'opencode/grok-code',
39
+ 'grok-code': 'opencode/grok-code',
40
+ 'grok-code-fast-1': 'opencode/grok-code',
41
+ };
42
+
43
+ // Codex models (OpenAI API)
44
+ export const codexModels = {
45
+ 'gpt5': 'gpt-5',
46
+ 'gpt5-codex': 'gpt-5-codex',
47
+ 'o3': 'o3',
48
+ 'o3-mini': 'o3-mini',
49
+ 'gpt4': 'gpt-4',
50
+ 'gpt4o': 'gpt-4o',
51
+ 'claude': 'claude-3-5-sonnet',
52
+ 'sonnet': 'claude-3-5-sonnet',
53
+ 'opus': 'claude-3-opus',
54
+ };
55
+
56
+ /**
57
+ * Map model name to full model ID for a specific tool
58
+ * @param {string} tool - The tool name (claude, agent, opencode, codex)
59
+ * @param {string} model - The model name or alias
60
+ * @returns {string} The full model ID
61
+ */
62
+ export const mapModelForTool = (tool, model) => {
63
+ switch (tool) {
64
+ case 'claude':
65
+ return claudeModels[model] || model;
66
+ case 'agent':
67
+ return agentModels[model] || model;
68
+ case 'opencode':
69
+ return opencodeModels[model] || model;
70
+ case 'codex':
71
+ return codexModels[model] || model;
72
+ default:
73
+ return model;
74
+ }
75
+ };
76
+
77
+ /**
78
+ * Validate if a model is compatible with a tool
79
+ * @param {string} tool - The tool name (claude, agent, opencode, codex)
80
+ * @param {string} model - The model name or alias
81
+ * @returns {boolean} True if the model is compatible with the tool
82
+ */
83
+ export const isModelCompatibleWithTool = (tool, model) => {
84
+ const mappedModel = mapModelForTool(tool, model);
85
+
86
+ switch (tool) {
87
+ case 'claude':
88
+ // Claude only accepts models in the claude- namespace
89
+ return mappedModel.startsWith('claude-');
90
+ case 'agent':
91
+ // Agent accepts any model with provider prefix (opencode/, anthropic/, etc.)
92
+ // or models in the agentModels list
93
+ return mappedModel.includes('/') || Object.keys(agentModels).includes(model);
94
+ case 'opencode':
95
+ // OpenCode accepts models with provider prefix
96
+ return mappedModel.includes('/') || Object.keys(opencodeModels).includes(model);
97
+ case 'codex':
98
+ // Codex accepts OpenAI and some Claude models
99
+ return Object.keys(codexModels).includes(model) ||
100
+ mappedModel.startsWith('gpt-') ||
101
+ mappedModel.startsWith('o3') ||
102
+ mappedModel.startsWith('claude-');
103
+ default:
104
+ return true;
105
+ }
106
+ };
107
+
108
+ /**
109
+ * Get a list of valid model names for a tool
110
+ * @param {string} tool - The tool name
111
+ * @returns {string[]} Array of valid model names
112
+ */
113
+ export const getValidModelsForTool = (tool) => {
114
+ switch (tool) {
115
+ case 'claude':
116
+ return Object.keys(claudeModels);
117
+ case 'agent':
118
+ return Object.keys(agentModels);
119
+ case 'opencode':
120
+ return Object.keys(opencodeModels);
121
+ case 'codex':
122
+ return Object.keys(codexModels);
123
+ default:
124
+ return [];
125
+ }
126
+ };
127
+
128
+ /**
129
+ * Validate tool-model compatibility and throw descriptive error if invalid
130
+ * @param {string} tool - The tool name
131
+ * @param {string} model - The model name
132
+ * @throws {Error} If the model is not compatible with the tool
133
+ */
134
+ export const validateToolModelCompatibility = (tool, model) => {
135
+ if (!isModelCompatibleWithTool(tool, model)) {
136
+ const validModels = getValidModelsForTool(tool);
137
+ const mappedModel = mapModelForTool(tool, model);
138
+
139
+ throw new Error(
140
+ `Model '${model}' (mapped to '${mappedModel}') is not compatible with --tool ${tool}.\n` +
141
+ `Valid models for ${tool}: ${validModels.join(', ')}\n` +
142
+ 'Hint: Different tools use different model APIs and naming conventions.'
143
+ );
144
+ }
145
+ };