apex-dev 2.0.0 → 3.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 (77) hide show
  1. package/.local/share/amp/history.jsonl +33 -0
  2. package/.local/share/amp/session.json +3 -3
  3. package/.local/share/amp/threads/T-019c9761-858c-719b-911f-bc2e4c8cbdde.json +188 -0
  4. package/.local/share/amp/threads/T-019c9761-f5f3-7606-a900-ebe7f10d6e37.json +121 -0
  5. package/.local/share/amp/threads/T-019c9763-b1ae-729d-90aa-f59938ce912e.json +799 -0
  6. package/.local/share/amp/threads/T-019c9769-4a8a-77b8-beab-f48973276f9a.json +1541 -0
  7. package/.local/share/amp/threads/T-019c9772-edac-7075-b26e-0ada1f8697d2.json +7 -0
  8. package/.local/share/amp/threads/T-019c97e8-a9ab-71a1-a8f9-109c540c98bf.json +111 -0
  9. package/.local/share/amp/threads/T-019c97e9-2277-753c-8c5d-df745fa6cfff.json +7 -0
  10. package/.local/share/amp/threads/T-019c97e9-f28e-758d-9663-e37047a8ed95.json +111 -0
  11. package/.local/share/amp/threads/T-019c97ea-17c7-77b8-92b2-f641c069bcc9.json +71 -0
  12. package/.local/share/amp/threads/T-019c97ea-44c6-75b8-88bc-d88113194f6a.json +1611 -0
  13. package/.local/share/amp/threads/T-019c97ec-abae-7251-a5f6-693adf496a1c.json +7 -0
  14. package/.local/share/amp/threads/T-019c97f5-8e61-73ad-8c5d-2637abedcde6.json +1341 -0
  15. package/.local/share/amp/threads/T-019c989d-4f4e-7249-bde0-21d19455ccae.json +163 -0
  16. package/.local/share/amp/threads/T-019c989d-9024-73c4-bee8-e2ae45028a39.json +124 -0
  17. package/.local/share/amp/threads/T-019c989e-1394-74ad-8234-ac573fcdb4c7.json +1260 -0
  18. package/.local/share/amp/threads/T-019c989f-e3dd-772e-85ac-525d0fc88fda.json +403 -0
  19. package/.local/share/amp/threads/T-019c98a1-7b0c-778a-b311-2e1cff85d710.json +3422 -0
  20. package/.local/share/amp/threads/T-019c98c5-4b7f-7284-99e9-88aa8c18ba66.json +1830 -0
  21. package/.local/share/amp/threads/T-019c98d0-f27f-76ec-be10-6df96f22be99.json +4061 -0
  22. package/.local/share/amp/threads/T-019c98f9-d031-704d-a0c2-f2f395f68f2b.json +509 -0
  23. package/.local/share/amp/threads/T-019c9919-f9ee-766c-90be-af7a07f6a4c6.json +2075 -0
  24. package/.local/share/amp/threads/T-019c991c-b98b-7158-9083-cc52408beb13.json +7 -0
  25. package/.local/share/amp/threads/T-019c991d-66d6-72aa-a9a1-105f7df0ea06.json +7 -0
  26. package/.local/share/amp/threads/T-019c9c2e-71a4-77ff-bd7f-b053da7f9000.json +1637 -0
  27. package/.local/share/amp/threads/T-019c9c45-27ca-728b-ba77-835115dfa9b2.json +3893 -0
  28. package/.local/share/amp/threads/T-019c9c48-45dc-736a-9752-e4119fe698f9.json +7 -0
  29. package/.local/share/amp/threads/T-019c9c4d-266b-72d0-b56e-74a5777e6583.json +7 -0
  30. package/.local/share/amp/threads/T-019c9c52-ab89-758f-9178-bda99c39d10b.json +7 -0
  31. package/.local/share/opencode/opencode.db +0 -0
  32. package/.local/share/opencode/opencode.db-shm +0 -0
  33. package/.local/share/opencode/opencode.db-wal +0 -0
  34. package/.local/share/opencode/storage/agent-usage-reminder/ses_36870ea98ffe8S5ZOCE4F11yFh.json +6 -0
  35. package/.local/share/opencode/storage/agent-usage-reminder/ses_3687a3e9affewUnHBzvpiPR6df.json +6 -0
  36. package/.local/share/opencode/storage/agent-usage-reminder/ses_36886e68dffeKVgUWf6lzXdEEt.json +6 -0
  37. package/.local/share/opencode/storage/session_diff/ses_36870ea98ffe8S5ZOCE4F11yFh.json +1 -0
  38. package/.local/share/opencode/storage/session_diff/ses_3687a3e9affewUnHBzvpiPR6df.json +1 -0
  39. package/.local/share/opencode/storage/session_diff/ses_36886e68dffeKVgUWf6lzXdEEt.json +1 -0
  40. package/.local/state/replit/log-query.db +0 -0
  41. package/.local/state/replit/log-query.db-shm +0 -0
  42. package/.local/state/replit/log-query.db-wal +0 -0
  43. package/.upm/store.json +1 -1
  44. package/AGENTS.md +32 -0
  45. package/bun.lock +137 -103
  46. package/index.jsx +24 -0
  47. package/package.json +9 -9
  48. package/src/agent.js +252 -169
  49. package/src/app.jsx +96 -0
  50. package/src/commands.js +66 -38
  51. package/src/components/AssistantMessage.jsx +83 -0
  52. package/src/components/ChatArea.jsx +84 -0
  53. package/src/components/DiffView.jsx +26 -0
  54. package/src/components/Divider.jsx +8 -0
  55. package/src/components/Header.jsx +44 -0
  56. package/src/components/HelpModal.jsx +81 -0
  57. package/src/components/InputBar.jsx +32 -0
  58. package/src/components/Spinner.jsx +23 -0
  59. package/src/components/StatusBar.jsx +44 -0
  60. package/src/components/SystemMessage.jsx +31 -0
  61. package/src/components/ThinkBlock.jsx +29 -0
  62. package/src/components/ToolCallItem.jsx +43 -0
  63. package/src/components/UserMessage.jsx +11 -0
  64. package/src/components/Welcome.jsx +14 -0
  65. package/src/config.js +118 -2
  66. package/src/hooks/useLayout.js +15 -0
  67. package/src/hooks/useStore.js +6 -0
  68. package/src/prompt.js +67 -48
  69. package/src/store.js +99 -0
  70. package/src/theme.js +19 -0
  71. package/src/thinking.js +0 -24
  72. package/src/toolExecutors.js +580 -23
  73. package/src/tools.js +146 -4
  74. package/src/utils.js +32 -0
  75. package/tsconfig.json +10 -0
  76. package/index.js +0 -92
  77. package/src/ui.js +0 -269
