apex-dev 3.0.2 → 3.0.3

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 (153) hide show
  1. package/dist/highlights-eq9cgrbb.scm +604 -0
  2. package/dist/highlights-ghv9g403.scm +205 -0
  3. package/dist/highlights-hk7bwhj4.scm +284 -0
  4. package/dist/highlights-r812a2qc.scm +150 -0
  5. package/dist/highlights-x6tmsnaa.scm +115 -0
  6. package/dist/index.js +62488 -0
  7. package/dist/injections-73j83es3.scm +27 -0
  8. package/dist/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
  9. package/dist/tree-sitter-markdown-411r6y9b.wasm +0 -0
  10. package/dist/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
  11. package/dist/tree-sitter-typescript-zxjzwt75.wasm +0 -0
  12. package/dist/tree-sitter-zig-e78zbjpm.wasm +0 -0
  13. package/package.json +10 -4
  14. package/.config/amp/settings.json +0 -3
  15. package/.config/opencode/oh-my-opencode.json +0 -58
  16. package/.config/opencode/opencode.json +0 -6
  17. package/.local/share/amp/device-id.json +0 -3
  18. package/.local/share/amp/history.jsonl +0 -78
  19. package/.local/share/amp/session.json +0 -6
  20. package/.local/share/amp/threads/T-019c93b8-fce7-7083-aab9-d5f1c88a9545.json +0 -2528
  21. package/.local/share/amp/threads/T-019c93c8-4b7a-71df-94ac-867d8236a288.json +0 -7
  22. package/.local/share/amp/threads/T-019c93cd-5a7d-728e-8289-02e0ef4ca2ff.json +0 -680
  23. package/.local/share/amp/threads/T-019c93e7-83ca-7633-9eed-12bdcc118163.json +0 -873
  24. package/.local/share/amp/threads/T-019c93ea-ccd3-765a-88c9-42d7b631e977.json +0 -620
  25. package/.local/share/amp/threads/T-019c93ee-5977-71af-9ab7-c4611004b703.json +0 -1000
  26. package/.local/share/amp/threads/T-019c93f0-8328-71ed-a250-6da169cebfe1.json +0 -829
  27. package/.local/share/amp/threads/T-019c93f5-7bdd-703b-b2cd-0a04da64441a.json +0 -459
  28. package/.local/share/amp/threads/T-019c93f8-2b2e-733b-8249-9876546d9b5b.json +0 -764
  29. package/.local/share/amp/threads/T-019c93fd-fade-7195-a3b7-358f180d40b8.json +0 -7
  30. package/.local/share/amp/threads/T-019c93fe-2e56-705e-827e-eb99bd02e257.json +0 -3593
  31. package/.local/share/amp/threads/T-019c9408-6e64-77e1-9519-b913e3b24a03.json +0 -1559
  32. package/.local/share/amp/threads/T-019c9409-feeb-736d-b92c-4f7a263a643c.json +0 -7
  33. package/.local/share/amp/threads/T-019c940b-8d11-755b-b9e1-f923d8a5e6ba.json +0 -7
  34. package/.local/share/amp/threads/T-019c943a-6c5e-76a5-bf4e-170f7ad452ce.json +0 -979
  35. package/.local/share/amp/threads/T-019c94b2-1c8f-76d8-96d0-82449a028849.json +0 -1584
  36. package/.local/share/amp/threads/T-019c94b6-68f0-726e-92dd-90c5411ca28c.json +0 -7
  37. package/.local/share/amp/threads/T-019c94bf-a589-72a3-b3c2-a81359d9e0a6.json +0 -7
  38. package/.local/share/amp/threads/T-019c94e1-1bd9-70ab-b6f2-abd5cab4f4ce.json +0 -1035
  39. package/.local/share/amp/threads/T-019c94fd-cc4a-714b-896a-74f94020f6eb.json +0 -1310
  40. package/.local/share/amp/threads/T-019c9501-8976-7138-aca6-245a01a8fe9b.json +0 -7
  41. package/.local/share/amp/threads/T-019c9504-4b51-763e-8a9f-5d4cdfcf0cfa.json +0 -496
  42. package/.local/share/amp/threads/T-019c9506-4e3b-74fd-8eda-cedbf3793598.json +0 -2679
  43. package/.local/share/amp/threads/T-019c9508-178c-718c-88d2-caf816d64f65.json +0 -965
  44. package/.local/share/amp/threads/T-019c9509-2812-71fd-8fd2-923e29ad34fa.json +0 -7
  45. package/.local/share/amp/threads/T-019c950e-69fe-77d6-9854-fc73b77a3148.json +0 -4570
  46. package/.local/share/amp/threads/T-019c9707-6e2b-741c-b4d4-117026a78449.json +0 -2899
  47. package/.local/share/amp/threads/T-019c971b-6bc0-77b8-8868-f8956d3e71a8.json +0 -7
  48. package/.local/share/amp/threads/T-019c971b-c87c-75f3-a61f-beb18a1cb25f.json +0 -474
  49. package/.local/share/amp/threads/T-019c971d-d371-70ac-9805-5c739908e73b.json +0 -802
  50. package/.local/share/amp/threads/T-019c9722-d73d-74f1-9d1d-8fafaad0ede7.json +0 -7
  51. package/.local/share/amp/threads/T-019c9761-858c-719b-911f-bc2e4c8cbdde.json +0 -188
  52. package/.local/share/amp/threads/T-019c9761-f5f3-7606-a900-ebe7f10d6e37.json +0 -121
  53. package/.local/share/amp/threads/T-019c9763-b1ae-729d-90aa-f59938ce912e.json +0 -799
  54. package/.local/share/amp/threads/T-019c9769-4a8a-77b8-beab-f48973276f9a.json +0 -1541
  55. package/.local/share/amp/threads/T-019c9772-edac-7075-b26e-0ada1f8697d2.json +0 -7
  56. package/.local/share/amp/threads/T-019c97e8-a9ab-71a1-a8f9-109c540c98bf.json +0 -111
  57. package/.local/share/amp/threads/T-019c97e9-2277-753c-8c5d-df745fa6cfff.json +0 -7
  58. package/.local/share/amp/threads/T-019c97e9-f28e-758d-9663-e37047a8ed95.json +0 -111
  59. package/.local/share/amp/threads/T-019c97ea-17c7-77b8-92b2-f641c069bcc9.json +0 -71
  60. package/.local/share/amp/threads/T-019c97ea-44c6-75b8-88bc-d88113194f6a.json +0 -1611
  61. package/.local/share/amp/threads/T-019c97ec-abae-7251-a5f6-693adf496a1c.json +0 -7
  62. package/.local/share/amp/threads/T-019c97f5-8e61-73ad-8c5d-2637abedcde6.json +0 -1341
  63. package/.local/share/amp/threads/T-019c989d-4f4e-7249-bde0-21d19455ccae.json +0 -163
  64. package/.local/share/amp/threads/T-019c989d-9024-73c4-bee8-e2ae45028a39.json +0 -124
  65. package/.local/share/amp/threads/T-019c989e-1394-74ad-8234-ac573fcdb4c7.json +0 -1260
  66. package/.local/share/amp/threads/T-019c989f-e3dd-772e-85ac-525d0fc88fda.json +0 -403
  67. package/.local/share/amp/threads/T-019c98a1-7b0c-778a-b311-2e1cff85d710.json +0 -3422
  68. package/.local/share/amp/threads/T-019c98c5-4b7f-7284-99e9-88aa8c18ba66.json +0 -1830
  69. package/.local/share/amp/threads/T-019c98d0-f27f-76ec-be10-6df96f22be99.json +0 -4061
  70. package/.local/share/amp/threads/T-019c98f9-d031-704d-a0c2-f2f395f68f2b.json +0 -509
  71. package/.local/share/amp/threads/T-019c9919-f9ee-766c-90be-af7a07f6a4c6.json +0 -2075
  72. package/.local/share/amp/threads/T-019c991c-b98b-7158-9083-cc52408beb13.json +0 -7
  73. package/.local/share/amp/threads/T-019c991d-66d6-72aa-a9a1-105f7df0ea06.json +0 -7
  74. package/.local/share/amp/threads/T-019c9c2e-71a4-77ff-bd7f-b053da7f9000.json +0 -1637
  75. package/.local/share/amp/threads/T-019c9c45-27ca-728b-ba77-835115dfa9b2.json +0 -3893
  76. package/.local/share/amp/threads/T-019c9c48-45dc-736a-9752-e4119fe698f9.json +0 -7
  77. package/.local/share/amp/threads/T-019c9c4d-266b-72d0-b56e-74a5777e6583.json +0 -7
  78. package/.local/share/amp/threads/T-019c9c52-ab89-758f-9178-bda99c39d10b.json +0 -7
  79. package/.local/share/amp/threads/T-019c9c56-5715-72e2-b8b4-87711a842dd1.json +0 -1799
  80. package/.local/share/amp/threads/T-019c9c5b-88b1-74cb-97e9-16b23e03daa2.json +0 -727
  81. package/.local/share/amp/threads/T-019c9c5c-3b3e-721c-ad2e-a2ef245dce3f.json +0 -738
  82. package/.local/share/amp/threads/T-019c9c5c-fd78-736f-9d29-a66d23839d40.json +0 -256
  83. package/.local/share/amp/threads/T-019c9c5d-db4a-74cd-ad2a-925fac87131d.json +0 -1859
  84. package/.local/share/kilo/kilo.db +0 -0
  85. package/.local/share/kilo/kilo.db-shm +0 -0
  86. package/.local/share/kilo/kilo.db-wal +0 -0
  87. package/.local/share/kilo/storage/migration +0 -1
  88. package/.local/share/kilo/storage/session_diff/ses_36bea4cb9ffe1b0j5HEL14KEaU.json +0 -1
  89. package/.local/share/kilo/storage/session_diff/ses_36beaa8f2ffeeZ3Y39SQ9UDWQQ.json +0 -1
  90. package/.local/share/kilo/telemetry-id +0 -1
  91. package/.local/share/opencode/auth.json +0 -6
  92. package/.local/share/opencode/opencode.db +0 -0
  93. package/.local/share/opencode/opencode.db-shm +0 -0
  94. package/.local/share/opencode/opencode.db-wal +0 -0
  95. package/.local/share/opencode/storage/agent-usage-reminder/ses_36870ea98ffe8S5ZOCE4F11yFh.json +0 -6
  96. package/.local/share/opencode/storage/agent-usage-reminder/ses_3687a3e9affewUnHBzvpiPR6df.json +0 -6
  97. package/.local/share/opencode/storage/agent-usage-reminder/ses_36886e68dffeKVgUWf6lzXdEEt.json +0 -6
  98. package/.local/share/opencode/storage/agent-usage-reminder/ses_36bee9f1effeJbiHHLWLR6O3WJ.json +0 -6
  99. package/.local/share/opencode/storage/agent-usage-reminder/ses_36c25e50affef2nhaXq9aSgKH3.json +0 -6
  100. package/.local/share/opencode/storage/agent-usage-reminder/ses_36c260708ffel4wG4yhdo0knDD.json +0 -6
  101. package/.local/share/opencode/storage/agent-usage-reminder/ses_36c261531ffeoVcvqXxry2bN9H.json +0 -6
  102. package/.local/share/opencode/storage/agent-usage-reminder/ses_36c291bddffePWRiaFLLJAC1y7.json +0 -6
  103. package/.local/share/opencode/storage/migration +0 -1
  104. package/.local/share/opencode/storage/session_diff/ses_36870ea98ffe8S5ZOCE4F11yFh.json +0 -1
  105. package/.local/share/opencode/storage/session_diff/ses_3687a3e9affewUnHBzvpiPR6df.json +0 -1
  106. package/.local/share/opencode/storage/session_diff/ses_36886e68dffeKVgUWf6lzXdEEt.json +0 -1
  107. package/.local/share/opencode/storage/session_diff/ses_36bee9f1effeJbiHHLWLR6O3WJ.json +0 -1
  108. package/.local/share/opencode/storage/session_diff/ses_36c25e50affef2nhaXq9aSgKH3.json +0 -1
  109. package/.local/share/opencode/storage/session_diff/ses_36c260708ffel4wG4yhdo0knDD.json +0 -1
  110. package/.local/share/opencode/storage/session_diff/ses_36c261531ffeoVcvqXxry2bN9H.json +0 -1
  111. package/.local/share/opencode/storage/session_diff/ses_36c291bddffePWRiaFLLJAC1y7.json +0 -1
  112. package/.local/share/opencode/storage/session_diff/ses_36c2af1c5ffegxEaOZOGcVykyy.json +0 -1
  113. package/.local/share/opencode/storage/session_diff/ses_36c2be235ffeOa6x8UCk1HW4kU.json +0 -1
  114. package/.local/share/opencode/tool-output/tool_c93da840c0016GrdyAkOnHGezU +0 -2330
  115. package/.local/share/opencode/tool-output/tool_c9411e784001cRoQqwVDb1a6lY +0 -1017
  116. package/.local/state/replit/log-query.db +0 -0
  117. package/.local/state/replit/log-query.db-shm +0 -0
  118. package/.local/state/replit/log-query.db-wal +0 -0
  119. package/.replit +0 -41
  120. package/.upm/store.json +0 -1
  121. package/AGENTS.md +0 -28
  122. package/bun.lock +0 -271
  123. package/generated-icon.png +0 -0
  124. package/hello.txt +0 -1
  125. package/index.jsx +0 -24
  126. package/src/agent.js +0 -504
  127. package/src/app.jsx +0 -96
  128. package/src/commands.js +0 -133
  129. package/src/components/AssistantMessage.jsx +0 -83
  130. package/src/components/ChatArea.jsx +0 -84
  131. package/src/components/DiffView.jsx +0 -26
  132. package/src/components/Divider.jsx +0 -8
  133. package/src/components/Header.jsx +0 -44
  134. package/src/components/HelpModal.jsx +0 -81
  135. package/src/components/InputBar.jsx +0 -32
  136. package/src/components/Spinner.jsx +0 -23
  137. package/src/components/StatusBar.jsx +0 -44
  138. package/src/components/SystemMessage.jsx +0 -31
  139. package/src/components/ThinkBlock.jsx +0 -29
  140. package/src/components/ToolCallItem.jsx +0 -43
  141. package/src/components/UserMessage.jsx +0 -11
  142. package/src/components/Welcome.jsx +0 -14
  143. package/src/config.js +0 -196
  144. package/src/hooks/useLayout.js +0 -15
  145. package/src/hooks/useStore.js +0 -6
  146. package/src/prompt.js +0 -101
  147. package/src/store.js +0 -99
  148. package/src/theme.js +0 -19
  149. package/src/thinking.js +0 -54
  150. package/src/toolExecutors.js +0 -853
  151. package/src/tools.js +0 -335
  152. package/src/utils.js +0 -32
  153. package/tsconfig.json +0 -10
