apex-dev 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/.config/amp/settings.json +3 -0
  2. package/.config/opencode/oh-my-opencode.json +58 -0
  3. package/.config/opencode/opencode.json +6 -0
  4. package/.local/share/amp/device-id.json +3 -0
  5. package/.local/share/amp/history.jsonl +33 -0
  6. package/.local/share/amp/session.json +6 -0
  7. package/.local/share/amp/threads/T-019c93b8-fce7-7083-aab9-d5f1c88a9545.json +2528 -0
  8. package/.local/share/amp/threads/T-019c93c8-4b7a-71df-94ac-867d8236a288.json +7 -0
  9. package/.local/share/amp/threads/T-019c93cd-5a7d-728e-8289-02e0ef4ca2ff.json +680 -0
  10. package/.local/share/amp/threads/T-019c93e7-83ca-7633-9eed-12bdcc118163.json +873 -0
  11. package/.local/share/amp/threads/T-019c93ea-ccd3-765a-88c9-42d7b631e977.json +620 -0
  12. package/.local/share/amp/threads/T-019c93ee-5977-71af-9ab7-c4611004b703.json +1000 -0
  13. package/.local/share/amp/threads/T-019c93f0-8328-71ed-a250-6da169cebfe1.json +829 -0
  14. package/.local/share/amp/threads/T-019c93f5-7bdd-703b-b2cd-0a04da64441a.json +459 -0
  15. package/.local/share/amp/threads/T-019c93f8-2b2e-733b-8249-9876546d9b5b.json +764 -0
  16. package/.local/share/amp/threads/T-019c93fd-fade-7195-a3b7-358f180d40b8.json +7 -0
  17. package/.local/share/amp/threads/T-019c93fe-2e56-705e-827e-eb99bd02e257.json +3593 -0
  18. package/.local/share/amp/threads/T-019c9408-6e64-77e1-9519-b913e3b24a03.json +1559 -0
  19. package/.local/share/amp/threads/T-019c9409-feeb-736d-b92c-4f7a263a643c.json +7 -0
  20. package/.local/share/amp/threads/T-019c940b-8d11-755b-b9e1-f923d8a5e6ba.json +7 -0
  21. package/.local/share/amp/threads/T-019c943a-6c5e-76a5-bf4e-170f7ad452ce.json +979 -0
  22. package/.local/share/amp/threads/T-019c94b2-1c8f-76d8-96d0-82449a028849.json +1584 -0
  23. package/.local/share/amp/threads/T-019c94b6-68f0-726e-92dd-90c5411ca28c.json +7 -0
  24. package/.local/share/amp/threads/T-019c94bf-a589-72a3-b3c2-a81359d9e0a6.json +7 -0
  25. package/.local/share/amp/threads/T-019c94e1-1bd9-70ab-b6f2-abd5cab4f4ce.json +1035 -0
  26. package/.local/share/amp/threads/T-019c94fd-cc4a-714b-896a-74f94020f6eb.json +1310 -0
  27. package/.local/share/amp/threads/T-019c9501-8976-7138-aca6-245a01a8fe9b.json +7 -0
  28. package/.local/share/amp/threads/T-019c9504-4b51-763e-8a9f-5d4cdfcf0cfa.json +496 -0
  29. package/.local/share/amp/threads/T-019c9506-4e3b-74fd-8eda-cedbf3793598.json +2679 -0
  30. package/.local/share/amp/threads/T-019c9508-178c-718c-88d2-caf816d64f65.json +965 -0
  31. package/.local/share/amp/threads/T-019c9509-2812-71fd-8fd2-923e29ad34fa.json +7 -0
  32. package/.local/share/kilo/kilo.db +0 -0
  33. package/.local/share/kilo/kilo.db-shm +0 -0
  34. package/.local/share/kilo/kilo.db-wal +0 -0
  35. package/.local/share/kilo/storage/migration +1 -0
  36. package/.local/share/kilo/storage/session_diff/ses_36bea4cb9ffe1b0j5HEL14KEaU.json +1 -0
  37. package/.local/share/kilo/storage/session_diff/ses_36beaa8f2ffeeZ3Y39SQ9UDWQQ.json +1 -0
  38. package/.local/share/kilo/telemetry-id +1 -0
  39. package/.local/share/opencode/auth.json +6 -0
  40. package/.local/share/opencode/opencode.db +0 -0
  41. package/.local/share/opencode/opencode.db-shm +0 -0
  42. package/.local/share/opencode/opencode.db-wal +0 -0
  43. package/.local/share/opencode/storage/agent-usage-reminder/ses_36bee9f1effeJbiHHLWLR6O3WJ.json +6 -0
  44. package/.local/share/opencode/storage/agent-usage-reminder/ses_36c25e50affef2nhaXq9aSgKH3.json +6 -0
  45. package/.local/share/opencode/storage/agent-usage-reminder/ses_36c260708ffel4wG4yhdo0knDD.json +6 -0
  46. package/.local/share/opencode/storage/agent-usage-reminder/ses_36c261531ffeoVcvqXxry2bN9H.json +6 -0
  47. package/.local/share/opencode/storage/agent-usage-reminder/ses_36c291bddffePWRiaFLLJAC1y7.json +6 -0
  48. package/.local/share/opencode/storage/migration +1 -0
  49. package/.local/share/opencode/storage/session_diff/ses_36bee9f1effeJbiHHLWLR6O3WJ.json +1 -0
  50. package/.local/share/opencode/storage/session_diff/ses_36c25e50affef2nhaXq9aSgKH3.json +1 -0
  51. package/.local/share/opencode/storage/session_diff/ses_36c260708ffel4wG4yhdo0knDD.json +1 -0
  52. package/.local/share/opencode/storage/session_diff/ses_36c261531ffeoVcvqXxry2bN9H.json +1 -0
  53. package/.local/share/opencode/storage/session_diff/ses_36c291bddffePWRiaFLLJAC1y7.json +1 -0
  54. package/.local/share/opencode/storage/session_diff/ses_36c2af1c5ffegxEaOZOGcVykyy.json +1 -0
  55. package/.local/share/opencode/storage/session_diff/ses_36c2be235ffeOa6x8UCk1HW4kU.json +1 -0
  56. package/.local/share/opencode/tool-output/tool_c93da840c0016GrdyAkOnHGezU +2330 -0
  57. package/.local/share/opencode/tool-output/tool_c9411e784001cRoQqwVDb1a6lY +1017 -0
  58. package/.replit +21 -0
  59. package/.upm/store.json +1 -0
  60. package/bun.lock +237 -0
  61. package/generated-icon.png +0 -0
  62. package/index.js +1587 -0
  63. package/package.json +24 -0
