apex-dev 2.1.3 → 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.
- package/.local/share/amp/history.jsonl +30 -0
- package/.local/share/amp/session.json +2 -2
- package/.local/share/amp/threads/T-019c9769-4a8a-77b8-beab-f48973276f9a.json +1541 -0
- package/.local/share/amp/threads/T-019c9772-edac-7075-b26e-0ada1f8697d2.json +7 -0
- package/.local/share/amp/threads/T-019c97e8-a9ab-71a1-a8f9-109c540c98bf.json +111 -0
- package/.local/share/amp/threads/T-019c97e9-2277-753c-8c5d-df745fa6cfff.json +7 -0
- package/.local/share/amp/threads/T-019c97e9-f28e-758d-9663-e37047a8ed95.json +111 -0
- package/.local/share/amp/threads/T-019c97ea-17c7-77b8-92b2-f641c069bcc9.json +71 -0
- package/.local/share/amp/threads/T-019c97ea-44c6-75b8-88bc-d88113194f6a.json +1611 -0
- package/.local/share/amp/threads/T-019c97ec-abae-7251-a5f6-693adf496a1c.json +7 -0
- package/.local/share/amp/threads/T-019c97f5-8e61-73ad-8c5d-2637abedcde6.json +1341 -0
- package/.local/share/amp/threads/T-019c989d-4f4e-7249-bde0-21d19455ccae.json +163 -0
- package/.local/share/amp/threads/T-019c989d-9024-73c4-bee8-e2ae45028a39.json +124 -0
- package/.local/share/amp/threads/T-019c989e-1394-74ad-8234-ac573fcdb4c7.json +1260 -0
- package/.local/share/amp/threads/T-019c989f-e3dd-772e-85ac-525d0fc88fda.json +403 -0
- package/.local/share/amp/threads/T-019c98a1-7b0c-778a-b311-2e1cff85d710.json +3422 -0
- package/.local/share/amp/threads/T-019c98c5-4b7f-7284-99e9-88aa8c18ba66.json +1830 -0
- package/.local/share/amp/threads/T-019c98d0-f27f-76ec-be10-6df96f22be99.json +4061 -0
- package/.local/share/amp/threads/T-019c98f9-d031-704d-a0c2-f2f395f68f2b.json +509 -0
- package/.local/share/amp/threads/T-019c9919-f9ee-766c-90be-af7a07f6a4c6.json +2075 -0
- package/.local/share/amp/threads/T-019c991c-b98b-7158-9083-cc52408beb13.json +7 -0
- package/.local/share/amp/threads/T-019c991d-66d6-72aa-a9a1-105f7df0ea06.json +7 -0
- package/.local/share/amp/threads/T-019c9c2e-71a4-77ff-bd7f-b053da7f9000.json +1637 -0
- package/.local/share/amp/threads/T-019c9c45-27ca-728b-ba77-835115dfa9b2.json +3893 -0
- package/.local/share/amp/threads/T-019c9c48-45dc-736a-9752-e4119fe698f9.json +7 -0
- package/.local/share/amp/threads/T-019c9c4d-266b-72d0-b56e-74a5777e6583.json +7 -0
- package/.local/share/amp/threads/T-019c9c52-ab89-758f-9178-bda99c39d10b.json +7 -0
- package/.local/share/opencode/opencode.db +0 -0
- package/.local/share/opencode/opencode.db-shm +0 -0
- package/.local/share/opencode/opencode.db-wal +0 -0
- package/.local/share/opencode/storage/agent-usage-reminder/ses_36870ea98ffe8S5ZOCE4F11yFh.json +6 -0
- package/.local/share/opencode/storage/agent-usage-reminder/ses_3687a3e9affewUnHBzvpiPR6df.json +6 -0
- package/.local/share/opencode/storage/agent-usage-reminder/ses_36886e68dffeKVgUWf6lzXdEEt.json +6 -0
- package/.local/share/opencode/storage/session_diff/ses_36870ea98ffe8S5ZOCE4F11yFh.json +1 -0
- package/.local/share/opencode/storage/session_diff/ses_3687a3e9affewUnHBzvpiPR6df.json +1 -0
- package/.local/share/opencode/storage/session_diff/ses_36886e68dffeKVgUWf6lzXdEEt.json +1 -0
- package/.local/state/replit/log-query.db +0 -0
- package/.local/state/replit/log-query.db-shm +0 -0
- package/.local/state/replit/log-query.db-wal +0 -0
- package/.upm/store.json +1 -1
- package/AGENTS.md +32 -0
- package/bun.lock +137 -103
- package/index.jsx +24 -0
- package/package.json +9 -9
- package/src/agent.js +252 -169
- package/src/app.jsx +96 -0
- package/src/commands.js +66 -38
- package/src/components/AssistantMessage.jsx +83 -0
- package/src/components/ChatArea.jsx +84 -0
- package/src/components/DiffView.jsx +26 -0
- package/src/components/Divider.jsx +8 -0
- package/src/components/Header.jsx +44 -0
- package/src/components/HelpModal.jsx +81 -0
- package/src/components/InputBar.jsx +32 -0
- package/src/components/Spinner.jsx +23 -0
- package/src/components/StatusBar.jsx +44 -0
- package/src/components/SystemMessage.jsx +31 -0
- package/src/components/ThinkBlock.jsx +29 -0
- package/src/components/ToolCallItem.jsx +43 -0
- package/src/components/UserMessage.jsx +11 -0
- package/src/components/Welcome.jsx +14 -0
- package/src/config.js +118 -2
- package/src/hooks/useLayout.js +15 -0
- package/src/hooks/useStore.js +6 -0
- package/src/prompt.js +67 -48
- package/src/store.js +99 -0
- package/src/theme.js +19 -0
- package/src/thinking.js +0 -24
- package/src/toolExecutors.js +521 -23
- package/src/tools.js +124 -4
- package/src/utils.js +32 -0
- package/tsconfig.json +10 -0
- package/index.js +0 -92
- 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
|
};
|
package/src/toolExecutors.js
CHANGED
|
@@ -9,14 +9,60 @@ const {
|
|
|
9
9
|
TOOL_TIMEOUT,
|
|
10
10
|
REVIEWER_MODEL,
|
|
11
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,
|
|
12
22
|
nvidiaClient,
|
|
13
23
|
session,
|
|
14
24
|
truncateOutput,
|
|
15
25
|
resolvePath,
|
|
16
26
|
sleep,
|
|
27
|
+
getMode,
|
|
17
28
|
} = require('./config');
|
|
18
29
|
|
|
19
|
-
async function
|
|
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) {
|
|
20
66
|
try {
|
|
21
67
|
switch (name) {
|
|
22
68
|
case 'Read': {
|
|
@@ -274,6 +320,107 @@ async function executeTool(name, args) {
|
|
|
274
320
|
return result;
|
|
275
321
|
}
|
|
276
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
|
+
|
|
277
424
|
case 'CodeReview': {
|
|
278
425
|
// Auto-collect all modified files from this session
|
|
279
426
|
const allFiles = new Set([...session.filesModified]);
|
|
@@ -317,33 +464,384 @@ async function executeTool(name, args) {
|
|
|
317
464
|
];
|
|
318
465
|
|
|
319
466
|
try {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
break;
|
|
330
|
-
} catch (retryErr) {
|
|
331
|
-
if (attempt < 2 && retryErr.status >= 400 && retryErr.status < 500) {
|
|
332
|
-
await sleep(1000 * Math.pow(2, attempt));
|
|
333
|
-
continue;
|
|
334
|
-
}
|
|
335
|
-
throw retryErr;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
const reviewText = reviewResponse.choices[0]?.message?.content
|
|
339
|
-
|| reviewResponse.choices[0]?.message?.reasoning_content
|
|
340
|
-
|| '(No response from reviewer)';
|
|
341
|
-
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);
|
|
342
476
|
} catch (apiErr) {
|
|
343
477
|
return `Error: Code review failed — ${apiErr.message}`;
|
|
344
478
|
}
|
|
345
479
|
}
|
|
346
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
|
+
|
|
347
845
|
default:
|
|
348
846
|
return `Unknown tool: ${name}`;
|
|
349
847
|
}
|