package/src/thinking.js CHANGED
@@ -1,7 +1,5 @@
1
1
  'use strict';
2
2
 
3
- const { t, indent } = require('./ui');
4
-
5
3
  function parseThinkBlocks(text) {
6
4
  const thinkRegex = /<think>([\s\S]*?)(?:<\/think>|think>)/g;
7
5
  const thoughts = [];
@@ -35,7 +33,6 @@ function splitAtPartialTag(text) {
35
33
  const prefixes = [
36
34
  '</think>', '</think', '</thin', '</thi', '</th', '</t', '</',
37
35
  '<think>', '<think', '<thin', '<thi', '<th', '<t',
38
- 'think>', 'think', 'thin', 'thi', 'th',
39
36
  '<',
40
37
  ];
41
38
  for (const prefix of prefixes) {
@@ -49,30 +46,9 @@ function splitAtPartialTag(text) {
49
46
  return { safe: text, pending: '' };
50
47
  }
51
48
 
52
- function showThinkingBlock(thoughts) {
53
- if (!thoughts.length) return;
54
- const combined = thoughts.join('\n\n');
55
- const lines = combined.split('\n');
56
- const maxLines = 8;
57
- const preview = lines.length > maxLines ? lines.slice(0, maxLines) : lines;
58
- const isTruncated = lines.length > maxLines;
59
-
60
- console.log(indent(t.dim.italic('▸ Thinking'), 2));
61
- for (const line of preview) {
62
- if (line.trim()) {
63
- console.log(indent(t.dim.italic(line), 4));
64
- }
65
- }
66
- if (isTruncated) {
67
- console.log(indent(t.dim.italic(`... +${lines.length - maxLines} more lines`), 4));
68
- }
69
- console.log();
70
- }
71
-
72
49
  module.exports = {
73
50
  parseThinkBlocks,
74
51
  findThinkClose,
75
52
  stripStrayCloseTag,
76
53
  splitAtPartialTag,
77
- showThinkingBlock,
78
54
  };
@@ -2,20 +2,67 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const https = require('https');
5
6
  const { execSync } = require('child_process');
6
7
  const {
7
8
  PROJECT_ROOT,
8
9
  TOOL_TIMEOUT,
9
10
  REVIEWER_MODEL,
10
11
  REVIEWER_SYSTEM_PROMPT,
12
+ FILE_PICKER_MODEL,
13
+ FILE_PICKER_SYSTEM_PROMPT,
14
+ THINKER_MODEL,
15
+ THINKER_SYSTEM_PROMPT,
16
+ COMMANDER_MODEL,
17
+ COMMANDER_SYSTEM_PROMPT,
18
+ CONTEXT_PRUNER_MODEL,
19
+ CONTEXT_PRUNER_SYSTEM_PROMPT,
20
+ SELECTOR_SYSTEM_PROMPT,
21
+ NVIDIA_MODEL,
11
22
  nvidiaClient,
12
23
  session,
13
24
  truncateOutput,
14
25
  resolvePath,
15
26
  sleep,
27
+ getMode,
16
28
  } = require('./config');
17
29
 
18
- async function executeTool(name, args) {
30
+ async function streamCompletion(params, onStream) {
31
+ for (let attempt = 0; attempt <= 2; attempt++) {
32
+ let content = '';
33
+ let reasoning = '';
34
+ try {
35
+ if (onStream) {
36
+ const stream = await nvidiaClient.chat.completions.create({ ...params, stream: true });
37
+ for await (const chunk of stream) {
38
+ const delta = chunk.choices?.[0]?.delta;
39
+ if (delta?.content) {
40
+ content += delta.content;
41
+ onStream(content || reasoning);
42
+ }
43
+ if (delta?.reasoning_content) {
44
+ reasoning += delta.reasoning_content;
45
+ onStream(content || reasoning);
46
+ }
47
+ }
48
+ return content || reasoning || '';
49
+ } else {
50
+ const response = await nvidiaClient.chat.completions.create(params);
51
+ return response.choices[0]?.message?.content
52
+ || response.choices[0]?.message?.reasoning_content
53
+ || '';
54
+ }
55
+ } catch (err) {
56
+ if (attempt < 2 && err.status >= 400 && err.status < 500) {
57
+ await sleep(1000 * Math.pow(2, attempt));
58
+ continue;
59
+ }
60
+ throw err;
61
+ }
62
+ }
63
+ }
64
+
65
+ async function executeTool(name, args, onStream) {
19
66
  try {
20
67
  switch (name) {
21
68
  case 'Read': {
@@ -215,6 +262,165 @@ async function executeTool(name, args) {
215
262
  return truncateOutput(`Task: ${args.description}\n${'─'.repeat(40)}\n${results.join('\n\n')}`);
216
263
  }
217
264
 
265
+ case 'WebSearch': {
266
+ const apiKey = process.env.EXA_API_KEY;
267
+ if (!apiKey) return 'Error: EXA_API_KEY environment variable is not set. Get one at https://dashboard.exa.ai/api-keys';
268
+
269
+ const body = JSON.stringify({
270
+ query: args.query,
271
+ numResults: Math.min(args.num_results || 5, 10),
272
+ type: args.type || 'auto',
273
+ ...(args.include_domains && { includeDomains: args.include_domains }),
274
+ ...(args.category && { category: args.category }),
275
+ contents: { highlights: { maxCharacters: 300 }, text: { maxCharacters: 1000 } },
276
+ });
277
+
278
+ const result = await new Promise((resolve) => {
279
+ const req = https.request({
280
+ hostname: 'api.exa.ai',
281
+ path: '/search',
282
+ method: 'POST',
283
+ headers: {
284
+ 'Content-Type': 'application/json',
285
+ 'x-api-key': apiKey,
286
+ },
287
+ }, (res) => {
288
+ let data = '';
289
+ res.on('data', (chunk) => data += chunk);
290
+ res.on('end', () => {
291
+ if (res.statusCode !== 200) {
292
+ resolve(`Error: Exa API returned ${res.statusCode}: ${data}`);
293
+ return;
294
+ }
295
+ try {
296
+ const json = JSON.parse(data);
297
+ if (!json.results || json.results.length === 0) {
298
+ resolve('No results found.');
299
+ return;
300
+ }
301
+ const formatted = json.results.map((r, i) => {
302
+ let entry = `${i + 1}. **${r.title || 'Untitled'}**\n ${r.url}`;
303
+ if (r.publishedDate) entry += `\n Published: ${r.publishedDate.split('T')[0]}`;
304
+ if (r.author) entry += `\n Author: ${r.author}`;
305
+ if (r.text) entry += `\n ${r.text.trim().slice(0, 500)}`;
306
+ else if (r.highlights && r.highlights.length) entry += `\n ${r.highlights[0].trim().slice(0, 300)}`;
307
+ return entry;
308
+ }).join('\n\n');
309
+ resolve(truncateOutput(`Web Search Results (${json.results.length}):\n${'─'.repeat(40)}\n${formatted}`));
310
+ } catch (e) {
311
+ resolve(`Error: Failed to parse Exa response: ${e.message}`);
312
+ }
313
+ });
314
+ });
315
+ req.on('error', (e) => resolve(`Error: Exa request failed: ${e.message}`));
316
+ req.setTimeout(15000, () => { req.destroy(); resolve('Error: Exa search timed out.'); });
317
+ req.write(body);
318
+ req.end();
319
+ });
320
+ return result;
321
+ }
322
+
323
+ case 'FilePickerMax': {
324
+ // Gather full directory tree
325
+ let tree = '';
326
+ try {
327
+ tree = execSync(
328
+ `find "${PROJECT_ROOT}" -not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/.cache/*" -not -path "*/.local/*" -not -path "*/.upm/*" -not -path "*/.config/*" 2>/dev/null | head -500`,
329
+ { encoding: 'utf-8', timeout: 15000 }
330
+ ).trim();
331
+ // Make paths relative
332
+ tree = tree.split('\n').map(f => path.relative(PROJECT_ROOT, f) || '.').join('\n');
333
+ } catch {
334
+ tree = '(failed to scan directory tree)';
335
+ }
336
+
337
+ // Gather previews of all source files (first 8 lines each)
338
+ const sourceExts = /\.(js|ts|jsx|tsx|py|rb|go|rs|java|c|cpp|h|hpp|css|scss|html|svelte|vue|json|yaml|yml|toml|md|sql|sh|bash|env|cfg|ini|xml)$/i;
339
+ const allFiles = tree.split('\n').filter(f => sourceExts.test(f));
340
+ const previews = [];
341
+ for (const relFile of allFiles.slice(0, 200)) {
342
+ const absFile = path.resolve(PROJECT_ROOT, relFile);
343
+ try {
344
+ const stat = fs.statSync(absFile, { throwIfNoEntry: false });
345
+ if (!stat || stat.isDirectory() || stat.size > 512 * 1024) continue;
346
+ const content = fs.readFileSync(absFile, 'utf-8');
347
+ const first8 = content.split('\n').slice(0, 8).join('\n');
348
+ previews.push(`--- ${relFile} ---\n${first8}`);
349
+ } catch { /* skip unreadable */ }
350
+ }
351
+
352
+ const pickerMessages = [
353
+ { role: 'system', content: FILE_PICKER_SYSTEM_PROMPT },
354
+ {
355
+ role: 'user',
356
+ content: `# Prompt\n${args.prompt}\n\n# Directory Tree\n${tree}\n\n# File Previews (first 8 lines each)\n${previews.join('\n\n')}`,
357
+ },
358
+ ];
359
+
360
+ try {
361
+ const header = `FilePickerMax Results\n${'─'.repeat(40)}\n`;
362
+ const streamCb = onStream ? (text) => onStream(truncateOutput(header + text)) : null;
363
+ const raw = await streamCompletion({
364
+ model: FILE_PICKER_MODEL,
365
+ messages: pickerMessages,
366
+ max_tokens: 4096,
367
+ temperature: 0.2,
368
+ }, streamCb) || '[]';
369
+ return truncateOutput(header + raw);
370
+ } catch (apiErr) {
371
+ return `Error: FilePickerMax failed — ${apiErr.message}`;
372
+ }
373
+ }
374
+
375
+ case 'TodoList': {
376
+ const todoFile = path.join(PROJECT_ROOT, '.apex-todos.json');
377
+ const loadTodos = () => {
378
+ try { return JSON.parse(fs.readFileSync(todoFile, 'utf-8')); }
379
+ catch { return []; }
380
+ };
381
+ const saveTodos = (todos) => fs.writeFileSync(todoFile, JSON.stringify(todos, null, 2), 'utf-8');
382
+ const formatTodos = (todos) => {
383
+ if (todos.length === 0) return 'Todo list is empty.';
384
+ return todos.map((t, i) =>
385
+ `${i + 1}. [${t.done ? 'x' : ' '}] ${t.text}${t.done ? ' ✓' : ''}`
386
+ ).join('\n');
387
+ };
388
+
389
+ const todos = loadTodos();
390
+ switch (args.action) {
391
+ case 'add': {
392
+ if (!args.text) return 'Error: "text" is required for add action.';
393
+ todos.push({ text: args.text, done: false, created: Date.now() });
394
+ saveTodos(todos);
395
+ return `Added item ${todos.length}: ${args.text}\n\n${formatTodos(todos)}`;
396
+ }
397
+ case 'list':
398
+ return formatTodos(todos);
399
+ case 'done': {
400
+ const idx = (args.index || 0) - 1;
401
+ if (idx < 0 || idx >= todos.length) return `Error: Invalid index. Use 1-${todos.length}.`;
402
+ todos[idx].done = true;
403
+ saveTodos(todos);
404
+ return `Completed: ${todos[idx].text}\n\n${formatTodos(todos)}`;
405
+ }
406
+ case 'remove': {
407
+ const idx = (args.index || 0) - 1;
408
+ if (idx < 0 || idx >= todos.length) return `Error: Invalid index. Use 1-${todos.length}.`;
409
+ const removed = todos.splice(idx, 1)[0];
410
+ saveTodos(todos);
411
+ return `Removed: ${removed.text}\n\n${formatTodos(todos)}`;
412
+ }
413
+ case 'clear': {
414
+ const before = todos.length;
415
+ const remaining = todos.filter(t => !t.done);
416
+ saveTodos(remaining);
417
+ return `Cleared ${before - remaining.length} completed item(s).\n\n${formatTodos(remaining)}`;
418
+ }
419
+ default:
420
+ return `Error: Unknown action "${args.action}". Use add, list, done, remove, or clear.`;
421
+ }
422
+ }
423
+
218
424
  case 'CodeReview': {
219
425
  // Auto-collect all modified files from this session
220
426
  const allFiles = new Set([...session.filesModified]);
@@ -258,33 +464,384 @@ async function executeTool(name, args) {
258
464
  ];
259
465
 
260
466
  try {
261
- let reviewResponse;
262
- for (let attempt = 0; attempt <= 2; attempt++) {
263
- try {
264
- reviewResponse = await nvidiaClient.chat.completions.create({
265
- model: REVIEWER_MODEL,
266
- messages: reviewMessages,
267
- max_tokens: 4096,
268
- temperature: 0.3,
269
- });
270
- break;
271
- } catch (retryErr) {
272
- if (attempt < 2 && retryErr.status >= 400 && retryErr.status < 500) {
273
- await sleep(1000 * Math.pow(2, attempt));
274
- continue;
275
- }
276
- throw retryErr;
277
- }
278
- }
279
- const reviewText = reviewResponse.choices[0]?.message?.content
280
- || reviewResponse.choices[0]?.message?.reasoning_content
281
- || '(No response from reviewer)';
282
- return truncateOutput(`Code Review (${REVIEWER_MODEL}) — ${allFiles.size} file(s)\n${'─'.repeat(40)}\n${reviewText}`);
467
+ const header = `Code Review (${REVIEWER_MODEL}) — ${allFiles.size} file(s)\n${'─'.repeat(40)}\n`;
468
+ const streamCb = onStream ? (text) => onStream(truncateOutput(header + text)) : null;
469
+ const reviewText = await streamCompletion({
470
+ model: REVIEWER_MODEL,
471
+ messages: reviewMessages,
472
+ max_tokens: 4096,
473
+ temperature: 0.3,
474
+ }, streamCb) || '(No response from reviewer)';
475
+ return truncateOutput(header + reviewText);
283
476
  } catch (apiErr) {
284
477
  return `Error: Code review failed — ${apiErr.message}`;
285
478
  }
286
479
  }
287
480
 
481
+ // ===== Thinker — Deep reasoning/planning =====
482
+ case 'Thinker': {
483
+ const historyContext = session.conversationHistory.slice(-10).map(m =>
484
+ `[${m.role}]: ${(m.content || '').slice(0, 500)}`
485
+ ).join('\n');
486
+
487
+ const thinkerMessages = [
488
+ { role: 'system', content: THINKER_SYSTEM_PROMPT },
489
+ {
490
+ role: 'user',
491
+ content: `# Recent conversation context\n${historyContext}\n\n# Task to reason about\n${args.prompt}`,
492
+ },
493
+ ];
494
+
495
+ try {
496
+ const header = `Thinker (${THINKER_MODEL})\n${'─'.repeat(40)}\n`;
497
+ const streamCb = onStream ? (text) => onStream(truncateOutput(header + text)) : null;
498
+ const result = await streamCompletion({
499
+ model: THINKER_MODEL,
500
+ messages: thinkerMessages,
501
+ max_tokens: 4096,
502
+ temperature: 0.4,
503
+ }, streamCb) || '(No response from thinker)';
504
+ return truncateOutput(header + result);
505
+ } catch (apiErr) {
506
+ return `Error: Thinker failed — ${apiErr.message}`;
507
+ }
508
+ }
509
+
510
+ // ===== ThinkerBestOfN — Multiple reasoning passes, select best =====
511
+ case 'ThinkerBestOfN': {
512
+ const mode = getMode();
513
+ if (mode !== 'max') {
514
+ return 'ThinkerBestOfN is only available in MAX mode. Use /mode max to enable it, or use Thinker instead.';
515
+ }
516
+
517
+ const n = Math.min(5, Math.max(2, args.n || 3));
518
+ const historyCtx = session.conversationHistory.slice(-10).map(m =>
519
+ `[${m.role}]: ${(m.content || '').slice(0, 500)}`
520
+ ).join('\n');
521
+
522
+ const header = `Best-of-${n} Thinker (MAX mode)\n${'─'.repeat(40)}\n`;
523
+ if (onStream) onStream(header + `Spawning ${n} parallel thinking agents...`);
524
+
525
+ // Spawn N parallel thinking calls
526
+ const thinkPromises = [];
527
+ for (let i = 0; i < n; i++) {
528
+ const label = String.fromCharCode(65 + i); // A, B, C, ...
529
+ thinkPromises.push(
530
+ streamCompletion({
531
+ model: THINKER_MODEL,
532
+ messages: [
533
+ { role: 'system', content: THINKER_SYSTEM_PROMPT + `\n\nYou are Thinker ${label}. Approach this from a unique angle. Be creative and thorough.` },
534
+ {
535
+ role: 'user',
536
+ content: `# Context\n${historyCtx}\n\n# Task\n${args.prompt}`,
537
+ },
538
+ ],
539
+ max_tokens: 3072,
540
+ temperature: 0.7 + (i * 0.1), // Slightly different temperatures for diversity
541
+ }, null).then(result => ({ label, result }))
542
+ );
543
+ }
544
+
545
+ let thoughts;
546
+ try {
547
+ thoughts = await Promise.all(thinkPromises);
548
+ } catch (apiErr) {
549
+ return `Error: ThinkerBestOfN failed — ${apiErr.message}`;
550
+ }
551
+
552
+ if (onStream) onStream(header + `All ${n} thinkers completed. Selecting best response...`);
553
+
554
+ // Format thoughts for selector
555
+ const thoughtsFormatted = thoughts.map(t =>
556
+ `## Thought ${t.label}\n${t.result || '(empty)'}`
557
+ ).join('\n\n');
558
+
559
+ // Selector picks the best
560
+ try {
561
+ const selectorResult = await streamCompletion({
562
+ model: REVIEWER_MODEL,
563
+ messages: [
564
+ {
565
+ role: 'system',
566
+ content: `You are a thought selector. You will receive ${n} different reasoning responses to the same question. Pick the best one based on depth, correctness, clarity, and actionability. Output JSON only:\n{ "chosen": "A", "reason": "why this is best" }`,
567
+ },
568
+ { role: 'user', content: `# Original question\n${args.prompt}\n\n${thoughtsFormatted}` },
569
+ ],
570
+ max_tokens: 1024,
571
+ temperature: 0.1,
572
+ }, null);
573
+
574
+ let chosen = 'A';
575
+ let reason = '';
576
+ try {
577
+ const parsed = JSON.parse(selectorResult.replace(/```json?\n?/g, '').replace(/```/g, '').trim());
578
+ chosen = parsed.chosen || 'A';
579
+ reason = parsed.reason || '';
580
+ } catch { /* default to A */ }
581
+
582
+ const winningThought = thoughts.find(t => t.label === chosen) || thoughts[0];
583
+ const result = `${header}Selected: Thought ${chosen}${reason ? ` — ${reason}` : ''}\n\n${winningThought.result}`;
584
+ if (onStream) onStream(truncateOutput(result));
585
+ return truncateOutput(result);
586
+ } catch (apiErr) {
587
+ // Fallback to first thought
588
+ const result = `${header}Selector failed, using Thought A:\n\n${thoughts[0].result}`;
589
+ return truncateOutput(result);
590
+ }
591
+ }
592
+
593
+ // ===== EditorMultiPrompt — Parallel implementation strategies, select best =====
594
+ case 'EditorMultiPrompt': {
595
+ const mode = getMode();
596
+ if (mode !== 'max') {
597
+ return 'EditorMultiPrompt is only available in MAX mode. Use /mode max to enable it.';
598
+ }
599
+
600
+ const strategies = args.strategies || ['straightforward implementation', 'alternative approach'];
601
+ const filesCtx = (args.files || []).map(f =>
602
+ `--- ${f.path} ---\n${f.content}`
603
+ ).join('\n\n');
604
+
605
+ const header = `Multi-Prompt Editor (${strategies.length} strategies)\n${'─'.repeat(40)}\n`;
606
+ if (onStream) onStream(header + `Spawning ${strategies.length} parallel editor agents...`);
607
+
608
+ // Spawn parallel editor agents, one per strategy
609
+ const editorPromises = strategies.map((strategy, i) => {
610
+ const label = String.fromCharCode(65 + i);
611
+ return streamCompletion({
612
+ model: NVIDIA_MODEL,
613
+ messages: [
614
+ {
615
+ role: 'system',
616
+ content: `You are Code Editor ${label}. You implement code changes using a specific strategy. Output your implementation as a series of file edits.\n\nFor each file change, output:\n--- EDIT: path/to/file ---\nOLD:\n\`\`\`\nexact old code\n\`\`\`\nNEW:\n\`\`\`\nnew replacement code\n\`\`\`\n\nFor new files, output:\n--- CREATE: path/to/file ---\n\`\`\`\nfull file content\n\`\`\`\n\nBe precise. Match existing code style.`,
617
+ },
618
+ {
619
+ role: 'user',
620
+ content: `# Task\n${args.prompt}\n\n# Strategy\n${strategy}\n\n# Current files\n${filesCtx}`,
621
+ },
622
+ ],
623
+ max_tokens: 4096,
624
+ temperature: 0.3,
625
+ }, null).then(result => ({ label, strategy, result: result || '(empty)' }));
626
+ });
627
+
628
+ let implementations;
629
+ try {
630
+ implementations = await Promise.all(editorPromises);
631
+ } catch (apiErr) {
632
+ return `Error: EditorMultiPrompt failed — ${apiErr.message}`;
633
+ }
634
+
635
+ if (onStream) onStream(header + `All editors completed. Selecting best implementation...`);
636
+
637
+ // Format implementations for selector
638
+ const implFormatted = implementations.map(impl =>
639
+ `## Implementation ${impl.label} — Strategy: "${impl.strategy}"\n${impl.result}`
640
+ ).join('\n\n');
641
+
642
+ // Selector picks the best
643
+ try {
644
+ const selectorResult = await streamCompletion({
645
+ model: REVIEWER_MODEL,
646
+ messages: [
647
+ { role: 'system', content: SELECTOR_SYSTEM_PROMPT },
648
+ {
649
+ role: 'user',
650
+ content: `# Original task\n${args.prompt}\n\n${implFormatted}`,
651
+ },
652
+ ],
653
+ max_tokens: 1024,
654
+ temperature: 0.1,
655
+ }, null);
656
+
657
+ let chosen = 'A';
658
+ let reason = '';
659
+ let improvements = '';
660
+ try {
661
+ const parsed = JSON.parse(selectorResult.replace(/```json?\n?/g, '').replace(/```/g, '').trim());
662
+ chosen = parsed.chosen || 'A';
663
+ reason = parsed.reason || '';
664
+ improvements = parsed.improvements || '';
665
+ } catch { /* default to A */ }
666
+
667
+ const winning = implementations.find(impl => impl.label === chosen) || implementations[0];
668
+ let result = `${header}Selected: Implementation ${chosen} ("${winning.strategy}")`;
669
+ if (reason) result += `\nReason: ${reason}`;
670
+ if (improvements) result += `\nImprovements to consider: ${improvements}`;
671
+ result += `\n\n${winning.result}`;
672
+ if (onStream) onStream(truncateOutput(result));
673
+ return truncateOutput(result);
674
+ } catch (apiErr) {
675
+ const result = `${header}Selector failed, using Implementation A:\n\n${implementations[0].result}`;
676
+ return truncateOutput(result);
677
+ }
678
+ }
679
+
680
+ // ===== CodeReviewMulti — Multiple review perspectives in parallel =====
681
+ case 'CodeReviewMulti': {
682
+ const mode = getMode();
683
+ if (mode !== 'max') {
684
+ return 'CodeReviewMulti is only available in MAX mode. Use /mode max to enable it, or use CodeReview for a single review.';
685
+ }
686
+
687
+ const perspectives = args.perspectives || [
688
+ 'correctness, logic errors, and edge cases',
689
+ 'security vulnerabilities and data safety',
690
+ 'performance, efficiency, and resource usage',
691
+ ];
692
+
693
+ // Gather modified files
694
+ const modFiles = new Set([...session.filesModified]);
695
+ if (modFiles.size === 0) return 'CodeReviewMulti skipped — no files were modified.';
696
+
697
+ const modFileContents = [];
698
+ for (const fp of modFiles) {
699
+ if (!fs.existsSync(fp)) continue;
700
+ const stat = fs.statSync(fp);
701
+ if (stat.isDirectory()) continue;
702
+ modFileContents.push(`--- ${path.relative(PROJECT_ROOT, fp)} ---\n${fs.readFileSync(fp, 'utf-8')}`);
703
+ }
704
+
705
+ let diffText = '';
706
+ try {
707
+ diffText = execSync('git diff 2>/dev/null', { encoding: 'utf-8', cwd: PROJECT_ROOT, timeout: 10000 }).trim();
708
+ } catch {}
709
+
710
+ const header = `Multi-Perspective Code Review (${perspectives.length} reviewers)\n${'─'.repeat(40)}\n`;
711
+ if (onStream) onStream(header + `Spawning ${perspectives.length} parallel reviewers...`);
712
+
713
+ const reviewPromises = perspectives.map((perspective, i) => {
714
+ const label = String.fromCharCode(65 + i);
715
+ return streamCompletion({
716
+ model: REVIEWER_MODEL,
717
+ messages: [
718
+ {
719
+ role: 'system',
720
+ content: REVIEWER_SYSTEM_PROMPT + `\n\nFocus specifically on: ${perspective}. You are Reviewer ${label}.`,
721
+ },
722
+ {
723
+ role: 'user',
724
+ content: `# Changes\n${args.prompt}\n\n# Files (${modFiles.size})\n${modFileContents.join('\n\n')}${diffText ? `\n\n# Git diff\n\`\`\`diff\n${diffText}\n\`\`\`` : ''}`,
725
+ },
726
+ ],
727
+ max_tokens: 3072,
728
+ temperature: 0.3,
729
+ }, null).then(result => ({ label, perspective, result: result || '(no issues found)' }));
730
+ });
731
+
732
+ let reviews;
733
+ try {
734
+ reviews = await Promise.all(reviewPromises);
735
+ } catch (apiErr) {
736
+ return `Error: CodeReviewMulti failed — ${apiErr.message}`;
737
+ }
738
+
739
+ let result = header;
740
+ for (const review of reviews) {
741
+ result += `\n## Reviewer ${review.label} — ${review.perspective}\n${review.result}\n`;
742
+ }
743
+ if (onStream) onStream(truncateOutput(result));
744
+ return truncateOutput(result);
745
+ }
746
+
747
+ // ===== Commander — Terminal command specialist =====
748
+ case 'Commander': {
749
+ const header = `Commander (${COMMANDER_MODEL})\n${'─'.repeat(40)}\n`;
750
+ if (onStream) onStream(header + 'Planning commands...');
751
+
752
+ let commandPlan;
753
+ try {
754
+ commandPlan = await streamCompletion({
755
+ model: COMMANDER_MODEL,
756
+ messages: [
757
+ { role: 'system', content: COMMANDER_SYSTEM_PROMPT },
758
+ { role: 'user', content: args.prompt },
759
+ ],
760
+ max_tokens: 2048,
761
+ temperature: 0.2,
762
+ }, null);
763
+ } catch (apiErr) {
764
+ return `Error: Commander failed — ${apiErr.message}`;
765
+ }
766
+
767
+ // Parse command plan
768
+ let commands;
769
+ try {
770
+ commands = JSON.parse(commandPlan.replace(/```json?\n?/g, '').replace(/```/g, '').trim());
771
+ if (!Array.isArray(commands)) commands = [commands];
772
+ } catch {
773
+ return truncateOutput(`${header}Failed to parse command plan:\n${commandPlan}`);
774
+ }
775
+
776
+ // Execute commands in sequence
777
+ const results = [];
778
+ for (const cmd of commands) {
779
+ const command = typeof cmd === 'string' ? cmd : cmd.command;
780
+ const description = typeof cmd === 'string' ? '' : (cmd.description || '');
781
+ if (onStream) onStream(truncateOutput(`${header}Running: ${command}${description ? ` (${description})` : ''}...`));
782
+ try {
783
+ const output = execSync(command, {
784
+ encoding: 'utf-8',
785
+ timeout: TOOL_TIMEOUT,
786
+ cwd: PROJECT_ROOT,
787
+ maxBuffer: 1024 * 1024 * 5,
788
+ stdio: ['pipe', 'pipe', 'pipe'],
789
+ });
790
+ results.push(`✓ ${command}${description ? `\n (${description})` : ''}\n${(output || '').trim()}`);
791
+ session.commandsRun.push(command);
792
+ } catch (err) {
793
+ results.push(`✗ ${command}\nExit code: ${err.status}\n${(err.stdout || '').trim()}\n${(err.stderr || '').trim()}`);
794
+ session.commandsRun.push(command);
795
+ break;
796
+ }
797
+ }
798
+
799
+ const result = `${header}${results.join('\n\n')}`;
800
+ if (onStream) onStream(truncateOutput(result));
801
+ return truncateOutput(result);
802
+ }
803
+
804
+ // ===== ContextPruner — Conversation summarization =====
805
+ case 'ContextPruner': {
806
+ if (session.conversationHistory.length < 6) {
807
+ return 'Context pruning skipped — conversation is still short.';
808
+ }
809
+
810
+ const header = `Context Pruner\n${'─'.repeat(40)}\n`;
811
+ if (onStream) onStream(header + 'Summarizing conversation...');
812
+
813
+ const historyText = session.conversationHistory.map(m =>
814
+ `[${m.role}]: ${(m.content || '').slice(0, 1000)}`
815
+ ).join('\n');
816
+
817
+ try {
818
+ const summary = await streamCompletion({
819
+ model: CONTEXT_PRUNER_MODEL,
820
+ messages: [
821
+ { role: 'system', content: CONTEXT_PRUNER_SYSTEM_PROMPT },
822
+ { role: 'user', content: `# Conversation to summarize (${session.conversationHistory.length} messages)\n\n${historyText}` },
823
+ ],
824
+ max_tokens: 2048,
825
+ temperature: 0.2,
826
+ }, null);
827
+
828
+ // Replace conversation history with summary
829
+ const oldLen = session.conversationHistory.length;
830
+ session.conversationHistory = [
831
+ {
832
+ role: 'system',
833
+ content: `[Context Summary — ${oldLen} messages condensed]\n${summary}`,
834
+ },
835
+ ];
836
+
837
+ const result = `${header}Condensed ${oldLen} messages into summary.\n\n${summary}`;
838
+ if (onStream) onStream(truncateOutput(result));
839
+ return truncateOutput(result);
840
+ } catch (apiErr) {
841
+ return `Error: Context pruning failed — ${apiErr.message}`;
842
+ }
843
+ }
844
+
288
845
  default:
289
846
  return `Unknown tool: ${name}`;
290
847
  }