package/index.js ADDED
@@ -0,0 +1,1587 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const chalk = require('chalk');
5
+ const ora = require('ora');
6
+ const boxen = require('boxen');
7
+ const figures = require('figures');
8
+ const readline = require('readline');
9
+ const { highlight } = require('cli-highlight');
10
+ const OpenAI = require('openai');
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { execSync, exec } = require('child_process');
14
+ const { promisify } = require('util');
15
+ const execAsync = promisify(exec);
16
+
17
+ // ===== Config =====
18
+ const NVIDIA_MODEL = 'nvidia/llama-3.1-nemotron-ultra-253b-v1';
19
+ const REVIEWER_MODEL = 'nvidia/llama-3.1-nemotron-ultra-253b-v1';
20
+ const MAX_TOOL_ITERATIONS = 50;
21
+ const MAX_OUTPUT_LEN = 12000;
22
+ const TOOL_TIMEOUT = 60000;
23
+ const PROJECT_ROOT = process.cwd();
24
+
25
+ const nvidiaClient = new OpenAI({
26
+ apiKey: process.env.NVIDIA_API_KEY || '',
27
+ baseURL: 'https://integrate.api.nvidia.com/v1',
28
+ });
29
+
30
+ // ===== Session State =====
31
+ const session = {
32
+ conversationHistory: [],
33
+ totalTokens: 0,
34
+ totalCost: 0,
35
+ toolCallCount: 0,
36
+ filesModified: new Set(),
37
+ filesRead: new Set(),
38
+ commandsRun: [],
39
+ editHistory: [], // { path, before, after, timestamp }
40
+ startTime: Date.now(),
41
+ turnCount: 0,
42
+ };
43
+
44
+ // ===== Agentic System Prompt =====
45
+ function buildSystemPrompt() {
46
+ let gitInfo = '';
47
+ try {
48
+ const branch = execSync('git rev-parse --abbrev-ref HEAD 2>/dev/null', { encoding: 'utf-8', cwd: PROJECT_ROOT }).trim();
49
+ const status = execSync('git status --short 2>/dev/null', { encoding: 'utf-8', cwd: PROJECT_ROOT }).trim();
50
+ const remoteUrl = execSync('git config --get remote.origin.url 2>/dev/null', { encoding: 'utf-8', cwd: PROJECT_ROOT }).trim();
51
+ gitInfo = `\nGit branch: ${branch}\nGit remote: ${remoteUrl}\nGit status:\n${status || '(clean)'}`;
52
+ } catch {}
53
+
54
+ let projectInfo = '';
55
+ try {
56
+ const pkg = JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, 'package.json'), 'utf-8'));
57
+ projectInfo = `\nProject: ${pkg.name || 'unknown'} v${pkg.version || '0.0.0'}`;
58
+ if (pkg.dependencies) projectInfo += `\nDependencies: ${Object.keys(pkg.dependencies).join(', ')}`;
59
+ if (pkg.devDependencies) projectInfo += `\nDev dependencies: ${Object.keys(pkg.devDependencies).join(', ')}`;
60
+ if (pkg.scripts) projectInfo += `\nScripts: ${Object.keys(pkg.scripts).join(', ')}`;
61
+ } catch {}
62
+
63
+ return `detailed thinking on
64
+
65
+ You are Apex AI, a friendly and approachable coding assistant. You're warm, helpful, and conversational — like a knowledgeable friend who loves coding.
66
+
67
+ # Personality
68
+
69
+ - Be friendly, warm, and encouraging. Use a casual but professional tone.
70
+ - When a user asks a question that doesn't need any tools (like general knowledge, advice, greetings, or casual chat), just answer directly and naturally. You do NOT always need to use tools — only use them when the task actually requires reading, writing, or executing something on the filesystem.
71
+ - NEVER say things like "I don't have any tool to call" or "I don't have the tools for that." Just respond helpfully with what you know.
72
+ - If you're unsure, say so honestly, but still try your best to help.
73
+
74
+ # Core Principles
75
+
76
+ 1. **Take initiative.** When asked to do something, DO it — don't just explain how. Use your tools to read, write, edit, and execute code until the task is COMPLETE.
77
+ 2. **Read before you edit.** NEVER modify a file you haven't read first. Always understand existing code before making changes.
78
+ 3. **Work incrementally.** Make small changes, verify they work (run tests, check syntax), then continue. Don't make huge changes blind.
79
+ 4. **Verify your work.** After making changes, run the relevant tests, linter, or type checker. If something fails, fix it before responding.
80
+ 5. **Follow conventions.** Match the existing code style, frameworks, libraries, and patterns. Check imports and neighboring files first.
81
+ 6. **Minimal changes.** Only change what's needed. Don't refactor, add features, or "improve" code beyond what was asked.
82
+ 7. **Keep going.** If a tool call fails, try to recover. If you need more context, read more files. Don't give up after one attempt.
83
+
84
+ # Tool Usage
85
+
86
+ - Use Bash for running commands (tests, builds, linters, git operations)
87
+ - Use Read to understand files before modifying them
88
+ - Use Grep/Glob/ListDir to explore the codebase and find relevant code
89
+ - Use Edit for surgical changes to existing files (preferred over Write for existing files)
90
+ - Use Write only for creating new files
91
+ - Use Patch for making multiple edits to the same file at once
92
+ - Execute multiple independent tool calls in parallel when possible
93
+ - After editing code, verify with: syntax check, run tests, or at minimum read the changed section
94
+
95
+ # Workflow for Code Tasks
96
+
97
+ 1. Explore: Use ListDir, Glob, Grep to understand the codebase structure
98
+ 2. Read: Read relevant files to understand context, conventions, dependencies
99
+ 3. Plan: Think about what changes are needed (briefly)
100
+ 4. Implement: Make changes using Edit/Write
101
+ 5. Verify: Run tests or check the result with Bash
102
+ 6. Iterate: If verification fails, fix and re-verify
103
+ 7. **Review: ALWAYS call CodeReview as the FINAL step after any code changes.** Pass a summary of what you did. It will auto-detect all files you modified and send them to a senior reviewer sub-agent. If the reviewer finds critical or warning-level issues, fix them before responding to the user.
104
+
105
+ # Environment
106
+ Working directory: ${PROJECT_ROOT}
107
+ OS: ${process.platform}
108
+ Node: ${process.version}${projectInfo}${gitInfo}
109
+
110
+ # Important
111
+ - You can call multiple tools at once — do so when the calls are independent
112
+ - Don't ask for permission to use tools — just use them
113
+ - Don't explain what you're going to do in detail — just do it and summarize what you did
114
+ - If the user asks you to fix something, find the bug AND fix it
115
+ - If the user asks you to build something, build it completely and verify it works
116
+ - Maximum tool iterations per turn: ${MAX_TOOL_ITERATIONS}`;
117
+ }
118
+
119
+ // ===== Tool Definitions =====
120
+ const toolDefs = [
121
+ {
122
+ type: 'function',
123
+ function: {
124
+ name: 'Read',
125
+ description: 'Read the contents of a file. Returns line-numbered content. Always read a file before editing it.',
126
+ parameters: {
127
+ type: 'object',
128
+ properties: {
129
+ path: { type: 'string', description: 'File path to read (absolute or relative to project root).' },
130
+ start_line: { type: 'number', description: 'Start line (1-indexed). Omit to read from beginning.' },
131
+ end_line: { type: 'number', description: 'End line (1-indexed). Omit to read to end (max 500 lines).' },
132
+ },
133
+ required: ['path'],
134
+ },
135
+ },
136
+ },
137
+ {
138
+ type: 'function',
139
+ function: {
140
+ name: 'Write',
141
+ description: 'Create a new file or completely overwrite an existing file. For modifying existing files, prefer Edit instead.',
142
+ parameters: {
143
+ type: 'object',
144
+ properties: {
145
+ path: { type: 'string', description: 'File path to write.' },
146
+ content: { type: 'string', description: 'Full content to write.' },
147
+ },
148
+ required: ['path', 'content'],
149
+ },
150
+ },
151
+ },
152
+ {
153
+ type: 'function',
154
+ function: {
155
+ name: 'Edit',
156
+ description: 'Replace an exact string in a file with new content. The old_str must match exactly (including whitespace). For existing files, this is preferred over Write.',
157
+ parameters: {
158
+ type: 'object',
159
+ properties: {
160
+ path: { type: 'string', description: 'File path to edit.' },
161
+ old_str: { type: 'string', description: 'Exact string to find (must be unique in the file).' },
162
+ new_str: { type: 'string', description: 'Replacement string.' },
163
+ },
164
+ required: ['path', 'old_str', 'new_str'],
165
+ },
166
+ },
167
+ },
168
+ {
169
+ type: 'function',
170
+ function: {
171
+ name: 'Patch',
172
+ description: 'Apply multiple find-and-replace edits to a single file atomically. Use when you need to make several changes to the same file.',
173
+ parameters: {
174
+ type: 'object',
175
+ properties: {
176
+ path: { type: 'string', description: 'File path to patch.' },
177
+ edits: {
178
+ type: 'array',
179
+ description: 'Array of edits to apply in order.',
180
+ items: {
181
+ type: 'object',
182
+ properties: {
183
+ old_str: { type: 'string', description: 'Exact string to find.' },
184
+ new_str: { type: 'string', description: 'Replacement string.' },
185
+ },
186
+ required: ['old_str', 'new_str'],
187
+ },
188
+ },
189
+ },
190
+ required: ['path', 'edits'],
191
+ },
192
+ },
193
+ },
194
+ {
195
+ type: 'function',
196
+ function: {
197
+ name: 'Bash',
198
+ description: 'Execute a shell command. Use for running tests, builds, git commands, installing packages, checking syntax, etc. Commands have a 60-second timeout.',
199
+ parameters: {
200
+ type: 'object',
201
+ properties: {
202
+ command: { type: 'string', description: 'Shell command to execute.' },
203
+ cwd: { type: 'string', description: 'Working directory (defaults to project root).' },
204
+ },
205
+ required: ['command'],
206
+ },
207
+ },
208
+ },
209
+ {
210
+ type: 'function',
211
+ function: {
212
+ name: 'Grep',
213
+ description: 'Search for a pattern across files using regex. Returns matching lines with file paths and line numbers.',
214
+ parameters: {
215
+ type: 'object',
216
+ properties: {
217
+ pattern: { type: 'string', description: 'Regex pattern to search for.' },
218
+ path: { type: 'string', description: 'Directory or file to search in (defaults to project root).' },
219
+ include: { type: 'string', description: 'File glob pattern to include, e.g. "*.js" or "*.ts"' },
220
+ case_sensitive: { type: 'boolean', description: 'Case-sensitive search (default: false).' },
221
+ },
222
+ required: ['pattern'],
223
+ },
224
+ },
225
+ },
226
+ {
227
+ type: 'function',
228
+ function: {
229
+ name: 'Glob',
230
+ description: 'Find files matching a glob pattern. Returns file paths sorted by modification time.',
231
+ parameters: {
232
+ type: 'object',
233
+ properties: {
234
+ pattern: { type: 'string', description: 'Glob pattern like "**/*.js", "src/**/*.ts", "*.json"' },
235
+ cwd: { type: 'string', description: 'Base directory for the search (defaults to project root).' },
236
+ },
237
+ required: ['pattern'],
238
+ },
239
+ },
240
+ },
241
+ {
242
+ type: 'function',
243
+ function: {
244
+ name: 'ListDir',
245
+ description: 'List the contents of a directory. Shows files and subdirectories with type indicators.',
246
+ parameters: {
247
+ type: 'object',
248
+ properties: {
249
+ path: { type: 'string', description: 'Directory path to list (defaults to project root).' },
250
+ recursive: { type: 'boolean', description: 'If true, list recursively (max depth 3).' },
251
+ },
252
+ required: [],
253
+ },
254
+ },
255
+ },
256
+ {
257
+ type: 'function',
258
+ function: {
259
+ name: 'UndoEdit',
260
+ description: 'Undo the last edit made to a specific file, restoring its previous content.',
261
+ parameters: {
262
+ type: 'object',
263
+ properties: {
264
+ path: { type: 'string', description: 'File path to undo the last edit for.' },
265
+ },
266
+ required: ['path'],
267
+ },
268
+ },
269
+ },
270
+ {
271
+ type: 'function',
272
+ function: {
273
+ name: 'Task',
274
+ description: 'Spawn a sub-task by executing a sequence of shell commands for a complex multi-step operation. Useful for build-test-fix cycles.',
275
+ parameters: {
276
+ type: 'object',
277
+ properties: {
278
+ description: { type: 'string', description: 'Brief description of the task.' },
279
+ commands: {
280
+ type: 'array',
281
+ description: 'Shell commands to execute in sequence. Stops on first failure.',
282
+ items: { type: 'string' },
283
+ },
284
+ },
285
+ required: ['description', 'commands'],
286
+ },
287
+ },
288
+ },
289
+ {
290
+ type: 'function',
291
+ function: {
292
+ name: 'CodeReview',
293
+ description: 'MANDATORY — call this as the FINAL step after completing any code-writing or code-editing task. Spawns a code-review sub-agent that reviews ALL files you modified during this turn. Provide a summary of what you built/changed so the reviewer has context. The reviewer will analyse your changes for bugs, logic errors, style issues, and missed edge cases, then return its findings. If the reviewer finds problems, fix them before responding to the user.',
294
+ parameters: {
295
+ type: 'object',
296
+ properties: {
297
+ prompt: { type: 'string', description: 'Summary of what you built or changed, so the reviewer understands the intent behind the code. Include the original user request and any key design decisions.' },
298
+ files: {
299
+ type: 'array',
300
+ description: 'Additional file paths to include beyond the auto-detected modified files (e.g. related files for context). Modified files are always included automatically.',
301
+ items: { type: 'string' },
302
+ },
303
+ },
304
+ required: ['prompt'],
305
+ },
306
+ },
307
+ },
308
+ ];
309
+
310
+ // ===== Tool Executors =====
311
+ function truncateOutput(str) {
312
+ if (str.length > MAX_OUTPUT_LEN) {
313
+ return str.slice(0, MAX_OUTPUT_LEN) + `\n... (truncated, ${str.length} chars total)`;
314
+ }
315
+ return str;
316
+ }
317
+
318
+ function resolvePath(p) {
319
+ if (!p) return PROJECT_ROOT;
320
+ return path.isAbsolute(p) ? p : path.resolve(PROJECT_ROOT, p);
321
+ }
322
+
323
+ async function executeTool(name, args) {
324
+ try {
325
+ switch (name) {
326
+ case 'Read': {
327
+ const filePath = resolvePath(args.path);
328
+ const stat = fs.statSync(filePath, { throwIfNoEntry: false });
329
+ if (!stat) return `Error: File not found: ${filePath}`;
330
+ if (stat.isDirectory()) return `Error: ${filePath} is a directory. Use ListDir instead.`;
331
+ const content = fs.readFileSync(filePath, 'utf-8');
332
+ const lines = content.split('\n');
333
+ const start = Math.max(0, (args.start_line || 1) - 1);
334
+ const end = args.end_line ? Math.min(lines.length, args.end_line) : Math.min(lines.length, start + 500);
335
+ const slice = lines.slice(start, end);
336
+ const numbered = slice.map((l, i) => `${start + i + 1}: ${l}`).join('\n');
337
+ session.filesRead.add(filePath);
338
+ if (end < lines.length) {
339
+ return truncateOutput(numbered) + `\n(showing lines ${start + 1}-${end} of ${lines.length})`;
340
+ }
341
+ return truncateOutput(numbered);
342
+ }
343
+
344
+ case 'Write': {
345
+ const filePath = resolvePath(args.path);
346
+ const dir = path.dirname(filePath);
347
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
348
+ const existed = fs.existsSync(filePath);
349
+ const before = existed ? fs.readFileSync(filePath, 'utf-8') : null;
350
+ fs.writeFileSync(filePath, args.content, 'utf-8');
351
+ if (before !== null) {
352
+ session.editHistory.push({ path: filePath, before, after: args.content, timestamp: Date.now() });
353
+ }
354
+ session.filesModified.add(filePath);
355
+ const lines = args.content.split('\n').length;
356
+ return `${existed ? 'Overwritten' : 'Created'}: ${filePath} (${lines} lines)`;
357
+ }
358
+
359
+ case 'Edit': {
360
+ const filePath = resolvePath(args.path);
361
+ if (!fs.existsSync(filePath)) return `Error: File not found: ${filePath}`;
362
+ const content = fs.readFileSync(filePath, 'utf-8');
363
+ const count = content.split(args.old_str).length - 1;
364
+ if (count === 0) return `Error: old_str not found in ${path.basename(filePath)}. Make sure it matches exactly (including whitespace and indentation).`;
365
+ 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.`;
366
+ const updated = content.replace(args.old_str, args.new_str);
367
+ fs.writeFileSync(filePath, updated, 'utf-8');
368
+ session.editHistory.push({ path: filePath, before: content, after: updated, timestamp: Date.now() });
369
+ session.filesModified.add(filePath);
370
+ // Generate a mini diff
371
+ const oldLines = args.old_str.split('\n');
372
+ const newLines = args.new_str.split('\n');
373
+ let diff = `Edited: ${filePath}\n`;
374
+ oldLines.forEach(l => diff += `- ${l}\n`);
375
+ newLines.forEach(l => diff += `+ ${l}\n`);
376
+ return diff;
377
+ }
378
+
379
+ case 'Patch': {
380
+ const filePath = resolvePath(args.path);
381
+ if (!fs.existsSync(filePath)) return `Error: File not found: ${filePath}`;
382
+ let content = fs.readFileSync(filePath, 'utf-8');
383
+ const before = content;
384
+ const results = [];
385
+ for (let i = 0; i < args.edits.length; i++) {
386
+ const edit = args.edits[i];
387
+ if (!content.includes(edit.old_str)) {
388
+ results.push(`Edit ${i + 1}: FAILED - old_str not found`);
389
+ continue;
390
+ }
391
+ content = content.replace(edit.old_str, edit.new_str);
392
+ results.push(`Edit ${i + 1}: OK`);
393
+ }
394
+ fs.writeFileSync(filePath, content, 'utf-8');
395
+ session.editHistory.push({ path: filePath, before, after: content, timestamp: Date.now() });
396
+ session.filesModified.add(filePath);
397
+ return `Patched: ${filePath}\n${results.join('\n')}`;
398
+ }
399
+
400
+ case 'Bash': {
401
+ const cwd = args.cwd ? resolvePath(args.cwd) : PROJECT_ROOT;
402
+ session.commandsRun.push(args.command);
403
+ try {
404
+ const output = execSync(args.command, {
405
+ encoding: 'utf-8',
406
+ timeout: TOOL_TIMEOUT,
407
+ cwd,
408
+ maxBuffer: 1024 * 1024 * 5,
409
+ stdio: ['pipe', 'pipe', 'pipe'],
410
+ });
411
+ return truncateOutput(output || '(no output)');
412
+ } catch (err) {
413
+ // Return both stdout and stderr on failure
414
+ const stdout = err.stdout || '';
415
+ const stderr = err.stderr || '';
416
+ const exitCode = err.status || 1;
417
+ return truncateOutput(`Exit code: ${exitCode}\n${stdout}\n${stderr}`.trim());
418
+ }
419
+ }
420
+
421
+ case 'Grep': {
422
+ const searchPath = resolvePath(args.path);
423
+ const flags = args.case_sensitive ? '' : '-i';
424
+ const include = args.include ? `--include='${args.include}'` : '';
425
+ try {
426
+ const cmd = `grep -rn ${flags} ${include} --color=never "${args.pattern.replace(/"/g, '\\"')}" "${searchPath}" 2>/dev/null | head -80`;
427
+ const output = execSync(cmd, { encoding: 'utf-8', timeout: 15000 });
428
+ return truncateOutput(output || 'No matches found.');
429
+ } catch {
430
+ return 'No matches found.';
431
+ }
432
+ }
433
+
434
+ case 'Glob': {
435
+ const cwd = args.cwd ? resolvePath(args.cwd) : PROJECT_ROOT;
436
+ try {
437
+ // Use find command to simulate glob
438
+ const pattern = args.pattern;
439
+ let cmd;
440
+ if (pattern.includes('**')) {
441
+ const namePattern = pattern.replace(/\*\*\//g, '').replace(/\*/g, '*');
442
+ cmd = `find "${cwd}" -name "${namePattern}" -not -path "*/node_modules/*" -not -path "*/.git/*" 2>/dev/null | head -100`;
443
+ } else {
444
+ cmd = `find "${cwd}" -name "${pattern}" -not -path "*/node_modules/*" -not -path "*/.git/*" 2>/dev/null | head -100`;
445
+ }
446
+ const output = execSync(cmd, { encoding: 'utf-8', timeout: 10000 });
447
+ if (!output.trim()) return 'No files found matching pattern.';
448
+ // Make paths relative
449
+ const files = output.trim().split('\n').map(f => path.relative(cwd, f)).sort();
450
+ return files.join('\n');
451
+ } catch {
452
+ return 'No files found matching pattern.';
453
+ }
454
+ }
455
+
456
+ case 'ListDir': {
457
+ const dirPath = resolvePath(args.path);
458
+ if (!fs.existsSync(dirPath)) return `Error: Directory not found: ${dirPath}`;
459
+ const stat = fs.statSync(dirPath);
460
+ if (!stat.isDirectory()) return `Error: ${dirPath} is not a directory.`;
461
+
462
+ function listRecursive(dir, depth, maxDepth) {
463
+ const entries = fs.readdirSync(dir, { withFileTypes: true })
464
+ .filter(e => !e.name.startsWith('.') && e.name !== 'node_modules')
465
+ .sort((a, b) => {
466
+ if (a.isDirectory() && !b.isDirectory()) return -1;
467
+ if (!a.isDirectory() && b.isDirectory()) return 1;
468
+ return a.name.localeCompare(b.name);
469
+ });
470
+ const lines = [];
471
+ for (const entry of entries) {
472
+ const prefix = ' '.repeat(depth);
473
+ if (entry.isDirectory()) {
474
+ lines.push(`${prefix}${entry.name}/`);
475
+ if (depth < maxDepth) {
476
+ lines.push(...listRecursive(path.join(dir, entry.name), depth + 1, maxDepth));
477
+ }
478
+ } else {
479
+ const size = fs.statSync(path.join(dir, entry.name)).size;
480
+ const sizeStr = size < 1024 ? `${size}B` : size < 1024 * 1024 ? `${(size / 1024).toFixed(1)}K` : `${(size / (1024 * 1024)).toFixed(1)}M`;
481
+ lines.push(`${prefix}${entry.name} (${sizeStr})`);
482
+ }
483
+ }
484
+ return lines;
485
+ }
486
+
487
+ const maxDepth = args.recursive ? 3 : 0;
488
+ const lines = listRecursive(dirPath, 0, maxDepth);
489
+ return truncateOutput(lines.join('\n') || '(empty directory)');
490
+ }
491
+
492
+ case 'UndoEdit': {
493
+ const filePath = resolvePath(args.path);
494
+ const lastEdit = [...session.editHistory].reverse().find(e => e.path === filePath);
495
+ if (!lastEdit) return `Error: No edit history for ${filePath}`;
496
+ fs.writeFileSync(filePath, lastEdit.before, 'utf-8');
497
+ session.editHistory = session.editHistory.filter(e => e !== lastEdit);
498
+ return `Undone last edit to ${filePath}`;
499
+ }
500
+
501
+ case 'Task': {
502
+ const results = [];
503
+ for (const cmd of args.commands) {
504
+ try {
505
+ const output = execSync(cmd, {
506
+ encoding: 'utf-8',
507
+ timeout: TOOL_TIMEOUT,
508
+ cwd: PROJECT_ROOT,
509
+ maxBuffer: 1024 * 1024 * 5,
510
+ stdio: ['pipe', 'pipe', 'pipe'],
511
+ });
512
+ results.push(`✓ ${cmd}\n${output.trim()}`);
513
+ session.commandsRun.push(cmd);
514
+ } catch (err) {
515
+ results.push(`✗ ${cmd}\nExit code: ${err.status}\n${(err.stdout || '').trim()}\n${(err.stderr || '').trim()}`);
516
+ session.commandsRun.push(cmd);
517
+ break; // Stop on first failure
518
+ }
519
+ }
520
+ return truncateOutput(`Task: ${args.description}\n${'─'.repeat(40)}\n${results.join('\n\n')}`);
521
+ }
522
+
523
+ case 'CodeReview': {
524
+ // Auto-collect all modified files from this session
525
+ const allFiles = new Set([...session.filesModified]);
526
+ // Add any extra files the agent explicitly passed
527
+ if (args.files && args.files.length) {
528
+ for (const f of args.files) allFiles.add(resolvePath(f));
529
+ }
530
+
531
+ if (allFiles.size === 0) {
532
+ return 'CodeReview skipped — no files were modified this session.';
533
+ }
534
+
535
+ // Gather file contents for the reviewer's context
536
+ const fileContents = [];
537
+ for (const filePath of allFiles) {
538
+ if (!fs.existsSync(filePath)) {
539
+ fileContents.push(`--- ${filePath} ---\n[File not found]`);
540
+ continue;
541
+ }
542
+ const stat = fs.statSync(filePath);
543
+ if (stat.isDirectory()) continue;
544
+ const content = fs.readFileSync(filePath, 'utf-8');
545
+ fileContents.push(`--- ${path.relative(PROJECT_ROOT, filePath) || filePath} ---\n${content}`);
546
+ }
547
+
548
+ // Build git diff of modified files for richer context
549
+ let gitDiff = '';
550
+ try {
551
+ gitDiff = execSync('git diff 2>/dev/null', { encoding: 'utf-8', cwd: PROJECT_ROOT, timeout: 10000 }).trim();
552
+ } catch {}
553
+
554
+ const reviewMessages = [
555
+ {
556
+ role: 'system',
557
+ content: `You are a senior code reviewer. An AI coding assistant just made changes to a codebase. Your job is to review those changes thoroughly and report issues. Be specific — reference exact line numbers, function names, and variables.
558
+
559
+ Focus on:
560
+ 1. **Bugs & logic errors** — incorrect conditions, off-by-one, null/undefined risks, race conditions
561
+ 2. **Security** — exposed secrets, injection risks, unsafe operations
562
+ 3. **Edge cases** — unhandled inputs, missing error handling at boundaries
563
+ 4. **Code quality** — naming, readability, dead code, unnecessary complexity
564
+ 5. **Correctness** — does the code actually fulfil the stated intent?
565
+
566
+ If everything looks good, say so briefly. If there are problems, list them clearly with severity (critical / warning / nit). You have no tools; your only output is this review.`,
567
+ },
568
+ {
569
+ role: 'user',
570
+ 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\`\`\`` : ''}`,
571
+ },
572
+ ];
573
+
574
+ try {
575
+ const reviewResponse = await nvidiaClient.chat.completions.create({
576
+ model: REVIEWER_MODEL,
577
+ messages: reviewMessages,
578
+ max_tokens: 4096,
579
+ temperature: 0.3,
580
+ });
581
+ const reviewText = reviewResponse.choices[0]?.message?.content || '(No response from reviewer)';
582
+ return truncateOutput(`Code Review (${REVIEWER_MODEL}) — ${allFiles.size} file(s)\n${'─'.repeat(40)}\n${reviewText}`);
583
+ } catch (apiErr) {
584
+ return `Error: Code review failed — ${apiErr.message}`;
585
+ }
586
+ }
587
+
588
+ default:
589
+ return `Unknown tool: ${name}`;
590
+ }
591
+ } catch (err) {
592
+ return `Error executing ${name}: ${err.message}`;
593
+ }
594
+ }
595
+
596
+ // ===== Theme =====
597
+ const t = {
598
+ brand: chalk.hex('#818cf8'),
599
+ brandBold:chalk.hex('#818cf8').bold,
600
+ accent: chalk.hex('#a78bfa'),
601
+ dim: chalk.hex('#5a5a72'),
602
+ muted: chalk.hex('#8888a0'),
603
+ text: chalk.hex('#e4e4ed'),
604
+ green: chalk.hex('#22c55e'),
605
+ greenBg: chalk.bgHex('#22c55e').hex('#000'),
606
+ yellow: chalk.hex('#eab308'),
607
+ yellowBg: chalk.bgHex('#eab308').hex('#000'),
608
+ red: chalk.hex('#ef4444'),
609
+ redBg: chalk.bgHex('#ef4444').hex('#fff'),
610
+ blue: chalk.hex('#3b82f6'),
611
+ blueBg: chalk.bgHex('#3b82f6').hex('#fff'),
612
+ cyan: chalk.hex('#22d3ee'),
613
+ orange: chalk.hex('#f97316'),
614
+ orangeBg: chalk.bgHex('#f97316').hex('#000'),
615
+ purple: chalk.hex('#a855f7'),
616
+ purpleBg: chalk.bgHex('#a855f7').hex('#fff'),
617
+ bar: chalk.hex('#2a2a3a'),
618
+ codeBg: chalk.bgHex('#16161f'),
619
+ };
620
+
621
+ // ===== Layout Helpers =====
622
+ const COLS = Math.min(process.stdout.columns || 80, 120);
623
+
624
+ function hr(char = '─', color = t.bar) {
625
+ return color(char.repeat(COLS));
626
+ }
627
+
628
+ function stripAnsi(str) {
629
+ return str.replace(/\x1b\[[0-9;]*m/g, '');
630
+ }
631
+
632
+ function indent(str, n = 2) {
633
+ const prefix = ' '.repeat(n);
634
+ return str.split('\n').map(l => prefix + l).join('\n');
635
+ }
636
+
637
+ // ===== Header =====
638
+ function showHeader() {
639
+ console.clear();
640
+ let branch = 'main';
641
+ try { branch = execSync('git rev-parse --abbrev-ref HEAD 2>/dev/null', { encoding: 'utf-8', cwd: PROJECT_ROOT }).trim(); } catch {}
642
+
643
+ const logo = t.brandBold('⬡ Apex') + t.accent(' AI');
644
+ const model = t.dim('model: ') + t.text(NVIDIA_MODEL);
645
+ const reasoning = t.accent('⟡') + t.dim(' reasoning ') + t.green('ON');
646
+ const branchLabel = t.brand('⎇') + t.accent(` ${branch}`);
647
+ const status = t.green(figures.bullet) + t.dim(' connected');
648
+
649
+ console.log();
650
+ console.log(indent(`${logo} ${t.bar('│')} ${model} ${t.bar('│')} ${reasoning} ${t.bar('│')} ${branchLabel} ${t.bar('│')} ${status}`));
651
+ console.log(indent(hr()));
652
+ console.log();
653
+ }
654
+
655
+ // ===== Welcome =====
656
+ function showWelcome() {
657
+ const title = t.text.bold('What can I help you build?');
658
+ const subtitle = t.muted('I can read, write, execute code, and autonomously complete tasks in your project.');
659
+
660
+ console.log(indent(title));
661
+ console.log(indent(subtitle));
662
+ console.log();
663
+
664
+ const actions = [
665
+ [t.blue(figures.info), t.muted('Explore & explain the codebase')],
666
+ [t.red(figures.cross), t.muted('Find & fix bugs autonomously')],
667
+ [t.green(figures.tick), t.muted('Write & run tests')],
668
+ [t.yellow(figures.warning), t.muted('Refactor & optimize')],
669
+ [t.cyan('⚡'), t.muted('Build features end-to-end')],
670
+ [t.purple('⎇'), t.muted('Git operations & code review')],
671
+ ];
672
+
673
+ actions.forEach(([icon, label]) => {
674
+ console.log(indent(` ${icon} ${label}`));
675
+ });
676
+ console.log();
677
+ console.log(indent(t.dim(`Tools: Read, Write, Edit, Patch, Bash, Grep, Glob, ListDir, UndoEdit, Task, CodeReview`)));
678
+ console.log(indent(t.dim(`Max ${MAX_TOOL_ITERATIONS} autonomous tool calls per turn`)));
679
+ console.log();
680
+ console.log(indent(hr()));
681
+ console.log();
682
+ }
683
+
684
+ // ===== Reasoning (Think Block) Parser =====
685
+ function parseThinkBlocks(text) {
686
+ const thinkRegex = /<think>([\s\S]*?)(?:<\/think>|think>)/g;
687
+ const thoughts = [];
688
+ let match;
689
+ while ((match = thinkRegex.exec(text)) !== null) {
690
+ const content = match[1].trim();
691
+ if (content) thoughts.push(content);
692
+ }
693
+ const cleaned = text.replace(/<think>[\s\S]*?(?:<\/think>|think>)/g, '').trim();
694
+ return { thoughts, content: cleaned };
695
+ }
696
+ function findThinkClose(text) {
697
+ const fullClose = text.indexOf('</think>');
698
+ if (fullClose !== -1) return { pos: fullClose, len: 8 };
699
+ // Look for bare 'think>' but not as part of '<think>'
700
+ let searchFrom = 0;
701
+ while (searchFrom < text.length) {
702
+ const idx = text.indexOf('think>', searchFrom);
703
+ if (idx === -1) break;
704
+ if (idx === 0 || text[idx - 1] !== '<') return { pos: idx, len: 6 };
705
+ searchFrom = idx + 6;
706
+ }
707
+ return null;
708
+ }
709
+ function stripStrayCloseTag(text) {
710
+ return text.replace(/<\/think>/g, '').replace(/(?<!<)think>/g, '');
711
+ }
712
+ function splitAtPartialTag(text) {
713
+ const prefixes = [
714
+ '</think>', '</think', '</thin', '</thi', '</th', '</t', '</',
715
+ '<think>', '<think', '<thin', '<thi', '<th', '<t',
716
+ 'think>', 'think', 'thin', 'thi', 'th',
717
+ '<',
718
+ ];
719
+ for (const prefix of prefixes) {
720
+ if (text.endsWith(prefix)) {
721
+ // If it's a complete closing tag, strip it entirely (stray closing tag)
722
+ if (prefix === '</think>' || prefix === 'think>') {
723
+ return { safe: text.slice(0, -prefix.length), pending: '' };
724
+ }
725
+ return { safe: text.slice(0, -prefix.length), pending: prefix };
726
+ }
727
+ }
728
+ return { safe: text, pending: '' };
729
+ }
730
+ function showThinkingBlock(thoughts) {
731
+ if (!thoughts.length) return;
732
+ const combined = thoughts.join('\n\n');
733
+ const lines = combined.split('\n');
734
+ const maxLines = 15;
735
+ const preview = lines.length > maxLines ? lines.slice(0, maxLines) : lines;
736
+ const isTruncated = lines.length > maxLines;
737
+
738
+ // Calculate dimensions
739
+ const boxWidth = Math.min(COLS - 8, 70);
740
+ const contentWidth = boxWidth - 4;
741
+
742
+ // Header with icon and label
743
+ const headerIcon = t.purple('🧠');
744
+ const headerLabel = t.purple.bold('Thinking');
745
+ const timestamp = t.dim(new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }));
746
+ const lineCount = t.dim(`${lines.length} lines`);
747
+
748
+ // Top border with gradient effect
749
+ const topLine = t.dim('┌') + t.purple('─'.repeat(Math.floor(boxWidth / 2))) + t.accent('─'.repeat(Math.ceil(boxWidth / 2))) + t.dim('┐');
750
+
751
+ console.log(indent(topLine));
752
+ console.log(indent(t.dim('│') + ' ' + headerIcon + ' ' + headerLabel + t.dim(' · ') + timestamp + t.dim(' · ') + lineCount + ' '.repeat(Math.max(0, boxWidth - stripAnsi(headerIcon + headerLabel + timestamp + lineCount).length - 8)) + t.dim('│')));
753
+
754
+ // Divider
755
+ console.log(indent(t.dim('├') + t.purple('─'.repeat(boxWidth)) + t.dim('┤')));
756
+
757
+ // Content lines with word wrap
758
+ for (const line of preview) {
759
+ // Word wrap long lines
760
+ const words = line.split(' ');
761
+ let currentLine = '';
762
+ for (const word of words) {
763
+ // Handle words longer than contentWidth
764
+ if (word.length > contentWidth) {
765
+ if (currentLine) {
766
+ console.log(indent(t.dim('│ ') + t.dim.italic(currentLine.trim()) + ' '.repeat(Math.max(0, contentWidth - stripAnsi(currentLine).length)) + t.dim('│')));
767
+ currentLine = '';
768
+ }
769
+ // Split long word into chunks
770
+ for (let i = 0; i < word.length; i += contentWidth) {
771
+ const chunk = word.slice(i, i + contentWidth);
772
+ console.log(indent(t.dim('│ ') + t.dim.italic(chunk) + ' '.repeat(Math.max(0, contentWidth - chunk.length)) + t.dim('│')));
773
+ }
774
+ continue;
775
+ }
776
+ if ((currentLine + ' ' + word).trim().length > contentWidth) {
777
+ if (currentLine) {
778
+ console.log(indent(t.dim('│ ') + t.dim.italic(currentLine.trim()) + ' '.repeat(Math.max(0, contentWidth - stripAnsi(currentLine).length)) + t.dim('│')));
779
+ }
780
+ currentLine = word;
781
+ } else {
782
+ currentLine = (currentLine + ' ' + word).trim();
783
+ }
784
+ }
785
+ if (currentLine) {
786
+ console.log(indent(t.dim('│ ') + t.dim.italic(currentLine) + ' '.repeat(Math.max(0, contentWidth - stripAnsi(currentLine).length)) + t.dim('│')));
787
+ }
788
+ }
789
+
790
+ // Truncation indicator
791
+ if (isTruncated) {
792
+ const moreText = t.yellow(`+ ${lines.length - maxLines} more lines (scroll to see all)`);
793
+ console.log(indent(t.dim('│ ') + moreText + ' '.repeat(Math.max(0, contentWidth - stripAnsi(moreText).length)) + t.dim('│')));
794
+ }
795
+
796
+ // Bottom border
797
+ console.log(indent(t.dim('└' + '─'.repeat(boxWidth)) + t.dim('┘')));
798
+ console.log();
799
+ }
800
+
801
+ // ===== Message Display =====
802
+ function showUserMessage(text) {
803
+ const header = t.blueBg(' U ') + ' ' + chalk.white.bold('You') + ' ' + t.dim(timestamp());
804
+ console.log(indent(header));
805
+ console.log(indent(t.text(text), 6));
806
+ console.log();
807
+ }
808
+
809
+ function showAssistantHeader() {
810
+ const header = t.brand('✦') + ' ' + t.brandBold('Apex AI') + ' ' + t.dim(timestamp());
811
+ console.log(indent(header));
812
+ }
813
+
814
+ function showAssistantMessage(text) {
815
+ const lines = text.split('\n');
816
+ const formatted = [];
817
+ let inCodeBlock = false;
818
+ let codeLines = [];
819
+ let codeLang = '';
820
+
821
+ for (const line of lines) {
822
+ if (line.startsWith('```') && !inCodeBlock) {
823
+ inCodeBlock = true;
824
+ codeLang = line.slice(3).trim() || 'code';
825
+ codeLines = [];
826
+ } else if (line.startsWith('```') && inCodeBlock) {
827
+ inCodeBlock = false;
828
+ formatted.push(renderCodeBlock(codeLines.join('\n'), codeLang));
829
+ } else if (inCodeBlock) {
830
+ codeLines.push(line);
831
+ } else {
832
+ const processed = line.replace(/`([^`]+)`/g, (_, code) => t.cyan(code));
833
+ formatted.push(t.text(processed));
834
+ }
835
+ }
836
+
837
+ console.log(indent(formatted.join('\n'), 4));
838
+ console.log();
839
+ }
840
+
841
+ // ===== Code Block =====
842
+ function renderCodeBlock(code, lang) {
843
+ const width = Math.min(COLS - 8, 80);
844
+ const topBar = t.dim('┌─') + t.accent(` ${lang} `) + t.dim('─'.repeat(Math.max(0, width - lang.length - 4)) + '┐');
845
+ const bottomBar = t.dim('└' + '─'.repeat(width) + '┘');
846
+
847
+ let highlighted;
848
+ try {
849
+ highlighted = highlight(code, { language: lang, ignoreIllegals: true });
850
+ } catch {
851
+ highlighted = t.text(code);
852
+ }
853
+
854
+ const cLines = highlighted.split('\n').map((line, i) => {
855
+ const lineNo = t.dim(String(i + 1).padStart(3) + ' │ ');
856
+ return t.dim('│') + lineNo + line;
857
+ });
858
+
859
+ return [topBar, ...cLines, bottomBar].join('\n');
860
+ }
861
+
862
+ // ===== Tool Badges =====
863
+ function toolBadge(name) {
864
+ const badges = {
865
+ 'Read': t.blueBg(` ${name} `),
866
+ 'Edit': t.yellowBg(` ${name} `),
867
+ 'Write': t.yellowBg(` ${name} `),
868
+ 'Patch': t.orangeBg(` ${name} `),
869
+ 'Bash': t.greenBg(` ${name} `),
870
+ 'Grep': t.purpleBg(` ${name} `),
871
+ 'Glob': t.purpleBg(` ${name} `),
872
+ 'ListDir': chalk.bgHex('#06b6d4').hex('#000')(` ${name} `),
873
+ 'UndoEdit': t.redBg(` ${name} `),
874
+ 'Task': t.orangeBg(` ${name} `),
875
+ 'CodeReview': chalk.bgHex('#76b900').hex('#000')(` ${name} `),
876
+ };
877
+ return badges[name] || chalk.bgHex('#6366f1').hex('#fff')(` ${name} `);
878
+ }
879
+
880
+ function toolDetail(name, args) {
881
+ switch (name) {
882
+ case 'Bash': return args.command || '';
883
+ case 'Grep': return `"${args.pattern}"${args.path ? ` in ${args.path}` : ''}`;
884
+ case 'Glob': return args.pattern || '';
885
+ case 'ListDir': return args.path || '.';
886
+ case 'Read': {
887
+ let d = args.path || '';
888
+ if (args.start_line) d += `:${args.start_line}-${args.end_line || ''}`;
889
+ return d;
890
+ }
891
+ case 'Write': return args.path || '';
892
+ case 'Edit': return args.path || '';
893
+ case 'Patch': return `${args.path} (${(args.edits || []).length} edits)`;
894
+ case 'UndoEdit': return args.path || '';
895
+ case 'Task': return args.description || '';
896
+ case 'CodeReview': {
897
+ const extra = (args.files || []).length;
898
+ const modCount = session.filesModified.size;
899
+ return `${modCount} modified${extra ? ` + ${extra} extra` : ''} — ${(args.prompt || '').slice(0, 50)}`;
900
+ }
901
+ default: return JSON.stringify(args).slice(0, 80);
902
+ }
903
+ }
904
+
905
+ // ===== Tool Call Display =====
906
+ async function showToolCall(name, detail, startTime) {
907
+ const spinner = ora({
908
+ text: `${toolBadge(name)} ${t.muted(detail.length > 80 ? detail.slice(0, 77) + '...' : detail)}`,
909
+ prefixText: ' ',
910
+ spinner: 'dots',
911
+ color: 'magenta',
912
+ }).start();
913
+
914
+ return spinner;
915
+ }
916
+
917
+ function finishToolCall(spinner, name, detail, startTime, success = true) {
918
+ const elapsed = Date.now() - startTime;
919
+ const icon = success ? t.green(figures.tick) : t.red(figures.cross);
920
+ const timeStr = elapsed < 1000 ? `${elapsed}ms` : `${(elapsed / 1000).toFixed(1)}s`;
921
+
922
+ spinner.stopAndPersist({
923
+ symbol: icon,
924
+ text: `${toolBadge(name)} ${t.dim(detail.length > 80 ? detail.slice(0, 77) + '...' : detail)} ${t.dim(timeStr)}`,
925
+ prefixText: ' ',
926
+ });
927
+ }
928
+
929
+ // ===== Diff Display =====
930
+ function showDiff(filename, diffText) {
931
+ const width = Math.min(COLS - 8, 80);
932
+ console.log(indent(t.dim('┌─ ') + t.text.bold(path.basename(filename)) + t.dim(' ' + '─'.repeat(Math.max(0, width - path.basename(filename).length - 4)))));
933
+
934
+ const lines = diffText.split('\n');
935
+ for (const line of lines) {
936
+ if (line.startsWith('+')) {
937
+ console.log(indent(chalk.green('│ ' + line)));
938
+ } else if (line.startsWith('-')) {
939
+ console.log(indent(chalk.red('│ ' + line)));
940
+ }
941
+ }
942
+
943
+ console.log(indent(t.dim('└' + '─'.repeat(width))));
944
+ console.log();
945
+ }
946
+
947
+ // ===== Cost Summary =====
948
+ function showTurnSummary(tokens, duration, toolCalls) {
949
+ const costPerToken = 0.000002;
950
+ const cost = tokens * costPerToken;
951
+ session.totalCost += cost;
952
+
953
+ const items = [
954
+ t.dim('Tokens ') + t.text(tokens.toLocaleString()),
955
+ t.dim('Cost ') + t.text('$' + cost.toFixed(4)),
956
+ t.dim('Duration ') + t.text(duration + 's'),
957
+ t.dim('Tools ') + t.text(toolCalls.toString()),
958
+ ];
959
+
960
+ if (session.filesModified.size > 0) {
961
+ items.push(t.dim('Modified ') + t.text([...session.filesModified].map(f => path.basename(f)).join(', ')));
962
+ }
963
+
964
+ const box = boxen(items.join('\n'), {
965
+ padding: { left: 1, right: 1, top: 0, bottom: 0 },
966
+ borderColor: '#2a2a3a',
967
+ borderStyle: 'round',
968
+ dimBorder: true,
969
+ });
970
+ console.log(indent(box, 4));
971
+ console.log();
972
+ }
973
+
974
+ // ===== Interactive Prompt =====
975
+ function createPrompt() {
976
+ const rl = readline.createInterface({
977
+ input: process.stdin,
978
+ output: process.stdout,
979
+ prompt: '',
980
+ });
981
+ return rl;
982
+ }
983
+
984
+ function showPrompt() {
985
+ console.log();
986
+ console.log(' ' + t.dim('Enter to send · /help for commands'));
987
+ return ' ' + t.brand('❯ ');
988
+ }
989
+
990
+ // ===== State =====
991
+ let isProcessing = false;
992
+ let rlClosed = false;
993
+ let rl;
994
+ let askQuestion;
995
+
996
+ // ===== AI Conversation — Agentic Loop =====
997
+ async function handleUserInput(userInput) {
998
+ isProcessing = true;
999
+ session.turnCount++;
1000
+ showUserMessage(userInput);
1001
+
1002
+ session.conversationHistory.push({ role: 'user', content: userInput });
1003
+
1004
+ const startTime = Date.now();
1005
+ let turnTokens = 0;
1006
+ let turnToolCalls = 0;
1007
+
1008
+ try {
1009
+ showAssistantHeader();
1010
+ console.log();
1011
+
1012
+ const systemPrompt = buildSystemPrompt();
1013
+ let messages = [
1014
+ { role: 'system', content: systemPrompt },
1015
+ ...session.conversationHistory,
1016
+ ];
1017
+
1018
+ let iterations = 0;
1019
+
1020
+ // Agentic tool-use loop
1021
+ while (iterations < MAX_TOOL_ITERATIONS) {
1022
+ iterations++;
1023
+
1024
+ // Show thinking spinner while waiting for first token
1025
+ const thinkSpinner = ora({
1026
+ text: t.dim(iterations === 1 ? 'Reasoning deeply...' : 'Thinking further...'),
1027
+ prefixText: ' ',
1028
+ spinner: 'dots',
1029
+ color: 'magenta',
1030
+ }).start();
1031
+
1032
+ let stream;
1033
+ try {
1034
+ stream = await nvidiaClient.chat.completions.create({
1035
+ model: NVIDIA_MODEL,
1036
+ messages,
1037
+ max_tokens: 4096,
1038
+ temperature: 0.6,
1039
+ top_p: 0.95,
1040
+ tools: toolDefs,
1041
+ tool_choice: 'auto',
1042
+ stream: true,
1043
+ });
1044
+ } catch (apiErr) {
1045
+ thinkSpinner.stop();
1046
+ throw apiErr;
1047
+ }
1048
+
1049
+ // Accumulate streamed response
1050
+ let fullContent = '';
1051
+ const toolCallDeltas = {}; // index -> { id, name, arguments }
1052
+ let finishReason = null;
1053
+ let streamUsage = null;
1054
+ let reasoningText = ''; // collected reasoning from reasoning_content field
1055
+
1056
+ // Display state:
1057
+ // 'buffering' — accumulating, watching for <think> or safe-to-stream point
1058
+ // 'thinking' — inside <think>...</think>, accumulating reasoning
1059
+ // 'streaming' — think block done, streaming content live to terminal
1060
+ let displayState = 'buffering';
1061
+ let contentAccum = ''; // buffer for buffering/thinking states
1062
+ let thinkAccum = ''; // accumulates text inside <think>...</think>
1063
+ let streamingStarted = false;
1064
+ let thinkSpinnerStopped = false;
1065
+
1066
+ function stopThinkSpinner() {
1067
+ if (!thinkSpinnerStopped) { thinkSpinner.stop(); thinkSpinnerStopped = true; }
1068
+ }
1069
+
1070
+ function writeToTerminal(text) {
1071
+ if (!text) return;
1072
+ if (!streamingStarted) {
1073
+ streamingStarted = true;
1074
+ process.stdout.write(' ');
1075
+ }
1076
+ process.stdout.write(t.text(text));
1077
+ }
1078
+
1079
+ for await (const chunk of stream) {
1080
+ if (chunk.usage) streamUsage = chunk.usage;
1081
+
1082
+ const delta = chunk.choices?.[0]?.delta;
1083
+ if (!delta) {
1084
+ if (chunk.choices?.[0]?.finish_reason) finishReason = chunk.choices[0].finish_reason;
1085
+ continue;
1086
+ }
1087
+ if (chunk.choices[0].finish_reason) finishReason = chunk.choices[0].finish_reason;
1088
+
1089
+ // Accumulate tool call deltas
1090
+ if (delta.tool_calls) {
1091
+ for (const tc of delta.tool_calls) {
1092
+ const idx = tc.index;
1093
+ if (!toolCallDeltas[idx]) {
1094
+ toolCallDeltas[idx] = { id: tc.id || '', name: tc.function?.name || '', arguments: '' };
1095
+ }
1096
+ if (tc.id) toolCallDeltas[idx].id = tc.id;
1097
+ if (tc.function?.name) toolCallDeltas[idx].name = tc.function.name;
1098
+ if (tc.function?.arguments) toolCallDeltas[idx].arguments += tc.function.arguments;
1099
+ }
1100
+ }
1101
+
1102
+ // Handle reasoning_content field (some APIs send reasoning separately)
1103
+ if (delta.reasoning_content) {
1104
+ reasoningText += delta.reasoning_content;
1105
+ }
1106
+
1107
+ // Handle content tokens
1108
+ if (delta.content) {
1109
+ fullContent += delta.content;
1110
+ const hasTool = Object.keys(toolCallDeltas).length > 0;
1111
+
1112
+ if (displayState === 'streaming') {
1113
+ // Live-stream, but watch for a new <think> block
1114
+ contentAccum += delta.content;
1115
+ // Strip any stray closing tags
1116
+ contentAccum = stripStrayCloseTag(contentAccum);
1117
+ // Find and handle any <think> in the accumulated buffer
1118
+ const openIdx = contentAccum.indexOf('<think>');
1119
+ if (openIdx !== -1) {
1120
+ // Write everything before <think>
1121
+ if (openIdx > 0) writeToTerminal(contentAccum.slice(0, openIdx));
1122
+ thinkAccum = contentAccum.slice(openIdx + 7);
1123
+ contentAccum = '';
1124
+ displayState = 'thinking';
1125
+ // Check if closing tag already in this buffer
1126
+ const closeMatch = findThinkClose(thinkAccum);
1127
+ if (closeMatch) {
1128
+ const thought = thinkAccum.slice(0, closeMatch.pos).trim();
1129
+ const after = thinkAccum.slice(closeMatch.pos + closeMatch.len);
1130
+ thinkAccum = '';
1131
+ if (thought) showThinkingBlock([thought]);
1132
+ displayState = 'streaming';
1133
+ contentAccum = after;
1134
+ if (!hasTool) writeToTerminal(after);
1135
+ contentAccum = '';
1136
+ }
1137
+ } else {
1138
+ // No <think> — stream it, but hold back possible partial tag at end
1139
+ const { safe, pending } = splitAtPartialTag(contentAccum);
1140
+ contentAccum = pending;
1141
+ if (!hasTool && safe) writeToTerminal(safe);
1142
+ }
1143
+
1144
+ } else if (displayState === 'thinking') {
1145
+ // Accumulate inside think block
1146
+ thinkAccum += delta.content;
1147
+ const closeMatch = findThinkClose(thinkAccum);
1148
+ if (closeMatch) {
1149
+ const thought = thinkAccum.slice(0, closeMatch.pos).trim();
1150
+ const after = thinkAccum.slice(closeMatch.pos + closeMatch.len);
1151
+ thinkAccum = '';
1152
+ stopThinkSpinner();
1153
+ if (thought) showThinkingBlock([thought]);
1154
+ displayState = 'streaming';
1155
+ contentAccum = after;
1156
+ if (!hasTool && after) writeToTerminal(after);
1157
+ contentAccum = '';
1158
+ }
1159
+
1160
+ } else {
1161
+ // 'buffering' — accumulate until we know what's coming
1162
+ contentAccum += delta.content;
1163
+ // Strip stray closing tags
1164
+ contentAccum = stripStrayCloseTag(contentAccum);
1165
+ const openIdx = contentAccum.indexOf('<think>');
1166
+ if (openIdx !== -1) {
1167
+ // Text before <think>: stream it if non-empty
1168
+ const before = contentAccum.slice(0, openIdx);
1169
+ thinkAccum = contentAccum.slice(openIdx + 7);
1170
+ contentAccum = '';
1171
+ if (!hasTool && before.trim()) writeToTerminal(before);
1172
+ displayState = 'thinking';
1173
+ // Check if closing tag already present
1174
+ const closeMatch = findThinkClose(thinkAccum);
1175
+ if (closeMatch) {
1176
+ const thought = thinkAccum.slice(0, closeMatch.pos).trim();
1177
+ const after = thinkAccum.slice(closeMatch.pos + closeMatch.len);
1178
+ thinkAccum = '';
1179
+ stopThinkSpinner();
1180
+ if (thought) showThinkingBlock([thought]);
1181
+ displayState = 'streaming';
1182
+ contentAccum = after;
1183
+ if (!hasTool && after) writeToTerminal(after);
1184
+ contentAccum = '';
1185
+ }
1186
+ } else if (!contentAccum.includes('<') && contentAccum.length > 20) {
1187
+ // No think tag coming — start streaming immediately
1188
+ stopThinkSpinner();
1189
+ displayState = 'streaming';
1190
+ if (!hasTool) writeToTerminal(contentAccum);
1191
+ contentAccum = '';
1192
+ }
1193
+ // else keep buffering (might be start of <think>)
1194
+ }
1195
+ }
1196
+ }
1197
+
1198
+ // Stream ended — flush any remaining buffers
1199
+ stopThinkSpinner();
1200
+
1201
+ if (displayState === 'thinking') {
1202
+ // Unclosed <think> block — show what we accumulated
1203
+ const thought = (thinkAccum + contentAccum).trim();
1204
+ if (thought) showThinkingBlock([thought]);
1205
+ thinkAccum = '';
1206
+ contentAccum = '';
1207
+ displayState = 'streaming';
1208
+ } else if (displayState === 'buffering') {
1209
+ // Never left buffering — flush as plain content
1210
+ const hasTool = Object.keys(toolCallDeltas).length > 0;
1211
+ if (!hasTool && contentAccum.trim()) writeToTerminal(contentAccum);
1212
+ contentAccum = '';
1213
+ } else if (contentAccum) {
1214
+ // Flush any pending partial-tag buffer
1215
+ const hasTool = Object.keys(toolCallDeltas).length > 0;
1216
+ if (!hasTool) writeToTerminal(contentAccum);
1217
+ contentAccum = '';
1218
+ }
1219
+
1220
+ // Show reasoning from reasoning_content field if not already shown via tags
1221
+ if (reasoningText.trim() && displayState !== 'thinking') {
1222
+ showThinkingBlock([reasoningText.trim()]);
1223
+ }
1224
+
1225
+ if (streamingStarted) {
1226
+ process.stdout.write('\n\n');
1227
+ }
1228
+
1229
+ // Final parsed content for history/display (tags stripped)
1230
+ const { content: displayContent } = parseThinkBlocks(fullContent);
1231
+
1232
+ turnTokens += streamUsage?.total_tokens || 0;
1233
+
1234
+ // Reconstruct the full message object
1235
+ const toolCalls = Object.keys(toolCallDeltas).sort((a, b) => a - b).map(idx => ({
1236
+ id: toolCallDeltas[idx].id,
1237
+ type: 'function',
1238
+ function: { name: toolCallDeltas[idx].name, arguments: toolCallDeltas[idx].arguments },
1239
+ }));
1240
+
1241
+ const msg = {
1242
+ role: 'assistant',
1243
+ content: fullContent || null,
1244
+ ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
1245
+ };
1246
+
1247
+ // If the model wants to call tools
1248
+ if (toolCalls.length > 0) {
1249
+ messages.push(msg);
1250
+
1251
+ // Show intermediate reasoning that wasn't streamed (e.g. with tool calls)
1252
+ // (thinking block already shown above, midContent has tags stripped)
1253
+ if (displayContent && displayContent.trim()) {
1254
+ console.log(indent(t.dim.italic(displayContent.trim()), 6));
1255
+ console.log();
1256
+ }
1257
+
1258
+ // Execute tool calls (parallel for independent calls)
1259
+ const toolPromises = toolCalls.map(async (toolCall) => {
1260
+ const toolName = toolCall.function.name;
1261
+ let toolArgs;
1262
+ try {
1263
+ toolArgs = JSON.parse(toolCall.function.arguments);
1264
+ } catch {
1265
+ toolArgs = {};
1266
+ }
1267
+
1268
+ const detail = toolDetail(toolName, toolArgs);
1269
+ const callStart = Date.now();
1270
+ const spinner = await showToolCall(toolName, detail, callStart);
1271
+
1272
+ // Execute
1273
+ const result = await executeTool(toolName, toolArgs);
1274
+ const success = !result.startsWith('Error');
1275
+
1276
+ finishToolCall(spinner, toolName, detail, callStart, success);
1277
+ session.toolCallCount++;
1278
+ turnToolCalls++;
1279
+
1280
+ // Show result preview
1281
+ const resultLines = result.split('\n');
1282
+ let preview;
1283
+ if (toolName === 'Edit' || toolName === 'Patch') {
1284
+ // Show diff
1285
+ showDiff(toolArgs.path, result);
1286
+ } else if (resultLines.length > 5) {
1287
+ preview = resultLines.slice(0, 4).join('\n') + `\n... (${resultLines.length} lines)`;
1288
+ console.log(indent(t.dim(preview), 8));
1289
+ console.log();
1290
+ } else if (result.trim() && result !== '(no output)') {
1291
+ console.log(indent(t.dim(result.length > 300 ? result.slice(0, 297) + '...' : result), 8));
1292
+ console.log();
1293
+ }
1294
+
1295
+ return { id: toolCall.id, result };
1296
+ });
1297
+
1298
+ const toolResults = await Promise.all(toolPromises);
1299
+
1300
+ for (const { id, result } of toolResults) {
1301
+ messages.push({
1302
+ role: 'tool',
1303
+ tool_call_id: id,
1304
+ content: result,
1305
+ });
1306
+ }
1307
+
1308
+ // Check for stop condition
1309
+ if (finishReason === 'stop') break;
1310
+ continue;
1311
+ }
1312
+
1313
+ // No tool calls — final text response (already displayed above)
1314
+ if (fullContent) {
1315
+ const cleanedContent = displayContent.trim() || fullContent.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
1316
+ session.conversationHistory.push({ role: 'assistant', content: cleanedContent || fullContent });
1317
+ }
1318
+ break;
1319
+ }
1320
+
1321
+ if (iterations >= MAX_TOOL_ITERATIONS) {
1322
+ console.log(indent(t.yellow(`⚠ Reached maximum tool iterations (${MAX_TOOL_ITERATIONS}). Stopping.`)));
1323
+ console.log();
1324
+ }
1325
+
1326
+ const duration = ((Date.now() - startTime) / 1000).toFixed(1);
1327
+ session.totalTokens += turnTokens;
1328
+ showTurnSummary(turnTokens, duration, turnToolCalls);
1329
+ } catch (err) {
1330
+ console.log();
1331
+ console.log(indent(t.red('✗ Error: ') + t.text(err.message)));
1332
+ if (!process.env.NVIDIA_API_KEY) {
1333
+ console.log(indent(t.yellow('Set the NVIDIA_API_KEY environment variable with your API key from build.nvidia.com')));
1334
+ }
1335
+ console.log();
1336
+ }
1337
+
1338
+ console.log(indent(hr()));
1339
+ isProcessing = false;
1340
+ if (rlClosed) {
1341
+ // Readline closed during processing — re-create it if stdin is still usable
1342
+ if (!process.stdin.destroyed && process.stdin.readable) {
1343
+ setupInputLoop();
1344
+ askQuestion();
1345
+ return;
1346
+ }
1347
+ process.exit(0);
1348
+ }
1349
+ }
1350
+
1351
+ // ===== Utilities =====
1352
+ function timestamp() {
1353
+ return new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
1354
+ }
1355
+
1356
+ function sleep(ms) {
1357
+ return new Promise(r => setTimeout(r, ms));
1358
+ }
1359
+
1360
+ // ===== Input Loop =====
1361
+ function setupInputLoop() {
1362
+ rlClosed = false;
1363
+ rl = createPrompt();
1364
+
1365
+ rl.on('close', () => {
1366
+ rlClosed = true;
1367
+ if (isProcessing) return;
1368
+ // If stdin is still usable, re-create readline (e.g. terminal glitch)
1369
+ if (!process.stdin.destroyed && process.stdin.readable) {
1370
+ setupInputLoop();
1371
+ askQuestion();
1372
+ return;
1373
+ }
1374
+ console.log();
1375
+ showSessionSummary();
1376
+ console.log(indent(t.dim('Goodbye! ') + t.brand('✦')));
1377
+ console.log();
1378
+ process.exit(0);
1379
+ });
1380
+ }
1381
+
1382
+ // ===== Main Loop =====
1383
+ async function main() {
1384
+ showHeader();
1385
+ showWelcome();
1386
+
1387
+ setupInputLoop();
1388
+
1389
+ askQuestion = () => {
1390
+ if (rlClosed) return;
1391
+ process.stdin.resume();
1392
+ const promptStr = showPrompt();
1393
+
1394
+ rl.question(promptStr, async (answer) => {
1395
+ if (answer === null || answer === undefined) {
1396
+ process.exit(0);
1397
+ }
1398
+ const input = answer.trim();
1399
+
1400
+ if (!input) { askQuestion(); return; }
1401
+
1402
+ // Slash commands
1403
+ if (input.startsWith('/')) {
1404
+ await handleSlashCommand(input, rl);
1405
+ askQuestion();
1406
+ return;
1407
+ }
1408
+
1409
+ if (input === 'exit' || input === 'quit') {
1410
+ console.log();
1411
+ showSessionSummary();
1412
+ console.log(indent(t.dim('Goodbye! ') + t.brand('✦')));
1413
+ console.log();
1414
+ process.exit(0);
1415
+ }
1416
+
1417
+ try {
1418
+ await handleUserInput(input);
1419
+ } catch (err) {
1420
+ console.log(indent(t.red('✗ Unexpected error: ') + t.text(err.message)));
1421
+ console.log();
1422
+ }
1423
+ askQuestion();
1424
+ });
1425
+ };
1426
+
1427
+ askQuestion();
1428
+ }
1429
+
1430
+ // ===== Slash Commands =====
1431
+ async function handleSlashCommand(input, rl) {
1432
+ const [cmd, ...rest] = input.split(' ');
1433
+ const arg = rest.join(' ');
1434
+
1435
+ switch (cmd) {
1436
+ case '/help':
1437
+ showHelpMenu();
1438
+ break;
1439
+
1440
+ case '/clear':
1441
+ session.conversationHistory = [];
1442
+ showHeader();
1443
+ console.log(indent(t.green('✓ ') + t.muted('Conversation cleared.')));
1444
+ console.log();
1445
+ break;
1446
+
1447
+ case '/files':
1448
+ case '/ls': {
1449
+ const dirPath = arg ? resolvePath(arg) : PROJECT_ROOT;
1450
+ console.log();
1451
+ console.log(indent(t.text.bold('Project Files')));
1452
+ console.log();
1453
+ const result = await executeTool('ListDir', { path: dirPath, recursive: true });
1454
+ console.log(indent(t.dim(result), 4));
1455
+ console.log();
1456
+ break;
1457
+ }
1458
+
1459
+ case '/cost':
1460
+ case '/status': {
1461
+ const elapsed = ((Date.now() - session.startTime) / 1000 / 60).toFixed(1);
1462
+ console.log();
1463
+ console.log(indent(` ${t.dim('Session')} ${t.text(elapsed + ' min')} ${t.dim('Turns')} ${t.text(String(session.turnCount))} ${t.dim('Tools')} ${t.text(String(session.toolCallCount))} ${t.dim('Tokens')} ${t.text(session.totalTokens.toLocaleString())} ${t.dim('Cost')} ${t.text('$' + session.totalCost.toFixed(4))}`));
1464
+ console.log();
1465
+ break;
1466
+ }
1467
+
1468
+ case '/undo': {
1469
+ if (session.editHistory.length === 0) {
1470
+ console.log(indent(t.yellow('No edits to undo.')));
1471
+ } else {
1472
+ const last = session.editHistory[session.editHistory.length - 1];
1473
+ fs.writeFileSync(last.path, last.before, 'utf-8');
1474
+ session.editHistory.pop();
1475
+ console.log(indent(t.green('✓ ') + t.muted(`Undone last edit to ${path.basename(last.path)}`)));
1476
+ }
1477
+ console.log();
1478
+ break;
1479
+ }
1480
+
1481
+ case '/diff': {
1482
+ try {
1483
+ const diff = execSync('git diff --stat 2>/dev/null', { encoding: 'utf-8', cwd: PROJECT_ROOT });
1484
+ console.log();
1485
+ console.log(indent(t.text.bold('Git Diff')));
1486
+ console.log(indent(t.dim(diff || '(no changes)'), 4));
1487
+ console.log();
1488
+ } catch {
1489
+ console.log(indent(t.yellow('Not a git repository.')));
1490
+ console.log();
1491
+ }
1492
+ break;
1493
+ }
1494
+
1495
+ case '/git': {
1496
+ if (!arg) {
1497
+ console.log(indent(t.yellow('Usage: /git <command>')));
1498
+ console.log();
1499
+ break;
1500
+ }
1501
+ try {
1502
+ const output = execSync(`git ${arg}`, { encoding: 'utf-8', cwd: PROJECT_ROOT });
1503
+ console.log();
1504
+ console.log(indent(t.dim(output), 4));
1505
+ console.log();
1506
+ } catch (err) {
1507
+ console.log(indent(t.red(err.stderr || err.message)));
1508
+ console.log();
1509
+ }
1510
+ break;
1511
+ }
1512
+
1513
+ case '/quit':
1514
+ console.log();
1515
+ showSessionSummary();
1516
+ console.log(indent(t.dim('Goodbye! ') + t.brand('✦')));
1517
+ console.log();
1518
+ process.exit(0);
1519
+
1520
+ default:
1521
+ console.log(indent(t.yellow(`Unknown command: ${cmd}. Type /help for available commands.`)));
1522
+ console.log();
1523
+ }
1524
+ }
1525
+
1526
+ // ===== Session Summary =====
1527
+ function showSessionSummary() {
1528
+ const elapsed = ((Date.now() - session.startTime) / 1000 / 60).toFixed(1);
1529
+ const items = [
1530
+ t.dim('Session ') + t.text(`${elapsed} min`),
1531
+ t.dim('Turns ') + t.text(session.turnCount.toString()),
1532
+ t.dim('Tool calls ') + t.text(session.toolCallCount.toString()),
1533
+ t.dim('Tokens ') + t.text(session.totalTokens.toLocaleString()),
1534
+ t.dim('Cost ') + t.text('$' + session.totalCost.toFixed(4)),
1535
+ ];
1536
+
1537
+ if (session.filesModified.size > 0) {
1538
+ items.push(t.dim('Modified ') + t.text(`${session.filesModified.size} files`));
1539
+ }
1540
+ if (session.commandsRun.length > 0) {
1541
+ items.push(t.dim('Commands ') + t.text(`${session.commandsRun.length} executed`));
1542
+ }
1543
+
1544
+ console.log();
1545
+ const box = boxen(items.join('\n'), {
1546
+ title: 'Session Summary',
1547
+ titleAlignment: 'center',
1548
+ padding: { left: 1, right: 1, top: 0, bottom: 0 },
1549
+ borderColor: '#818cf8',
1550
+ borderStyle: 'round',
1551
+ });
1552
+ console.log(indent(box, 2));
1553
+ console.log();
1554
+ }
1555
+
1556
+ // ===== Help =====
1557
+ function showHelpMenu() {
1558
+ console.log();
1559
+ console.log(indent(t.text.bold('Slash Commands')));
1560
+ console.log();
1561
+ console.log(indent(` ${t.brand('/help')} ${t.muted('Show this menu')}`));
1562
+ console.log(indent(` ${t.brand('/files')} ${t.dim('[path]')} ${t.muted('Show project file tree')}`));
1563
+ console.log(indent(` ${t.brand('/clear')} ${t.muted('Clear conversation history')}`));
1564
+ console.log(indent(` ${t.brand('/cost')} ${t.muted('Show session statistics')}`));
1565
+ console.log(indent(` ${t.brand('/undo')} ${t.muted('Undo last file edit')}`));
1566
+ console.log(indent(` ${t.brand('/diff')} ${t.muted('Show git diff summary')}`));
1567
+ console.log(indent(` ${t.brand('/git')} ${t.dim('<cmd>')} ${t.muted('Run a git command')}`));
1568
+ console.log(indent(` ${t.brand('/quit')} ${t.muted('Exit with session summary')}`));
1569
+ console.log();
1570
+ console.log(indent(t.text.bold('Available Tools')));
1571
+ console.log();
1572
+ console.log(indent(` ${toolBadge('Read')} ${t.muted('Read file contents')}`));
1573
+ console.log(indent(` ${toolBadge('Write')} ${t.muted('Create or overwrite files')}`));
1574
+ console.log(indent(` ${toolBadge('Edit')} ${t.muted('Find-and-replace in files')}`));
1575
+ console.log(indent(` ${toolBadge('Patch')} ${t.muted('Multiple edits to one file')}`));
1576
+ console.log(indent(` ${toolBadge('Bash')} ${t.muted('Execute shell commands')}`));
1577
+ console.log(indent(` ${toolBadge('Grep')} ${t.muted('Search code with regex')}`));
1578
+ console.log(indent(` ${toolBadge('Glob')} ${t.muted('Find files by pattern')}`));
1579
+ console.log(indent(` ${toolBadge('ListDir')} ${t.muted('List directory contents')}`));
1580
+ console.log(indent(` ${toolBadge('UndoEdit')}${t.muted(' Revert last file edit')}`));
1581
+ console.log(indent(` ${toolBadge('Task')} ${t.muted('Run multi-step sub-tasks')}`));
1582
+ console.log(indent(` ${toolBadge('CodeReview')}${t.muted(' AI-powered code review sub-agent')}`));
1583
+ console.log();
1584
+ }
1585
+
1586
+ // ===== Run =====
1587
+ main().catch(console.error);