@@ -1,853 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const https = require('https');
6
- const { execSync } = require('child_process');
7
- const {
8
- PROJECT_ROOT,
9
- TOOL_TIMEOUT,
10
- REVIEWER_MODEL,
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,
22
- nvidiaClient,
23
- session,
24
- truncateOutput,
25
- resolvePath,
26
- sleep,
27
- getMode,
28
- } = require('./config');
29
-
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) {
66
- try {
67
- switch (name) {
68
- case 'Read': {
69
- const filePath = resolvePath(args.path);
70
- const stat = fs.statSync(filePath, { throwIfNoEntry: false });
71
- if (!stat) return `Error: File not found: ${filePath}`;
72
- if (stat.isDirectory()) return `Error: ${filePath} is a directory. Use ListDir instead.`;
73
- const content = fs.readFileSync(filePath, 'utf-8');
74
- const lines = content.split('\n');
75
- const start = Math.max(0, (args.start_line || 1) - 1);
76
- const end = args.end_line ? Math.min(lines.length, args.end_line) : Math.min(lines.length, start + 500);
77
- const slice = lines.slice(start, end);
78
- const numbered = slice.map((l, i) => `${start + i + 1}: ${l}`).join('\n');
79
- session.filesRead.add(filePath);
80
- if (end < lines.length) {
81
- return truncateOutput(numbered) + `\n(showing lines ${start + 1}-${end} of ${lines.length})`;
82
- }
83
- return truncateOutput(numbered);
84
- }
85
-
86
- case 'Write': {
87
- const filePath = resolvePath(args.path);
88
- const dir = path.dirname(filePath);
89
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
90
- const existed = fs.existsSync(filePath);
91
- const before = existed ? fs.readFileSync(filePath, 'utf-8') : null;
92
- fs.writeFileSync(filePath, args.content, 'utf-8');
93
- if (before !== null) {
94
- session.editHistory.push({ path: filePath, before, after: args.content, timestamp: Date.now() });
95
- }
96
- session.filesModified.add(filePath);
97
- const lines = args.content.split('\n').length;
98
- return `${existed ? 'Overwritten' : 'Created'}: ${filePath} (${lines} lines)`;
99
- }
100
-
101
- case 'Edit': {
102
- const filePath = resolvePath(args.path);
103
- if (!fs.existsSync(filePath)) return `Error: File not found: ${filePath}`;
104
- const content = fs.readFileSync(filePath, 'utf-8');
105
- const count = content.split(args.old_str).length - 1;
106
- if (count === 0) return `Error: old_str not found in ${path.basename(filePath)}. Make sure it matches exactly (including whitespace and indentation).`;
107
- if (count > 1) return `Error: old_str found ${count} times in ${path.basename(filePath)}. It must be unique. Add more surrounding context to make it unique.`;
108
- const updated = content.replace(args.old_str, args.new_str);
109
- fs.writeFileSync(filePath, updated, 'utf-8');
110
- session.editHistory.push({ path: filePath, before: content, after: updated, timestamp: Date.now() });
111
- session.filesModified.add(filePath);
112
- // Generate a mini diff
113
- const oldLines = args.old_str.split('\n');
114
- const newLines = args.new_str.split('\n');
115
- let diff = `Edited: ${filePath}\n`;
116
- oldLines.forEach(l => diff += `- ${l}\n`);
117
- newLines.forEach(l => diff += `+ ${l}\n`);
118
- return diff;
119
- }
120
-
121
- case 'Patch': {
122
- const filePath = resolvePath(args.path);
123
- if (!fs.existsSync(filePath)) return `Error: File not found: ${filePath}`;
124
- let content = fs.readFileSync(filePath, 'utf-8');
125
- const before = content;
126
- const results = [];
127
- for (let i = 0; i < args.edits.length; i++) {
128
- const edit = args.edits[i];
129
- if (!content.includes(edit.old_str)) {
130
- results.push(`Edit ${i + 1}: FAILED - old_str not found`);
131
- continue;
132
- }
133
- content = content.replace(edit.old_str, edit.new_str);
134
- results.push(`Edit ${i + 1}: OK`);
135
- }
136
- fs.writeFileSync(filePath, content, 'utf-8');
137
- session.editHistory.push({ path: filePath, before, after: content, timestamp: Date.now() });
138
- session.filesModified.add(filePath);
139
- return `Patched: ${filePath}\n${results.join('\n')}`;
140
- }
141
-
142
- case 'Bash': {
143
- const cwd = args.cwd ? resolvePath(args.cwd) : PROJECT_ROOT;
144
- session.commandsRun.push(args.command);
145
- try {
146
- const output = execSync(args.command, {
147
- encoding: 'utf-8',
148
- timeout: TOOL_TIMEOUT,
149
- cwd,
150
- maxBuffer: 1024 * 1024 * 5,
151
- stdio: ['pipe', 'pipe', 'pipe'],
152
- });
153
- return truncateOutput(output || '(no output)');
154
- } catch (err) {
155
- // Return both stdout and stderr on failure
156
- const stdout = err.stdout || '';
157
- const stderr = err.stderr || '';
158
- const exitCode = err.status || 1;
159
- return truncateOutput(`Exit code: ${exitCode}\n${stdout}\n${stderr}`.trim());
160
- }
161
- }
162
-
163
- case 'Grep': {
164
- const searchPath = resolvePath(args.path);
165
- const flags = args.case_sensitive ? '' : '-i';
166
- const include = args.include ? `--include='${args.include}'` : '';
167
- try {
168
- const cmd = `grep -rn ${flags} ${include} --color=never "${args.pattern.replace(/"/g, '\\"')}" "${searchPath}" 2>/dev/null | head -80`;
169
- const output = execSync(cmd, { encoding: 'utf-8', timeout: 15000 });
170
- return truncateOutput(output || 'No matches found.');
171
- } catch {
172
- return 'No matches found.';
173
- }
174
- }
175
-
176
- case 'Glob': {
177
- const cwd = args.cwd ? resolvePath(args.cwd) : PROJECT_ROOT;
178
- try {
179
- // Use find command to simulate glob
180
- const pattern = args.pattern;
181
- let cmd;
182
- if (pattern.includes('**')) {
183
- const namePattern = pattern.replace(/\*\*\//g, '').replace(/\*/g, '*');
184
- cmd = `find "${cwd}" -name "${namePattern}" -not -path "*/node_modules/*" -not -path "*/.git/*" 2>/dev/null | head -100`;
185
- } else {
186
- cmd = `find "${cwd}" -name "${pattern}" -not -path "*/node_modules/*" -not -path "*/.git/*" 2>/dev/null | head -100`;
187
- }
188
- const output = execSync(cmd, { encoding: 'utf-8', timeout: 10000 });
189
- if (!output.trim()) return 'No files found matching pattern.';
190
- // Make paths relative
191
- const files = output.trim().split('\n').map(f => path.relative(cwd, f)).sort();
192
- return files.join('\n');
193
- } catch {
194
- return 'No files found matching pattern.';
195
- }
196
- }
197
-
198
- case 'ListDir': {
199
- const dirPath = resolvePath(args.path);
200
- if (!fs.existsSync(dirPath)) return `Error: Directory not found: ${dirPath}`;
201
- const stat = fs.statSync(dirPath);
202
- if (!stat.isDirectory()) return `Error: ${dirPath} is not a directory.`;
203
-
204
- function listRecursive(dir, depth, maxDepth) {
205
- const entries = fs.readdirSync(dir, { withFileTypes: true })
206
- .filter(e => !e.name.startsWith('.') && e.name !== 'node_modules')
207
- .sort((a, b) => {
208
- if (a.isDirectory() && !b.isDirectory()) return -1;
209
- if (!a.isDirectory() && b.isDirectory()) return 1;
210
- return a.name.localeCompare(b.name);
211
- });
212
- const lines = [];
213
- for (const entry of entries) {
214
- const prefix = ' '.repeat(depth);
215
- if (entry.isDirectory()) {
216
- lines.push(`${prefix}${entry.name}/`);
217
- if (depth < maxDepth) {
218
- lines.push(...listRecursive(path.join(dir, entry.name), depth + 1, maxDepth));
219
- }
220
- } else {
221
- const size = fs.statSync(path.join(dir, entry.name)).size;
222
- const sizeStr = size < 1024 ? `${size}B` : size < 1024 * 1024 ? `${(size / 1024).toFixed(1)}K` : `${(size / (1024 * 1024)).toFixed(1)}M`;
223
- lines.push(`${prefix}${entry.name} (${sizeStr})`);
224
- }
225
- }
226
- return lines;
227
- }
228
-
229
- const maxDepth = args.recursive ? 3 : 0;
230
- const lines = listRecursive(dirPath, 0, maxDepth);
231
- return truncateOutput(lines.join('\n') || '(empty directory)');
232
- }
233
-
234
- case 'UndoEdit': {
235
- const filePath = resolvePath(args.path);
236
- const lastEdit = [...session.editHistory].reverse().find(e => e.path === filePath);
237
- if (!lastEdit) return `Error: No edit history for ${filePath}`;
238
- fs.writeFileSync(filePath, lastEdit.before, 'utf-8');
239
- session.editHistory = session.editHistory.filter(e => e !== lastEdit);
240
- return `Undone last edit to ${filePath}`;
241
- }
242
-
243
- case 'Task': {
244
- const results = [];
245
- for (const cmd of args.commands) {
246
- try {
247
- const output = execSync(cmd, {
248
- encoding: 'utf-8',
249
- timeout: TOOL_TIMEOUT,
250
- cwd: PROJECT_ROOT,
251
- maxBuffer: 1024 * 1024 * 5,
252
- stdio: ['pipe', 'pipe', 'pipe'],
253
- });
254
- results.push(`✓ ${cmd}\n${output.trim()}`);
255
- session.commandsRun.push(cmd);
256
- } catch (err) {
257
- results.push(`✗ ${cmd}\nExit code: ${err.status}\n${(err.stdout || '').trim()}\n${(err.stderr || '').trim()}`);
258
- session.commandsRun.push(cmd);
259
- break; // Stop on first failure
260
- }
261
- }
262
- return truncateOutput(`Task: ${args.description}\n${'─'.repeat(40)}\n${results.join('\n\n')}`);
263
- }
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
-
424
- case 'CodeReview': {
425
- // Auto-collect all modified files from this session
426
- const allFiles = new Set([...session.filesModified]);
427
- // Add any extra files the agent explicitly passed
428
- if (args.files && args.files.length) {
429
- for (const f of args.files) allFiles.add(resolvePath(f));
430
- }
431
-
432
- if (allFiles.size === 0) {
433
- return 'CodeReview skipped — no files were modified this session.';
434
- }
435
-
436
- // Gather file contents for the reviewer's context
437
- const fileContents = [];
438
- for (const filePath of allFiles) {
439
- if (!fs.existsSync(filePath)) {
440
- fileContents.push(`--- ${filePath} ---\n[File not found]`);
441
- continue;
442
- }
443
- const stat = fs.statSync(filePath);
444
- if (stat.isDirectory()) continue;
445
- const content = fs.readFileSync(filePath, 'utf-8');
446
- fileContents.push(`--- ${path.relative(PROJECT_ROOT, filePath) || filePath} ---\n${content}`);
447
- }
448
-
449
- // Build git diff of modified files for richer context
450
- let gitDiff = '';
451
- try {
452
- gitDiff = execSync('git diff 2>/dev/null', { encoding: 'utf-8', cwd: PROJECT_ROOT, timeout: 10000 }).trim();
453
- } catch {}
454
-
455
- const reviewMessages = [
456
- {
457
- role: 'system',
458
- content: REVIEWER_SYSTEM_PROMPT,
459
- },
460
- {
461
- role: 'user',
462
- content: `# What was changed\n${args.prompt}\n\n# Modified files (${allFiles.size})\n\n${fileContents.join('\n\n')}${gitDiff ? `\n\n# Git diff\n\`\`\`diff\n${gitDiff}\n\`\`\`` : ''}`,
463
- },
464
- ];
465
-
466
- try {
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);
476
- } catch (apiErr) {
477
- return `Error: Code review failed — ${apiErr.message}`;
478
- }
479
- }
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
-
845
- default:
846
- return `Unknown tool: ${name}`;
847
- }
848
- } catch (err) {
849
- return `Error executing ${name}: ${err.message}`;
850
- }
851
- }
852
-
853
- module.exports = { executeTool };