@krishivpb60/aether-ai-cli 1.3.3 → 1.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/chat.js CHANGED
@@ -1,1617 +1,2184 @@
1
- // ═══════════════════════════════════════════════════════════
2
- // AETHER AI CLI — Interactive Chat Loop
3
- // Universal AI Gateway & Cyberpunk Command Center
4
- // ═══════════════════════════════════════════════════════════
5
-
6
- import { createInterface } from "node:readline";
7
- import { writeFile } from "node:fs/promises";
8
- import { readdirSync, existsSync, statSync } from "node:fs";
9
- import { resolve, join, sep } from "node:path";
10
- import { exec } from "node:child_process";
11
- import chalk from "chalk";
12
- import { Marked } from "marked";
13
- import { markedTerminal } from "marked-terminal";
14
-
15
- import {
16
- colors,
17
- label,
18
- separator,
19
- keyValue,
20
- bullet,
21
- modeBadge,
22
- clearStreamedText,
23
- StreamFilter,
24
- stripCodeFences,
25
- getActiveTheme,
26
- setTheme,
27
- getThemesList,
28
- interactiveMenu
29
- } from "./ui/theme.js";
30
- import { createSpinner } from "./ui/spinner.js";
31
- import { showBanner } from "./ui/banner.js";
32
- import { routePrompt } from "./ai/router.js";
33
- import { getActiveProviders } from "./ai/providers.js";
34
- import {
35
- getAIConfig,
36
- loadHistory,
37
- saveHistory,
38
- clearHistory,
39
- setConfigValue,
40
- listSessions,
41
- switchSession,
42
- startNewSession
43
- } from "./config.js";
44
- import { MODES, DEFAULT_MODE, getModeByName } from "./modes.js";
45
- import { parseFile, formatContext } from "./file-parser.js";
46
- import { runMainframeHack } from "./ai/fallback.js";
47
- import { AGENT_INSTRUCTIONS } from "./agent.js";
48
- import { checkForUpdates } from "./updater.js";
49
- import { getSessionTokenStats, getBreakdownByModel, resetSessionTokenStats } from "./ai/tokens.js";
50
- import { getGitDiff } from "./git.js";
51
-
52
-
53
-
54
- // Configure marked dynamically for terminal output
55
- const getMarked = () => new Marked(markedTerminal({
56
- reflowText: true,
57
- width: process.stdout.columns ? Math.max(20, process.stdout.columns - 4) : 80,
58
- showSectionPrefix: false,
59
- code: (c) => colors.orange(c),
60
- codespan: (c) => colors.accent3(c),
61
- heading: (h) => colors.accent.bold(h),
62
- strong: (s) => colors.magenta.bold(s),
63
- em: chalk.italic,
64
- hr: (h) => colors.dim(h),
65
- }));
66
-
67
- /**
68
- * Starts the interactive Aether chat session.
69
- * @param {{ mode?: string, preferredProvider?: string }} [options={}]
70
- */
71
- export async function startChat(options = {}) {
72
- // Load AI config
73
- const aiConfig = await getAIConfig();
74
-
75
- // Run update check
76
- await checkForUpdates();
77
-
78
- // Reset token stats for the new session
79
- resetSessionTokenStats();
80
-
81
-
82
- // Set theme from configuration
83
- const theme = aiConfig.THEME || "cyberpunk";
84
- setTheme(theme);
85
-
86
- let currentMode = getModeByName(options.mode) || getModeByName(aiConfig.DEFAULT_MODE) || MODES[DEFAULT_MODE];
87
- let attachedFiles = [];
88
-
89
- // Persistent history loader
90
- const history = await loadHistory();
91
-
92
- // Mini-game state
93
- const game = {
94
- active: false,
95
- code: "",
96
- attempts: 0,
97
- maxAttempts: 6,
98
- };
99
-
100
- // Show banner
101
- showBanner(currentMode.name);
102
-
103
- // Active providers diagnostic check
104
- const active = getActiveProviders(aiConfig);
105
- if (active.length === 0) {
106
- console.log(
107
- "\n" + label.system + " " +
108
- colors.warning("No API keys configured. Using local fallback solvers.") + "\n" +
109
- " " + colors.muted("Run ") + colors.accent("aether setup") +
110
- colors.muted(" to configure providers (free options available!).\n")
111
- );
112
- } else {
113
- const providerNames = active.map((a) => a.provider.name);
114
- const unique = [...new Set(providerNames)];
115
- console.log(
116
- label.mesh + " " +
117
- colors.accent("Failover mesh online: ") +
118
- colors.text(unique.join(" → ")) +
119
- colors.muted(" → Krylo fallback")
120
- );
121
- console.log(
122
- " " + colors.dim(`${active.length} node(s) active across ${unique.length} provider(s)`) + "\n"
123
- );
124
- }
125
-
126
- // Display loaded history message if any
127
- if (history.length > 0) {
128
- console.log(
129
- " " + label.info + " " +
130
- colors.muted(`Restored ${Math.floor(history.length / 2)} message exchanges from persistent logs.`) + "\n"
131
- );
132
- }
133
-
134
- // Completer: handles commands & dynamic local file path autocomplete
135
- const completer = (line) => {
136
- const builtIn = [
137
- "/help", "/mode", "/modes", "/attach", "/files", "/clear",
138
- "/providers", "/export", "/status", "/copy", "/exit", "/quit",
139
- "/theme", "/themes", "/history-clear", "/game", "/abort", "/cmd", "/write",
140
- "/commit", "/run", "/history", "/autopilot", "/tokens", "/update",
141
- "/review", "/diagnose", "/explain", "/refactor", "/bug", "/doc", "/translate"
142
- ];
143
- const customCmds = aiConfig.CUSTOM_COMMANDS || {};
144
- const commands = [...builtIn, ...Object.keys(customCmds)];
145
-
146
- // File path autocompletion on /attach
147
- if (line.startsWith("/attach ")) {
148
- const query = line.slice(8);
149
- const lastSlash = Math.max(query.lastIndexOf("/"), query.lastIndexOf("\\"));
150
- let searchDir = ".";
151
- let searchPrefix = query;
152
-
153
- if (lastSlash !== -1) {
154
- searchDir = query.slice(0, lastSlash);
155
- if (searchDir === "") {
156
- searchDir = sep;
157
- }
158
- searchPrefix = query.slice(lastSlash + 1);
159
- }
160
-
161
- try {
162
- const resolved = resolve(searchDir);
163
- if (existsSync(resolved) && statSync(resolved).isDirectory()) {
164
- const files = readdirSync(resolved);
165
- const hits = files
166
- .filter((f) => f.toLowerCase().startsWith(searchPrefix.toLowerCase()) && !f.startsWith("."))
167
- .map((f) => {
168
- const fullPath = searchDir === "." || searchDir === sep ? f : join(searchDir, f);
169
- const fullResolved = resolve(fullPath);
170
- const isDir = statSync(fullResolved).isDirectory();
171
- return `/attach ${fullPath}${isDir ? "/" : ""}`;
172
- });
173
- return [hits.length ? hits : [], line];
174
- }
175
- } catch (e) {
176
- // Fallback silently on fs errors
177
- }
178
- return [[], line];
179
- }
180
-
181
- // Sub-arguments autocomplete on /mode
182
- if (line.startsWith("/mode ")) {
183
- const query = line.slice(6).toLowerCase();
184
- const modesList = Object.keys(MODES);
185
- const hits = modesList
186
- .filter((m) => m.startsWith(query))
187
- .map((m) => `/mode ${m}`);
188
- return [hits.length ? hits : [], line];
189
- }
190
-
191
- // Sub-arguments autocomplete on /theme
192
- if (line.startsWith("/theme ")) {
193
- const query = line.slice(7).toLowerCase();
194
- const themesList = getThemesList();
195
- const hits = themesList
196
- .filter((t) => t.startsWith(query))
197
- .map((t) => `/theme ${t}`);
198
- return [hits.length ? hits : [], line];
199
- }
200
-
201
- // Sub-arguments autocomplete on /cmd
202
- if (line.startsWith("/cmd ")) {
203
- const query = line.slice(5).toLowerCase();
204
- const subcmds = ["list", "add", "remove"];
205
- const hits = subcmds
206
- .filter((s) => s.startsWith(query))
207
- .map((s) => `/cmd ${s}`);
208
- return [hits.length ? hits : [], line];
209
- }
210
-
211
- const hits = commands.filter((c) => c.startsWith(line));
212
- return [hits.length ? hits : [], line];
213
- };
214
-
215
- // Create readline interface
216
- const rl = createInterface({
217
- input: process.stdin,
218
- output: process.stdout,
219
- prompt: colors.accent(" ❯ "),
220
- terminal: true,
221
- completer
222
- });
223
-
224
- // Load persistent history entries directly into the shell up/down array
225
- if (history.length > 0) {
226
- const userQueries = history
227
- .filter((h) => h.role === "user")
228
- .map((h) => h.content);
229
- // Readline history is structured newest first (index 0)
230
- rl.history = [...new Set(userQueries)].reverse();
231
- }
232
-
233
- // ── AI Execution Helper ──────────────────────────────────
234
- async function executeAIQuery(promptText, originalInput = promptText) {
235
- // ── Build Prompt with Context ─────────────────────────
236
- let fullPrompt = promptText;
237
- if (attachedFiles.length > 0) {
238
- const contexts = attachedFiles.map((f) => formatContext(f)).join("\n\n");
239
- fullPrompt = `${contexts}\n\n${promptText}`;
240
- }
241
-
242
- // Append AGENT_INSTRUCTIONS to mode systemPrompt
243
- const systemPrompt = currentMode.systemPrompt + "\n" + AGENT_INSTRUCTIONS;
244
-
245
- let loopCount = 0;
246
- const MAX_LOOPS = 5;
247
- let currentQueryPrompt = fullPrompt;
248
- let aiResponseText = "";
249
- let lastResult = null;
250
-
251
- try {
252
- while (loopCount < MAX_LOOPS) {
253
- const queryStartTime = Date.now();
254
- let firstTokenTime = 0;
255
-
256
- if (loopCount > 0) {
257
- console.log(colors.accent(`\n🤖 [Aether Autopilot Mode - Iteration ${loopCount + 1}/${MAX_LOOPS}]`));
258
- }
259
-
260
- const spinner = createSpinner(
261
- colors.muted(loopCount === 0 ? `Routing through mesh ${currentMode.label}...` : `Agent executing tasks...`)
262
- );
263
- spinner.start();
264
-
265
- let hasStartedStreaming = false;
266
- let streamedText = "";
267
- const filter = new StreamFilter(process.stdout.write.bind(process.stdout));
268
- const onToken = (token) => {
269
- if (!hasStartedStreaming) {
270
- hasStartedStreaming = true;
271
- firstTokenTime = Date.now();
272
- spinner.stop();
273
- }
274
- filter.write(token);
275
- streamedText += token;
276
- };
277
-
278
- const result = await routePrompt(currentQueryPrompt, systemPrompt, aiConfig, onToken, history);
279
- spinner.stop();
280
- filter.flush();
281
-
282
- aiResponseText = result.text;
283
- lastResult = result;
284
-
285
- if (hasStartedStreaming) {
286
- clearStreamedText(filter.filteredText);
287
- }
288
-
289
- // Display response
290
- console.log("");
291
- console.log(label.aether + " " + providerBadge(result));
292
- console.log(separator(""));
293
- console.log("");
294
-
295
- if (result.provider === "local" || result.provider === "krylo-fallback") {
296
- console.log(colors.text(" " + result.text.split("\n").join("\n ")));
297
- } else {
298
- let displayText = result.text;
299
- const rendered = getMarked().parse(displayText);
300
- console.log(rendered);
301
- }
302
-
303
- const elapsedSec = ((Date.now() - queryStartTime) / 1000).toFixed(1);
304
- let speedText = "";
305
- if (firstTokenTime > 0) {
306
- const streamElapsed = (Date.now() - firstTokenTime) / 1000;
307
- if (streamElapsed > 0.05) {
308
- const estimatedTokens = Math.max(1, Math.round(streamedText.length / 4));
309
- const tps = (estimatedTokens / streamElapsed).toFixed(1);
310
- speedText = ` ${tps} tok/s`;
311
- }
312
- }
313
-
314
- const showTokens = aiConfig.SHOW_TOKENS !== "false";
315
- let tokensText = "";
316
- if (showTokens && result.usage) {
317
- const { promptTokens, completionTokens } = result.usage;
318
- tokensText = ` ${promptTokens.toLocaleString()} in / ${completionTokens.toLocaleString()} out tokens`;
319
- }
320
-
321
- console.log(separator("─"));
322
- console.log(
323
- " " + colors.dim(`Node ${result.node} • ${result.provider}`) +
324
- (result.model ? colors.dim(` • ${result.model}`) : "") +
325
- colors.dim(` • ${elapsedSec}s${speedText}`) +
326
- colors.dim(tokensText) +
327
- colors.dim(` • ${Math.floor(history.length / 2)} exchanges`)
328
- );
329
- console.log("");
330
-
331
- // Process any agent tools output by the AI
332
- const { processAgentBlocks } = await import("./agent.js");
333
- const toolResults = await processAgentBlocks(aiResponseText, aiConfig, rl);
334
-
335
- if (toolResults.length === 0) {
336
- // No tools executed, end loop
337
- break;
338
- }
339
-
340
- // Store this turn in history so AI knows what happened
341
- history.push({ role: "user", content: currentQueryPrompt, timestamp: new Date() });
342
- history.push({
343
- role: "assistant",
344
- content: aiResponseText,
345
- provider: result.provider,
346
- model: result.model,
347
- node: result.node,
348
- timestamp: new Date(),
349
- });
350
- await saveHistory(history, currentMode.name);
351
-
352
- // Format tool outputs as next prompt
353
- let formattedResults = "### Agent Tool Outputs:\n";
354
- for (const tr of toolResults) {
355
- if (tr.success) {
356
- if (tr.tool === "RUN_COMMAND") {
357
- formattedResults += `\n- RUN_COMMAND "${tr.arg}" succeeded. Output:\n\`\`\`\n${tr.stdout || ""}${tr.stderr || ""}\n\`\`\`;`;
358
- } else if (tr.tool === "READ_FILE") {
359
- formattedResults += `\n- READ_FILE "${tr.arg}" succeeded. File Content:\n\`\`\`\n${tr.content}\n\`\`\`;`;
360
- } else if (tr.tool === "WRITE_FILE") {
361
- formattedResults += `\n- WRITE_FILE "${tr.arg}" succeeded.`;
362
- } else if (tr.tool === "SEARCH_WEB") {
363
- const resultsList = tr.results.map((r, i) => `${i+1}. [${r.title}](${r.url})\n ${r.snippet}`).join("\n");
364
- formattedResults += `\n- SEARCH_WEB "${tr.arg}" succeeded. Results:\n${resultsList}`;
365
- }
366
- } else {
367
- formattedResults += `\n- ${tr.tool} "${tr.arg}" failed: ${tr.error}`;
368
- }
369
- }
370
- formattedResults += "\n\nPlease continue and finalize your task or perform next steps.";
371
-
372
- currentQueryPrompt = formattedResults;
373
- loopCount++;
374
- }
375
-
376
- // Store final state in history
377
- if (loopCount > 0) {
378
- // Just save to disk to persist
379
- await saveHistory(history, currentMode.name);
380
- } else {
381
- // Standard single-turn save
382
- history.push({ role: "user", content: originalInput, timestamp: new Date() });
383
- history.push({
384
- role: "assistant",
385
- content: aiResponseText,
386
- provider: lastResult.provider,
387
- model: lastResult.model,
388
- node: lastResult.node,
389
- timestamp: new Date(),
390
- });
391
- await saveHistory(history, currentMode.name);
392
- }
393
-
394
- } catch (err) {
395
- console.log("\n" + label.error + " " + colors.danger(err.message) + "\n");
396
- }
397
-
398
- // Sync shell's recall history list
399
- const userQueries = history
400
- .filter((h) => h.role === "user")
401
- .map((h) => h.content);
402
- rl.history = [...new Set(userQueries)].reverse();
403
- }
404
-
405
- rl.prompt();
406
-
407
- rl.on("line", async (line) => {
408
- const input = line.trim();
409
- if (!input) {
410
- rl.prompt();
411
- return;
412
- }
413
-
414
- // ── Handle Game Input ──────────────────────────────────
415
- if (game.active && !input.startsWith("/")) {
416
- handleGuess(input, game);
417
- rl.prompt();
418
- return;
419
- }
420
-
421
- // ── Handle Slash Commands ──────────────────────────────
422
- if (input.startsWith("/")) {
423
- const [cmd, ...args] = input.split(/\s+/);
424
- const builtInList = [
425
- "/", "/help", "/mode", "/modes", "/attach", "/files", "/clear",
426
- "/providers", "/export", "/status", "/copy", "/exit", "/quit",
427
- "/theme", "/themes", "/history-clear", "/game", "/abort", "/cmd",
428
- "/guess", "/write", "/commit", "/run", "/history", "/autopilot", "/tokens",
429
- "/update", "/review", "/diagnose", "/explain", "/refactor", "/bug", "/doc",
430
- "/translate"
431
- ];
432
-
433
- const customCmds = aiConfig.CUSTOM_COMMANDS || {};
434
-
435
- if (!builtInList.includes(cmd.toLowerCase()) && customCmds[cmd]) {
436
- const template = customCmds[cmd];
437
- const userArg = args.join(" ");
438
- const rewrittenPrompt = template + (userArg ? " " + userArg : "");
439
-
440
- console.log("\n" + label.system + " " + colors.accent(`Executing custom command: `) + colors.text(cmd));
441
- console.log(" " + colors.muted("Prompt: ") + colors.text(rewrittenPrompt) + "\n");
442
-
443
- await executeAIQuery(rewrittenPrompt, input);
444
- rl.prompt();
445
- return;
446
- }
447
-
448
- const handled = await handleCommand(input, {
449
- currentMode,
450
- attachedFiles,
451
- history,
452
- aiConfig,
453
- game,
454
- setMode: (mode) => { currentMode = mode; },
455
- addFile: (file) => { attachedFiles.push(file); },
456
- clearFiles: () => { attachedFiles = []; },
457
- rl,
458
- });
459
- if (handled !== "exit") {
460
- rl.prompt();
461
- }
462
- return;
463
- }
464
-
465
- await executeAIQuery(input);
466
- rl.prompt();
467
- });
468
-
469
- rl.on("close", () => {
470
- console.log("\n" + label.system + " " + colors.muted("Session terminated. Stay cyberpunk. ⚡\n"));
471
- process.exit(0);
472
- });
473
- }
474
-
475
- /**
476
- * Handles slash commands in the chat.
477
- */
478
- async function handleCommand(input, ctx) {
479
- const [cmd, ...args] = input.split(/\s+/);
480
-
481
- switch (cmd.toLowerCase()) {
482
- case "/":
483
- case "/help":
484
- showHelp(ctx.aiConfig);
485
- break;
486
-
487
- case "/mode":
488
- handleModeSwitch(args, ctx);
489
- break;
490
-
491
- case "/modes":
492
- showModes();
493
- break;
494
-
495
- case "/attach":
496
- await handleAttach(args, ctx);
497
- break;
498
-
499
- case "/files":
500
- showAttachedFiles(ctx.attachedFiles);
501
- break;
502
-
503
- case "/clear":
504
- // Actual screen clear & scrollback reset
505
- process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
506
- showBanner(ctx.currentMode.name);
507
- break;
508
-
509
- case "/export":
510
- await handleExport(ctx.history);
511
- break;
512
-
513
- case "/status":
514
- showStatus(ctx);
515
- break;
516
-
517
- case "/providers":
518
- showActiveProviders(ctx.aiConfig);
519
- break;
520
-
521
- case "/update":
522
- console.log("\n" + label.system + " " + colors.muted("Checking registry for updates..."));
523
- await checkForUpdates(true);
524
- console.log("");
525
- break;
526
-
527
- case "/review":
528
- await handleReviewCommand(ctx);
529
- break;
530
-
531
- case "/diagnose":
532
- await handleDiagnoseCommand(args, ctx);
533
- break;
534
-
535
- case "/explain":
536
- case "/refactor":
537
- case "/bug":
538
- case "/doc":
539
- case "/translate":
540
- await handleFileAICommand(cmd, args, ctx);
541
- break;
542
-
543
- case "/theme":
544
- await handleThemeSwitch(args);
545
- break;
546
-
547
- case "/themes":
548
- showThemesList();
549
- break;
550
-
551
- case "/history-clear":
552
- await handleHistoryClear(ctx.history, ctx.rl);
553
- break;
554
-
555
- case "/game":
556
- handleGameStart(ctx.game);
557
- break;
558
-
559
- case "/abort":
560
- handleGameAbort(ctx.game);
561
- break;
562
-
563
- case "/guess":
564
- if (ctx.game.active) {
565
- handleGuess(args[0] || "", ctx.game);
566
- } else {
567
- console.log("\n" + label.system + " " + colors.warning("Game is not active. Type /game to start.\n"));
568
- }
569
- break;
570
-
571
- case "/copy":
572
- await handleCopy(ctx.history);
573
- break;
574
-
575
- case "/cmd":
576
- await handleCustomCommands(args, ctx);
577
- break;
578
-
579
- case "/write":
580
- await handleWriteFile(args, ctx);
581
- break;
582
-
583
- case "/commit":
584
- await handleCommitInsideChat(ctx);
585
- break;
586
-
587
- case "/run":
588
- await handleRunCommand(args, ctx);
589
- break;
590
-
591
- case "/history":
592
- await handleHistorySwitch(ctx);
593
- break;
594
-
595
- case "/autopilot":
596
- await handleAutopilotSwitch(args, ctx);
597
- break;
598
-
599
- case "/tokens":
600
- await handleTokensDisplay(ctx);
601
- break;
602
-
603
- case "/exit":
604
- case "/quit":
605
- ctx.rl.close();
606
- return "exit";
607
-
608
- default:
609
- console.log("\n" + label.system + " " + colors.warning(`Unknown command: ${cmd}. Type /help for available commands.\n`));
610
- }
611
- }
612
-
613
- // ── Command Handlers ────────────────────────────────────────
614
-
615
- function showHelp(aiConfig) {
616
- console.log("");
617
- console.log(colors.brand(" ⚡ AETHER CLI COMMANDS"));
618
- console.log(separator("─"));
619
- console.log("");
620
- console.log(keyValue("/", "Show this help menu"));
621
- console.log(keyValue("/help", "Show this help menu"));
622
- console.log(keyValue("/mode <name>", "Switch mode (" + Object.keys(MODES).join(", ") + ")"));
623
- console.log(keyValue("/modes", "List all modes with signal metrics"));
624
- console.log(keyValue("/theme <name>", "Switch visual theme (cyberpunk, matrix, synthwave, crimson)"));
625
- console.log(keyValue("/themes", "List available visual themes"));
626
- console.log(keyValue("/attach <path>", "Attach a file for context (supports Tab path autocomplete!)"));
627
- console.log(keyValue("/files", "List attached files"));
628
- console.log(keyValue("/clear", "Clear terminal screen and reprint banner"));
629
- console.log(keyValue("/providers", "Show active AI providers"));
630
- console.log(keyValue("/export", "Export conversation to file"));
631
- console.log(keyValue("/history", "List, switch, and resume past interactive chat sessions"));
632
- console.log(keyValue("/history-clear", "Clear saved persistent chat history"));
633
- console.log(keyValue("/autopilot <mode>", "View or switch agent autopilot level (off, safe, workspace, machine)"));
634
- console.log(keyValue("/tokens", "View detailed session token usage and exchanges telemetry"));
635
- console.log(keyValue("/update", "Force check for updates and update Aether CLI manually"));
636
- console.log(keyValue("/game", "Start the local mainframe hacking mini-game"));
637
- console.log(keyValue("/copy", "Copy the last assistant response to clipboard"));
638
- console.log(keyValue("/cmd <list|add|remove>", "Manage custom command shortcuts"));
639
- console.log(keyValue("/write <filename>", "Extract last code block and save to file"));
640
- console.log(keyValue("/commit", "Generate conventional commit message and commit changes"));
641
- console.log(keyValue("/run <command>", "Execute a shell command interactively"));
642
- console.log(keyValue("/review", "Run git diff and stream an AI code review"));
643
- console.log(keyValue("/diagnose [cmd]", "Run build/tests and AI-debug any errors"));
644
- console.log(keyValue("/explain <file>", "AI-explain the design and logic of a file"));
645
- console.log(keyValue("/refactor <file>", "AI-refactor the code of a target file"));
646
- console.log(keyValue("/bug <file>", "AI-audit a file to find potential logic bugs"));
647
- console.log(keyValue("/doc <file>", "AI-generate documentation/docstrings for a file"));
648
- console.log(keyValue("/translate <file> <lang>", "AI-translate code of a file to another language"));
649
- console.log(keyValue("/exit", "End session"));
650
-
651
- if (aiConfig && aiConfig.CUSTOM_COMMANDS) {
652
- const custom = aiConfig.CUSTOM_COMMANDS;
653
- const entries = Object.entries(custom);
654
- if (entries.length > 0) {
655
- console.log("");
656
- console.log(colors.brand(" CUSTOM SHORTCUTS"));
657
- console.log(separator(""));
658
- for (const [cmd, template] of entries) {
659
- console.log(keyValue(cmd, `Shortcut for: "${template}"`));
660
- }
661
- }
662
- }
663
- console.log("");
664
- }
665
-
666
- function handleModeSwitch(args, ctx) {
667
- const modeName = args[0];
668
- if (!modeName) {
669
- console.log("\n" + label.mode + " " + colors.warning("Usage: /mode <" + Object.keys(MODES).join("|") + ">\n"));
670
- return;
671
- }
672
-
673
- const newMode = getModeByName(modeName);
674
- if (!newMode) {
675
- console.log("\n" + label.mode + " " + colors.danger(`Unknown mode: "${modeName}".`) + " " + colors.muted("Available: " + Object.keys(MODES).join(", ") + "\n"));
676
- return;
677
- }
678
-
679
- ctx.setMode(newMode);
680
- console.log("\n" + label.mode + " " + colors.accent("Switched to ") + modeBadge(newMode.name));
681
- console.log(" " + colors.muted(newMode.description) + "\n");
682
-
683
- const sig = newMode.signal;
684
- console.log(" " + signalBar("Reasoning", sig.reasoning));
685
- console.log(" " + signalBar("Clarity", sig.clarity));
686
- console.log(" " + signalBar("System IQ", sig.systemIQ));
687
- console.log(" " + signalBar("Delivery", sig.delivery));
688
- console.log("");
689
- }
690
-
691
- function showModes() {
692
- console.log("");
693
- console.log(colors.brand(" ◈ AVAILABLE REASONING MODES"));
694
- console.log(separator("─"));
695
- console.log("");
696
-
697
- for (const mode of Object.values(MODES)) {
698
- console.log(" " + modeBadge(mode.name) + " " + colors.muted(`(${mode.layer})`));
699
- console.log(" " + colors.text(mode.description));
700
- const sig = mode.signal;
701
- console.log(" " + signalBar("RSN", sig.reasoning) + " " + signalBar("CLR", sig.clarity) + " " + signalBar("SIQ", sig.systemIQ) + " " + signalBar("DLV", sig.delivery));
702
- console.log("");
703
- }
704
- }
705
-
706
- async function handleAttach(args, ctx) {
707
- const filePath = args.join(" ").trim();
708
- if (!filePath) {
709
- const { scanWorkspaceFiles } = await import("./file-parser.js");
710
- const { interactiveCheckbox } = await import("./ui/theme.js");
711
-
712
- const workspaceFiles = scanWorkspaceFiles(process.cwd());
713
- if (workspaceFiles.length === 0) {
714
- console.log("\n" + label.file + " " + colors.muted("No supported files found in this workspace.\n"));
715
- return;
716
- }
717
-
718
- ctx.rl.pause();
719
- const selected = await interactiveCheckbox(
720
- "Attach files (Arrow Keys to navigate, Space to toggle, Enter to confirm, Esc/q to cancel):\n",
721
- workspaceFiles,
722
- ctx.attachedFiles.map(f => f.relativePath || f.name)
723
- );
724
- ctx.rl.resume();
725
-
726
- if (selected === null) {
727
- console.log("\n" + label.file + " " + colors.muted("Selection canceled.\n"));
728
- return;
729
- }
730
-
731
- ctx.clearFiles();
732
- if (selected.length === 0) {
733
- console.log("\n" + label.file + " " + colors.success("Cleared all attachments.\n"));
734
- return;
735
- }
736
-
737
- let successCount = 0;
738
- for (const file of selected) {
739
- try {
740
- const fileData = await parseFile(file);
741
- fileData.relativePath = file;
742
- ctx.addFile(fileData);
743
- successCount++;
744
- } catch (err) {
745
- console.log(label.error + " " + colors.danger(`Failed to attach ${file}: ${err.message}`));
746
- }
747
- }
748
- console.log("\n" + label.file + " " + colors.success(`Successfully attached ${successCount} file(s).\n`));
749
- return;
750
- }
751
-
752
- try {
753
- const fileData = await parseFile(filePath);
754
- ctx.addFile(fileData);
755
- console.log("\n" + label.file + " " + colors.success(`Attached: ${fileData.name}`));
756
- console.log(" " + colors.muted(`${formatBytes(fileData.size)} ${fileData.extension} • ${ctx.attachedFiles.length} file(s) loaded\n`));
757
- } catch (err) {
758
- console.log("\n" + label.error + " " + colors.danger(err.message) + "\n");
759
- }
760
- }
761
-
762
- function showAttachedFiles(files) {
763
- if (files.length === 0) {
764
- console.log("\n" + label.file + " " + colors.muted("No files attached. Use /attach <path> to add context.\n"));
765
- return;
766
- }
767
-
768
- console.log("");
769
- console.log(label.file + " " + colors.accent(`${files.length} file(s) attached:`));
770
- for (const f of files) {
771
- console.log(bullet(`${f.name} (${formatBytes(f.size)}, ${f.extension})`));
772
- }
773
- console.log("");
774
- }
775
-
776
- async function handleExport(history) {
777
- if (history.length === 0) {
778
- console.log("\n" + label.system + " " + colors.muted("No conversation to export.\n"));
779
- return;
780
- }
781
-
782
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
783
- const filename = `aether-chat-${timestamp}.md`;
784
- const filepath = resolve(filename);
785
-
786
- let content = `# Aether AI Chat Export\n*Exported at ${new Date().toLocaleString()}*\n\n---\n\n`;
787
-
788
- for (const entry of history) {
789
- if (entry.role === "user") {
790
- content += `## 👤 You\n${entry.content}\n\n`;
791
- } else {
792
- content += `## 🤖 Aether (${entry.provider || "unknown"})\n${entry.content}\n\n---\n\n`;
793
- }
794
- }
795
-
796
- try {
797
- await writeFile(filepath, content, "utf-8");
798
- console.log("\n" + label.system + " " + colors.success(`Exported to: ${filepath}\n`));
799
- } catch (err) {
800
- console.log("\n" + label.error + " " + colors.danger(`Export failed: ${err.message}\n`));
801
- }
802
- }
803
-
804
- function showStatus(ctx) {
805
- const active = getActiveProviders(ctx.aiConfig);
806
-
807
- console.log("");
808
- console.log(colors.brand(" ◈ SESSION STATUS"));
809
- console.log(separator("─"));
810
- console.log(keyValue(" Theme", getActiveTheme().toUpperCase()));
811
- console.log(keyValue(" Mode", ctx.currentMode.label));
812
- console.log(keyValue(" Layer", ctx.currentMode.layer));
813
- console.log(keyValue(" Exchanges", String(Math.floor(ctx.history.length / 2))));
814
- console.log(keyValue(" Files", String(ctx.attachedFiles.length)));
815
- console.log(keyValue(" Providers", String(active.length)));
816
- console.log("");
817
- }
818
-
819
- function showActiveProviders(aiConfig) {
820
- const active = getActiveProviders(aiConfig);
821
-
822
- console.log("");
823
- console.log(colors.brand(" ◈ ACTIVE PROVIDERS"));
824
- console.log(separator(""));
825
-
826
- if (active.length === 0) {
827
- console.log(" " + colors.warning("No providers. Run `aether setup` to configure.") + "\n");
828
- return;
829
- }
830
-
831
- for (const { provider } of active) {
832
- console.log(" " + colors.success("✓ ") + colors.text(provider.name) + colors.dim(` • ${provider.defaultModel}`));
833
- }
834
- console.log(" " + colors.success("✓ ") + colors.text("Krylo Companion") + colors.dim(" • Local fallback"));
835
- console.log(" " + colors.success("✓ ") + colors.text("Math Solver") + colors.dim(" • Local"));
836
- console.log("");
837
- }
838
-
839
- async function handleThemeSwitch(args) {
840
- const themeName = args[0];
841
- if (!themeName) {
842
- console.log("\n" + label.system + " " + colors.warning("Usage: /theme <theme-name>. Type /themes to list themes.\n"));
843
- return;
844
- }
845
-
846
- const success = setTheme(themeName);
847
- if (success) {
848
- await setConfigValue("THEME", themeName.toLowerCase().trim());
849
- console.log("\n" + label.system + " " + colors.success(`✓ Theme switched to ${themeName.toUpperCase()}`));
850
- console.log(" " + colors.muted("Visual grid modulates synchronized.\n"));
851
- } else {
852
- console.log("\n" + label.system + " " + colors.danger(`Unknown theme: "${themeName}".`) + " " + colors.muted(`Available: ${getThemesList().join(", ")}\n`));
853
- }
854
- }
855
-
856
- function showThemesList() {
857
- console.log("");
858
- console.log(colors.brand(" AVAILABLE COLOR THEMES"));
859
- console.log(separator("─"));
860
- const active = getActiveTheme();
861
- for (const t of getThemesList()) {
862
- const activeText = t === active ? colors.success("★ ACTIVE") : "";
863
- console.log(bullet(t.toUpperCase().padEnd(14) + activeText));
864
- }
865
- console.log("");
866
- }
867
-
868
- async function handleHistoryClear(history, rl) {
869
- await clearHistory();
870
- history.length = 0;
871
- if (rl) rl.history = [];
872
- console.log("\n" + label.system + " " + colors.success("✓ Persistent chat history and prompt history cleared.\n"));
873
- }
874
-
875
- async function handleAutopilotSwitch(args, ctx) {
876
- const setting = args[0]?.toLowerCase().trim();
877
- if (!setting) {
878
- const current = (ctx.aiConfig.AUTOPILOT || "off").toUpperCase();
879
- console.log("\n" + label.system + " " + colors.brand("🤖 AUTOPILOT AGENT CONFIGURATION"));
880
- console.log(separator("─"));
881
- console.log(keyValue(" Current Setting", current));
882
- console.log("");
883
- console.log(" " + colors.muted("Available Modes:"));
884
- console.log(" • " + colors.accent("off") + colors.text(" - Always ask user for confirmation before executing any actions."));
885
- console.log(" • " + colors.accent("safe") + colors.text(" - Run read-only/safe terminal commands and searches automatically."));
886
- console.log(" • " + colors.accent("workspace") + colors.text(" - Run any actions automatically if they stay inside the workspace."));
887
- console.log(" • " + colors.accent("machine") + colors.text(" - Complete autopilot. Run any action automatically (Full access)."));
888
- console.log("");
889
- console.log(" " + colors.muted("To change setting: ") + colors.accent("/autopilot <mode>") + "\n");
890
- return;
891
- }
892
-
893
- const valid = ["off", "safe", "workspace", "machine"];
894
- if (!valid.includes(setting)) {
895
- console.log("\n" + label.system + " " + colors.danger(`ERROR: Unknown autopilot mode "${setting}".`) + " " + colors.muted("Choose from: off, safe, workspace, machine.\n"));
896
- return;
897
- }
898
-
899
- await setConfigValue("AUTOPILOT", setting);
900
- ctx.aiConfig.AUTOPILOT = setting;
901
- console.log("\n" + label.system + " " + colors.success(`✓ Autopilot setting updated to ${setting.toUpperCase()} successfully.\n`));
902
- }
903
-
904
- async function handleHistorySwitch(ctx) {
905
- const sessions = listSessions();
906
- if (sessions.length === 0) {
907
- console.log("\n" + label.system + " " + colors.muted("No past chat sessions found.\n"));
908
- return;
909
- }
910
-
911
- const items = sessions.map((s) => {
912
- const dateStr = new Date(s.timestamp).toLocaleString();
913
- const count = s.messages.length;
914
- const exchanges = Math.floor(count / 2);
915
- // Find first user query preview
916
- const firstQuery = s.messages.find((m) => m.role === "user")?.content || "Empty conversation";
917
- const preview = firstQuery.length > 50 ? firstQuery.slice(0, 47) + "..." : firstQuery;
918
- const modeBadgeText = `[${s.mode}]`;
919
- return `${colors.dim(dateStr)} ${colors.brand(modeBadgeText.padEnd(12))} ${colors.muted(exchanges + " exch")} • ${colors.text(preview)}`;
920
- });
921
-
922
- // Add an option to start a new session
923
- items.push(colors.accent("➕ Start a new chat session"));
924
-
925
- ctx.rl.pause();
926
- const selectedIndex = await interactiveMenu(
927
- "Select a past chat session to resume (Arrow Keys to navigate, Enter to select, Esc/q to cancel):\n",
928
- items
929
- );
930
- ctx.rl.resume();
931
-
932
- if (selectedIndex === null) {
933
- console.log("\n" + label.system + " " + colors.muted("Selection canceled.\n"));
934
- return;
935
- }
936
-
937
- // Clear screen and load the selected session
938
- process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
939
-
940
- if (selectedIndex === sessions.length) {
941
- // Start new session
942
- const newSessionFile = startNewSession();
943
- ctx.history.length = 0;
944
- showBanner(ctx.currentMode.name);
945
- console.log("\n" + label.system + " " + colors.success("Started a new chat session.\n"));
946
- } else {
947
- const selectedSession = sessions[selectedIndex];
948
- switchSession(selectedSession.file);
949
-
950
- // Load history
951
- const loadedHistory = await loadHistory();
952
- ctx.history.length = 0;
953
- for (const msg of loadedHistory) {
954
- ctx.history.push(msg);
955
- }
956
-
957
- showBanner(ctx.currentMode.name);
958
- console.log("\n" + label.system + " " + colors.success(`✓ Switched to chat session from ${new Date(selectedSession.timestamp).toLocaleString()}`));
959
- console.log(" " + colors.muted(`Restored ${Math.floor(ctx.history.length / 2)} message exchanges.\n`));
960
- }
961
-
962
- // Sync shell's recall history list
963
- const userQueries = ctx.history
964
- .filter((h) => h.role === "user")
965
- .map((h) => h.content);
966
- ctx.rl.history = [...new Set(userQueries)].reverse();
967
- }
968
-
969
- function handleGameStart(game) {
970
- if (game.active) {
971
- console.log("\n" + label.system + " " + colors.warning("Mainframe breach is already in progress. Type /abort to cancel.\n"));
972
- return;
973
- }
974
-
975
- // Set up game
976
- game.active = true;
977
- game.attempts = 0;
978
-
979
- // Generate random 4-digit code
980
- const code = Array.from({ length: 4 }, () => Math.floor(Math.random() * 10)).join("");
981
- game.code = code;
982
-
983
- const rules = runMainframeHack();
984
- console.log("\n" + rules.text + "\n");
985
- }
986
-
987
- function handleGameAbort(game) {
988
- if (!game.active) {
989
- console.log("\n" + label.system + " " + colors.warning("No security breach in progress.\n"));
990
- return;
991
- }
992
- game.active = false;
993
- console.log("\n" + label.system + " " + colors.warning("Breach protocol aborted. Connection terminated.\n"));
994
- }
995
-
996
- function handleGuess(input, game) {
997
- const guess = input.trim();
998
- if (!/^\d{4}$/.test(guess)) {
999
- console.log("\n" + label.error + " " + colors.danger("BREACH ERROR: Code must be exactly 4 digits (0-9).") + "\n");
1000
- return;
1001
- }
1002
-
1003
- game.attempts++;
1004
-
1005
- const codeArr = game.code.split("");
1006
- const guessArr = guess.split("");
1007
-
1008
- let hits = 0;
1009
- let closes = 0;
1010
-
1011
- const codeUsed = [false, false, false, false];
1012
- const guessUsed = [false, false, false, false];
1013
-
1014
- // First pass: Hits
1015
- for (let i = 0; i < 4; i++) {
1016
- if (guessArr[i] === codeArr[i]) {
1017
- hits++;
1018
- codeUsed[i] = true;
1019
- guessUsed[i] = true;
1020
- }
1021
- }
1022
-
1023
- // Second pass: Closes
1024
- for (let i = 0; i < 4; i++) {
1025
- if (guessUsed[i]) continue;
1026
- for (let j = 0; j < 4; j++) {
1027
- if (codeUsed[j]) continue;
1028
- if (guessArr[i] === codeArr[j]) {
1029
- closes++;
1030
- codeUsed[j] = true;
1031
- break;
1032
- }
1033
- }
1034
- }
1035
-
1036
- console.log("");
1037
- console.log(colors.magenta(` [BREACH ATTEMPT #${game.attempts} / ${game.maxAttempts}]`));
1038
- console.log(colors.text(` BREACH INPUT: ${guess.split("").join(" ")}`));
1039
- console.log(colors.success(` HITS (Pos): ${"█ ".repeat(hits)}${"░ ".repeat(4 - hits)} (${hits})`));
1040
- console.log(colors.warning(` CLOSE (Val): ${"█ ".repeat(closes)}${"░ ".repeat(4 - closes)} (${closes})`));
1041
- console.log("");
1042
-
1043
- if (hits === 4) {
1044
- console.log(label.system + " " + colors.success("MAINFRAME BYPASSED! Access granted. Decryption complete. 🔓\n"));
1045
- game.active = false;
1046
- } else if (game.attempts >= game.maxAttempts) {
1047
- console.log(label.error + " " + colors.danger("SECURITY SHUTDOWN! Mainframe locked out. Intrusion logged. 🔒"));
1048
- console.log(" Intrusion PIN was: " + colors.accent(game.code) + "\n");
1049
- game.active = false;
1050
- } else {
1051
- console.log(colors.muted(" Recalibrating security bypass codes...") + "\n");
1052
- }
1053
- }
1054
-
1055
- async function handleCopy(history) {
1056
- const lastResponse = [...history].reverse().find((h) => h.role === "assistant");
1057
- if (!lastResponse) {
1058
- console.log("\n" + label.system + " " + colors.muted("No response to copy yet.\n"));
1059
- return;
1060
- }
1061
-
1062
- try {
1063
- await copyToClipboard(lastResponse.content);
1064
- console.log("\n" + label.system + " " + colors.success(" Last response copied to OS Clipboard successfully!\n"));
1065
- } catch (err) {
1066
- console.log("\n" + label.system + " " + colors.muted("Unable to copy automatically. Displaying content below:"));
1067
- console.log(colors.text(lastResponse.content.slice(0, 800)));
1068
- if (lastResponse.content.length > 800) {
1069
- console.log(colors.dim(" [... truncated, use /export to save full conversation]"));
1070
- }
1071
- console.log("");
1072
- }
1073
- }
1074
-
1075
- function copyToClipboard(text) {
1076
- return new Promise((resolve, reject) => {
1077
- let command;
1078
- if (process.platform === "win32") {
1079
- command = "clip";
1080
- } else if (process.platform === "darwin") {
1081
- command = "pbcopy";
1082
- } else {
1083
- command = "xclip -selection clipboard || xsel -ib";
1084
- }
1085
-
1086
- try {
1087
- const child = exec(command, (err) => {
1088
- if (err) reject(err);
1089
- else resolve();
1090
- });
1091
- child.stdin.write(text);
1092
- child.stdin.end();
1093
- } catch (e) {
1094
- reject(e);
1095
- }
1096
- });
1097
- }
1098
-
1099
- // ── Box / Badges / Theme helpers ─────────────────────────────
1100
-
1101
- function providerBadge(result) {
1102
- const badges = {
1103
- "groq": chalk.bgHex("#1a2a1a").hex("#67ffb0")(" Groq "),
1104
- "together ai": chalk.bgHex("#1a2a1a").hex("#67ffb0")(" Together "),
1105
- "cerebras": chalk.bgHex("#1a2a1a").hex("#67ffb0")(" Cerebras "),
1106
- "openai": chalk.bgHex("#1a2a1a").hex("#67ffb0")(" OpenAI "),
1107
- "google": chalk.bgHex("#1a1a2a").hex("#2d7dff")(" Gemini "),
1108
- "anthropic": chalk.bgHex("#2a1a2a").hex("#b06cff")(" Claude "),
1109
- "xai": chalk.bgHex("#1a2a1a").hex("#67ffb0")(" Grok "),
1110
- "mistral ai": chalk.bgHex("#1a1a2a").hex("#ffb900")(" Mistral "),
1111
- "openrouter": chalk.bgHex("#1a1a2a").hex("#6ce8ff")(" OpenRouter "),
1112
- "cohere": chalk.bgHex("#1a2a2a").hex("#6ce8ff")(" Cohere "),
1113
- "deepseek": chalk.bgHex("#1a1a2a").hex("#2d7dff")(" DeepSeek "),
1114
- "perplexity": chalk.bgHex("#1a2a2a").hex("#6ce8ff")(" Perplexity "),
1115
- "fireworks ai": chalk.bgHex("#2a1a1a").hex("#ff6b8d")(" Fireworks "),
1116
- "local": chalk.bgHex("#1a2a1a").hex("#67ffb0")(" Math Solver "),
1117
- "krylo-fallback": chalk.bgHex("#0c1825").hex("#6ce8ff")(" Krylo "),
1118
- };
1119
-
1120
- const badge = badges[result.provider] || colors.muted(` ${result.provider} `);
1121
- return badge + colors.dim(` Node ${result.node}`);
1122
- }
1123
-
1124
- function signalBar(name, value) {
1125
- const filled = Math.round(value / 10);
1126
- const empty = 10 - filled;
1127
- const bar = colors.accent("".repeat(filled)) + colors.dim("░".repeat(empty));
1128
- return `${colors.muted(name.padEnd(10))} ${bar} ${colors.muted(value + "%")}`;
1129
- }
1130
-
1131
- function formatBytes(bytes) {
1132
- if (bytes < 1024) return `${bytes}B`;
1133
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
1134
- return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
1135
- }
1136
-
1137
- /**
1138
- * Handles the management of custom slash command shortcuts.
1139
- */
1140
- async function handleCustomCommands(args, ctx) {
1141
- const sub = args[0]?.toLowerCase();
1142
-
1143
- if (sub === "list") {
1144
- const custom = ctx.aiConfig.CUSTOM_COMMANDS || {};
1145
- const entries = Object.entries(custom);
1146
-
1147
- console.log("");
1148
- console.log(colors.brand(" ⚡ CUSTOM SHORTCUT COMMANDS"));
1149
- console.log(separator("─"));
1150
-
1151
- if (entries.length === 0) {
1152
- console.log(" " + colors.muted("No custom commands registered."));
1153
- console.log(" " + colors.muted("Create one: ") + colors.accent("/cmd add /explain \"Explain this code:\"") + "\n");
1154
- return;
1155
- }
1156
-
1157
- for (const [cmd, template] of entries) {
1158
- console.log(` ${colors.accent(cmd.padEnd(16))} ${colors.text(template)}`);
1159
- }
1160
- console.log("");
1161
- return;
1162
- }
1163
-
1164
- if (sub === "add") {
1165
- const name = args[1];
1166
- const template = args.slice(2).join(" ");
1167
-
1168
- if (!name || !template) {
1169
- console.log("\n" + label.system + " " + colors.warning("Usage: /cmd add <name> <template>"));
1170
- console.log(" " + colors.muted("Example: /cmd add /explain \"Explain this code in detail:\"") + "\n");
1171
- return;
1172
- }
1173
-
1174
- if (!name.startsWith("/")) {
1175
- console.log("\n" + label.system + " " + colors.danger("ERROR: Command name must start with a slash '/' (e.g. /explain)") + "\n");
1176
- return;
1177
- }
1178
-
1179
- const builtIn = [
1180
- "/help", "/mode", "/modes", "/attach", "/files", "/clear",
1181
- "/providers", "/export", "/status", "/copy", "/exit", "/quit",
1182
- "/theme", "/themes", "/history-clear", "/game", "/abort", "/cmd", "/guess", "/tokens"
1183
- ];
1184
-
1185
- if (builtIn.includes(name.toLowerCase())) {
1186
- console.log("\n" + label.system + " " + colors.danger(`ERROR: Cannot override system command "${name}"`) + "\n");
1187
- return;
1188
- }
1189
-
1190
- const custom = ctx.aiConfig.CUSTOM_COMMANDS || {};
1191
- custom[name] = template;
1192
-
1193
- await setConfigValue("CUSTOM_COMMANDS", custom);
1194
- ctx.aiConfig.CUSTOM_COMMANDS = custom; // sync context
1195
-
1196
- console.log("\n" + label.system + " " + colors.success(`✓ Command registered successfully!`));
1197
- console.log(` ${colors.accent(name)} ➔ "${template}"\n`);
1198
- return;
1199
- }
1200
-
1201
- if (sub === "remove") {
1202
- const name = args[1];
1203
- if (!name) {
1204
- console.log("\n" + label.system + " " + colors.warning("Usage: /cmd remove <name>") + "\n");
1205
- return;
1206
- }
1207
-
1208
- const custom = ctx.aiConfig.CUSTOM_COMMANDS || {};
1209
- if (!custom[name]) {
1210
- console.log("\n" + label.system + " " + colors.warning(`No custom command named "${name}" exists.`) + "\n");
1211
- return;
1212
- }
1213
-
1214
- delete custom[name];
1215
- await setConfigValue("CUSTOM_COMMANDS", custom);
1216
- ctx.aiConfig.CUSTOM_COMMANDS = custom; // sync context
1217
-
1218
- console.log("\n" + label.system + " " + colors.success(`✓ Removed custom command: "${name}"\n`));
1219
- return;
1220
- }
1221
-
1222
- console.log("\n" + label.system + " " + colors.warning("Usage: /cmd <list|add|remove> [args]"));
1223
- console.log(" " + colors.muted("Type /help for help or /cmd list to see existing shortcuts.\n"));
1224
- }
1225
-
1226
- /**
1227
- * Extracts all code blocks from a markdown string.
1228
- */
1229
- function extractCodeBlocks(markdown) {
1230
- const regex = /```[\w-]*\n([\s\S]*?)\n```/g;
1231
- const blocks = [];
1232
- let match;
1233
- while ((match = regex.exec(markdown)) !== null) {
1234
- blocks.push(match[1]);
1235
- }
1236
- return blocks;
1237
- }
1238
-
1239
- /**
1240
- * Manual file writing command. Extracts the last code block of the previous
1241
- * assistant response and writes it to a file.
1242
- */
1243
- async function handleWriteFile(args, ctx) {
1244
- const filename = args.join(" ");
1245
- if (!filename) {
1246
- console.log("\n" + label.system + " " + colors.warning("Usage: /write <filename>") + "\n");
1247
- return;
1248
- }
1249
-
1250
- const lastResponse = [...ctx.history].reverse().find((h) => h.role === "assistant");
1251
- if (!lastResponse) {
1252
- console.log("\n" + label.system + " " + colors.muted("No assistant response available to write.\n"));
1253
- return;
1254
- }
1255
-
1256
- const codeBlocks = extractCodeBlocks(lastResponse.content);
1257
- if (codeBlocks.length === 0) {
1258
- console.log("\n" + label.system + " " + colors.warning("No code blocks found in the last response.\n"));
1259
- return;
1260
- }
1261
-
1262
- const blockContent = codeBlocks[codeBlocks.length - 1];
1263
- const filepath = resolve(filename);
1264
-
1265
- try {
1266
- const { dirname } = await import("node:path");
1267
- const { mkdir } = await import("node:fs/promises");
1268
- const dir = dirname(filepath);
1269
- await mkdir(dir, { recursive: true });
1270
- await writeFile(filepath, blockContent, "utf-8");
1271
- console.log("\n" + label.system + " " + colors.success(`✓ Code block successfully written to: ${filepath}\n`));
1272
- } catch (err) {
1273
- console.log("\n" + label.error + " " + colors.danger(`Write failed: ${err.message}\n`));
1274
- }
1275
- }
1276
-
1277
- /**
1278
- * Interactive git commit command inside chat loop.
1279
- */
1280
- async function handleCommitInsideChat(ctx) {
1281
- const { getGitDiff, runGitCommit } = await import("./git.js");
1282
- const { exec } = await import("node:child_process");
1283
- const { promisify } = await import("node:util");
1284
- const execAsync = promisify(exec);
1285
-
1286
- try {
1287
- const { diff, isStaged } = await getGitDiff();
1288
- if (!diff) {
1289
- console.log("\n" + label.system + " " + colors.warning("No staged or unstaged changes detected. Stage your files using 'git add' first.\n"));
1290
- return;
1291
- }
1292
-
1293
- if (!isStaged) {
1294
- ctx.rl.pause();
1295
- const stageAnswer = await new Promise((resolve) => {
1296
- ctx.rl.question(colors.warning("\nNo staged changes found. Do you want to stage all changes automatically? [y/N]: "), resolve);
1297
- });
1298
- ctx.rl.resume();
1299
-
1300
- if (stageAnswer.toLowerCase().trim() === "y" || stageAnswer.toLowerCase().trim() === "yes") {
1301
- await execAsync("git add .");
1302
- console.log(label.system + " " + colors.success("Staged all changes successfully."));
1303
- } else {
1304
- console.log("\n" + label.system + " " + colors.muted("Aborted. Please stage files using 'git add' first.\n"));
1305
- return;
1306
- }
1307
- }
1308
-
1309
- console.log("\n" + label.system + " " + colors.brand("Reading git diff and generating conventional commit message..."));
1310
- console.log("");
1311
-
1312
- const systemPrompt = "You are an expert developer assistant. Generate a concise, clear, and professional conventional commit message (e.g., 'feat: add login page', 'fix: resolve buffer overflow') based on the provided git diff. Output ONLY the commit message itself on a single line, with absolutely no backticks, markdown, explanations, prefix, or introductory text.";
1313
- const userPrompt = `Here is the git diff:\n\n${diff}`;
1314
-
1315
- let firstToken = true;
1316
- let commitMessage = "";
1317
- const onToken = (token) => {
1318
- if (firstToken) {
1319
- firstToken = false;
1320
- process.stdout.write(label.aether + " Suggested Commit Message: " + colors.success(token));
1321
- } else {
1322
- process.stdout.write(colors.success(token));
1323
- }
1324
- commitMessage += token;
1325
- };
1326
-
1327
- const result = await routePrompt(userPrompt, systemPrompt, ctx.aiConfig, onToken);
1328
- console.log("\n");
1329
-
1330
- const cleanMessage = result.text.trim().replace(/^`+|`+$/g, ""); // strip quotes/backticks
1331
-
1332
- ctx.rl.pause();
1333
- const answer = await new Promise((resolve) => {
1334
- ctx.rl.question(colors.muted("Commit with this message? [Y/n]: "), resolve);
1335
- });
1336
- ctx.rl.resume();
1337
-
1338
- if (answer.toLowerCase().trim() === "n" || answer.toLowerCase().trim() === "no") {
1339
- console.log("\n" + label.system + " " + colors.muted("Commit aborted.\n"));
1340
- return;
1341
- }
1342
-
1343
- console.log("\n" + label.system + " " + colors.brand("Executing git commit..."));
1344
- const output = await runGitCommit(cleanMessage);
1345
- console.log("\n" + colors.success(output) + "\n");
1346
-
1347
- } catch (err) {
1348
- console.log("\n" + label.error + " " + colors.danger(err.message) + "\n");
1349
- }
1350
- }
1351
-
1352
- /**
1353
- * Sandboxed interactive shell command execution.
1354
- */
1355
- async function handleRunCommand(args, ctx) {
1356
- const command = args.join(" ").trim();
1357
- if (!command) {
1358
- console.log("\n" + label.system + " " + colors.warning("Usage: /run <command>\n"));
1359
- return;
1360
- }
1361
-
1362
- const { spawn } = await import("node:child_process");
1363
-
1364
- console.log("\n" + label.system + " " + colors.brand(`Running command: ${command}`));
1365
- console.log(separator("") + "\n");
1366
-
1367
- ctx.rl.pause();
1368
-
1369
- return new Promise((resolve) => {
1370
- const isWindows = process.platform === "win32";
1371
- const shell = isWindows ? "cmd.exe" : "/bin/sh";
1372
- const shellArgs = isWindows ? ["/c", command] : ["-c", command];
1373
-
1374
- const child = spawn(shell, shellArgs, { stdio: "inherit" });
1375
-
1376
- child.on("close", (code) => {
1377
- ctx.rl.resume();
1378
- console.log("\n" + separator(""));
1379
- if (code === 0) {
1380
- console.log(label.system + " " + colors.success(`✓ Command exited successfully (code 0).\n`));
1381
- } else {
1382
- console.log(label.system + " " + colors.danger(`✗ Command failed with exit status ${code}.\n`));
1383
- }
1384
- resolve();
1385
- });
1386
-
1387
- child.on("error", (err) => {
1388
- ctx.rl.resume();
1389
- console.log("\n" + label.error + " " + colors.danger(`Failed to start command: ${err.message}\n`));
1390
- resolve();
1391
- });
1392
- });
1393
- }
1394
-
1395
- /**
1396
- * Interactive display of session token usage statistics.
1397
- */
1398
- async function handleTokensDisplay(ctx) {
1399
- const stats = getSessionTokenStats();
1400
- const breakdown = getBreakdownByModel();
1401
-
1402
- console.log("\n" + separator("━"));
1403
- console.log(colors.accent.bold(" ★ AETHER SESSION TOKEN TELEMETRY ★"));
1404
- console.log(separator("─"));
1405
-
1406
- const models = Object.keys(breakdown);
1407
- if (models.length === 0) {
1408
- console.log(colors.muted(" No queries executed in this session yet."));
1409
- } else {
1410
- // Print header
1411
- console.log(
1412
- colors.brand(" " + "Model".padEnd(35) + "Prompt".padStart(10) + "Completion".padStart(12) + "Total".padStart(10))
1413
- );
1414
- console.log(colors.dim(" " + "─".repeat(67)));
1415
- for (const [model, data] of Object.entries(breakdown)) {
1416
- const truncatedModel = model.length > 33 ? model.slice(0, 30) + "..." : model;
1417
- console.log(
1418
- " " + colors.text(truncatedModel.padEnd(35)) +
1419
- colors.brand(data.prompt.toLocaleString().padStart(10)) +
1420
- colors.brand(data.completion.toLocaleString().padStart(12)) +
1421
- colors.accent.bold(data.total.toLocaleString().padStart(10))
1422
- );
1423
- }
1424
- }
1425
-
1426
- console.log(separator("─"));
1427
- console.log(" " + colors.accent("Total Exchanges:") + colors.text(` ${stats.exchanges}`));
1428
- console.log(" " + colors.accent("Total Tokens:") + colors.text(` Prompt: ${stats.prompt.toLocaleString()} | Completion: ${stats.completion.toLocaleString()} | Sum: `) + colors.brand.bold(stats.total.toLocaleString()));
1429
- console.log(separator("━") + "\n");
1430
- }
1431
-
1432
- /**
1433
- * Streams an AI query prompt and prints telemetry details at the end.
1434
- */
1435
- async function executeAISpecialCommand(prompt, specialLabel, ctx) {
1436
- const systemPrompt = ctx.currentMode.systemPrompt + "\n" + AGENT_INSTRUCTIONS;
1437
- let hasStarted = false;
1438
- let responseText = "";
1439
- const queryStartTime = Date.now();
1440
- let firstTokenTime = 0;
1441
-
1442
- const onToken = (token) => {
1443
- if (!hasStarted) {
1444
- hasStarted = true;
1445
- firstTokenTime = Date.now();
1446
- process.stdout.write("\n" + label.aether + " " + colors.accent(specialLabel) + "\n" + separator("─") + "\n\n");
1447
- }
1448
- process.stdout.write(colors.success(token));
1449
- responseText += token;
1450
- };
1451
-
1452
- const result = await routePrompt(prompt, systemPrompt, ctx.aiConfig, onToken);
1453
- console.log("\n");
1454
-
1455
- const elapsedSec = ((Date.now() - queryStartTime) / 1000).toFixed(1);
1456
- let speedText = "";
1457
- if (firstTokenTime > 0) {
1458
- const streamElapsed = (Date.now() - firstTokenTime) / 1000;
1459
- if (streamElapsed > 0.05) {
1460
- const estimatedTokens = Math.max(1, Math.round(responseText.length / 4));
1461
- const tps = (estimatedTokens / streamElapsed).toFixed(1);
1462
- speedText = ` ${tps} tok/s`;
1463
- }
1464
- }
1465
-
1466
- const showTokens = ctx.aiConfig.SHOW_TOKENS !== "false";
1467
- let tokensText = "";
1468
- if (showTokens && result.usage) {
1469
- const { promptTokens, completionTokens } = result.usage;
1470
- tokensText = ` • ${promptTokens.toLocaleString()} in / ${completionTokens.toLocaleString()} out tokens`;
1471
- }
1472
-
1473
- console.log(separator(""));
1474
- console.log(
1475
- " " + colors.dim(`Node ${result.node} ${result.provider}`) +
1476
- (result.model ? colors.dim(` • ${result.model}`) : "") +
1477
- colors.dim(` ${elapsedSec}s${speedText}`) +
1478
- colors.dim(tokensText)
1479
- );
1480
- console.log("");
1481
- }
1482
-
1483
- /**
1484
- * Handler for the /review command (git diff analysis).
1485
- */
1486
- async function handleReviewCommand(ctx) {
1487
- console.log("\n" + label.system + " " + colors.muted("Running git diff to fetch repository changes..."));
1488
- try {
1489
- const { diff, isStaged } = await getGitDiff();
1490
- if (!diff) {
1491
- console.log(label.system + " " + colors.success("✓ No changes detected in the repository to review.\n"));
1492
- return;
1493
- }
1494
-
1495
- const specialLabel = `Reviewing ${isStaged ? "staged" : "unstaged"} changes...`;
1496
- const prompt = `Review the following git diff. Identify potential bugs, logical issues, security concerns, performance problems, and recommend optimization or code cleanup. Keep it concise, practical, and highly technical:\n\n\`\`\`diff\n${diff}\n\`\`\``;
1497
-
1498
- await executeAISpecialCommand(prompt, specialLabel, ctx);
1499
- } catch (err) {
1500
- console.log(label.system + " " + colors.danger(`Error: ${err.message}\n`));
1501
- }
1502
- }
1503
-
1504
- /**
1505
- * Handler for the /diagnose command (build & test diagnostics execution).
1506
- */
1507
- async function handleDiagnoseCommand(args, ctx) {
1508
- const defaultCmd = ctx.aiConfig.DIAGNOSE_CMD || "npm test";
1509
- const cmdToRun = args.join(" ").trim() || defaultCmd;
1510
-
1511
- console.log("\n" + label.system + " " + colors.muted(`Running diagnostics command: "${cmdToRun}"...`));
1512
-
1513
- const spinner = createSpinner("Executing diagnostics").start();
1514
- try {
1515
- const { exec } = await import("node:child_process");
1516
- const { promisify } = await import("node:util");
1517
- const execAsync = promisify(exec);
1518
- await execAsync(cmdToRun);
1519
- spinner.succeed("Diagnostics complete!");
1520
- console.log("\n" + label.system + " " + colors.success("✓ Diagnostics clean! Build and tests passed successfully.\n"));
1521
- } catch (err) {
1522
- spinner.fail("Diagnostics failed!");
1523
-
1524
- const output = (err.stdout || "") + "\n" + (err.stderr || "");
1525
- console.log("\n" + label.system + " " + colors.warning(`Diagnostics returned exit code ${err.code}.`));
1526
- console.log(colors.muted("Analyzing compiler/test output logs...\n"));
1527
-
1528
- const prompt = `The diagnostics command "${cmdToRun}" failed with exit code ${err.code}. Analyze the following stdout and stderr logs to determine the root cause, identify the files/lines causing the failure, and provide a step-by-step resolution and debugging plan:\n\n\`\`\`\n${output.slice(0, 15000)}\n\`\`\``;
1529
-
1530
- await executeAISpecialCommand(prompt, "Analyzing diagnostics logs...", ctx);
1531
- }
1532
- }
1533
-
1534
- /**
1535
- * Handler for file analysis commands: /explain, /refactor, /bug, /doc, /translate.
1536
- */
1537
- async function handleFileAICommand(cmdName, args, ctx) {
1538
- const filePath = args[0];
1539
- if (!filePath) {
1540
- console.log("\n" + label.system + " " + colors.warning(`Usage: ${cmdName} <file_path>\n`));
1541
- return;
1542
- }
1543
-
1544
- // Resolve path
1545
- const resolvedPath = resolve(process.cwd(), filePath);
1546
-
1547
- // Verify path is inside the workspace
1548
- const { isInsideWorkspace } = await import("./agent.js");
1549
- if (!isInsideWorkspace(resolvedPath)) {
1550
- console.log("\n" + label.system + " " + colors.danger("Error: Path is outside the current workspace sandbox.\n"));
1551
- return;
1552
- }
1553
-
1554
- if (!existsSync(resolvedPath)) {
1555
- console.log("\n" + label.system + " " + colors.danger(`Error: File does not exist at "${filePath}"\n`));
1556
- return;
1557
- }
1558
-
1559
- const stat = statSync(resolvedPath);
1560
- if (stat.isDirectory()) {
1561
- console.log("\n" + label.system + " " + colors.danger(`Error: "${filePath}" is a directory. File path required.\n`));
1562
- return;
1563
- }
1564
-
1565
- if (stat.size > 150 * 1024) { // 150KB limit
1566
- console.log("\n" + label.system + " " + colors.warning(`Warning: File "${filePath}" is too large (${Math.round(stat.size / 1024)}KB). Limits are 150KB to protect context limit.\n`));
1567
- return;
1568
- }
1569
-
1570
- // Read file content
1571
- let content;
1572
- try {
1573
- const { parseFile } = await import("./file-parser.js");
1574
- const parsed = await parseFile(resolvedPath);
1575
- content = parsed.content;
1576
- } catch (err) {
1577
- console.log("\n" + label.system + " " + colors.danger(`Error parsing file: ${err.message}\n`));
1578
- return;
1579
- }
1580
-
1581
- let prompt = "";
1582
- let labelText = "";
1583
-
1584
- switch (cmdName.toLowerCase()) {
1585
- case "/explain":
1586
- labelText = `Explaining ${filePath}...`;
1587
- prompt = `Explain the architecture, design patterns, logic flow, and purpose of the following code. Be clear, technical, and structured:\n\n\`\`\`\n${content}\n\`\`\``;
1588
- break;
1589
- case "/refactor":
1590
- labelText = `Refactoring ${filePath}...`;
1591
- prompt = `Suggest refactoring improvements for the following code. Focus on clean code design principles, optimization, readability, reducing complexity, and fixing potential logic bugs. Return both the refactored code block and explanations:\n\n\`\`\`\n${content}\n\`\`\``;
1592
- break;
1593
- case "/bug":
1594
- labelText = `Auditing bugs in ${filePath}...`;
1595
- prompt = `Perform a thorough static analysis and code review of the following code. Identify potential logical bugs, race conditions, edge case failures, performance bottlenecks, and security hazards. Suggest fixes:\n\n\`\`\`\n${content}\n\`\`\``;
1596
- break;
1597
- case "/doc":
1598
- labelText = `Generating documentation for ${filePath}...`;
1599
- prompt = `Generate comprehensive API documentation, JSDoc/docstrings, and comments for the following code. Ensure code parameters, return values, and types are documented:\n\n\`\`\`\n${content}\n\`\`\``;
1600
- break;
1601
- case "/translate":
1602
- const targetLang = args[1];
1603
- if (!targetLang) {
1604
- console.log("\n" + label.system + " " + colors.warning(`Usage: /translate <file_path> <target_language>\n`));
1605
- return;
1606
- }
1607
- labelText = `Translating ${filePath} to ${targetLang}...`;
1608
- prompt = `Translate the following code into ${targetLang}. Return a clean, syntactically correct, and beautifully structured code block of the translated code:\n\n\`\`\`\n${content}\n\`\`\``;
1609
- break;
1610
- }
1611
-
1612
- try {
1613
- await executeAISpecialCommand(prompt, labelText, ctx);
1614
- } catch (err) {
1615
- console.log("\n" + label.system + " " + colors.danger(`Error: ${err.message}\n`));
1616
- }
1617
- }
1
+ // ═══════════════════════════════════════════════════════════
2
+ // AETHER AI CLI — Interactive Chat Loop
3
+ // Universal AI Gateway & Cyberpunk Command Center
4
+ // ═══════════════════════════════════════════════════════════
5
+
6
+ import { createInterface } from "node:readline";
7
+ import { writeFile } from "node:fs/promises";
8
+ import { readdirSync, existsSync, statSync } from "node:fs";
9
+ import { resolve, join, sep } from "node:path";
10
+ import { exec } from "node:child_process";
11
+ import chalk from "chalk";
12
+ import { Marked } from "marked";
13
+ import { markedTerminal } from "marked-terminal";
14
+
15
+ import {
16
+ colors,
17
+ label,
18
+ separator,
19
+ keyValue,
20
+ bullet,
21
+ modeBadge,
22
+ clearStreamedText,
23
+ StreamFilter,
24
+ stripCodeFences,
25
+ getActiveTheme,
26
+ setTheme,
27
+ getThemesList,
28
+ interactiveMenu
29
+ } from "./ui/theme.js";
30
+ import { createSpinner } from "./ui/spinner.js";
31
+ import { showBanner } from "./ui/banner.js";
32
+ import { routePrompt } from "./ai/router.js";
33
+ import { getActiveProviders } from "./ai/providers.js";
34
+ import {
35
+ getAIConfig,
36
+ loadHistory,
37
+ saveHistory,
38
+ clearHistory,
39
+ setConfigValue,
40
+ listSessions,
41
+ switchSession,
42
+ startNewSession
43
+ } from "./config.js";
44
+ import { MODES, DEFAULT_MODE, getModeByName } from "./modes.js";
45
+ import { parseFile, formatContext } from "./file-parser.js";
46
+ import { runMainframeHack } from "./ai/fallback.js";
47
+ import { AGENT_INSTRUCTIONS } from "./agent.js";
48
+ import { checkForUpdates } from "./updater.js";
49
+ import { getSessionTokenStats, getBreakdownByModel, resetSessionTokenStats } from "./ai/tokens.js";
50
+ import { getGitDiff } from "./git.js";
51
+
52
+
53
+
54
+ // Configure marked dynamically for terminal output
55
+ const getMarked = () => new Marked(markedTerminal({
56
+ reflowText: true,
57
+ width: process.stdout.columns ? Math.max(20, process.stdout.columns - 4) : 80,
58
+ showSectionPrefix: false,
59
+ code: (c) => colors.orange(c),
60
+ codespan: (c) => colors.accent3(c),
61
+ heading: (h) => colors.accent.bold(h),
62
+ strong: (s) => colors.magenta.bold(s),
63
+ em: chalk.italic,
64
+ hr: (h) => colors.dim(h),
65
+ }));
66
+
67
+ /**
68
+ * Starts the interactive Aether chat session.
69
+ * @param {{ mode?: string, preferredProvider?: string }} [options={}]
70
+ */
71
+ export async function startChat(options = {}) {
72
+ // Load AI config
73
+ const aiConfig = await getAIConfig();
74
+
75
+ // Run update check
76
+ await checkForUpdates();
77
+
78
+ // Reset token stats for the new session
79
+ resetSessionTokenStats();
80
+
81
+
82
+ // Set theme from configuration
83
+ const theme = aiConfig.THEME || "cyberpunk";
84
+ setTheme(theme);
85
+
86
+ let currentMode = getModeByName(options.mode) || getModeByName(aiConfig.DEFAULT_MODE) || MODES[DEFAULT_MODE];
87
+ let attachedFiles = [];
88
+
89
+ // Persistent history loader
90
+ const history = await loadHistory();
91
+
92
+ // Mini-game state
93
+ const game = {
94
+ active: false,
95
+ code: "",
96
+ attempts: 0,
97
+ maxAttempts: 6,
98
+ };
99
+
100
+ // Show banner
101
+ showBanner(currentMode.name);
102
+
103
+ // Active providers diagnostic check
104
+ const active = getActiveProviders(aiConfig);
105
+ if (active.length === 0) {
106
+ console.log(
107
+ "\n" + label.system + " " +
108
+ colors.warning("No API keys configured. Using local fallback solvers.") + "\n" +
109
+ " " + colors.muted("Run ") + colors.accent("aether setup") +
110
+ colors.muted(" to configure providers (free options available!).\n")
111
+ );
112
+ } else {
113
+ const providerNames = active.map((a) => a.provider.name);
114
+ const unique = [...new Set(providerNames)];
115
+ console.log(
116
+ label.mesh + " " +
117
+ colors.accent("Failover mesh online: ") +
118
+ colors.text(unique.join(" → ")) +
119
+ colors.muted(" → Krylo fallback")
120
+ );
121
+ console.log(
122
+ " " + colors.dim(`${active.length} node(s) active across ${unique.length} provider(s)`) + "\n"
123
+ );
124
+ }
125
+
126
+ // Display loaded history message if any
127
+ if (history.length > 0) {
128
+ console.log(
129
+ " " + label.info + " " +
130
+ colors.muted(`Restored ${Math.floor(history.length / 2)} message exchanges from persistent logs.`) + "\n"
131
+ );
132
+ }
133
+
134
+ // Completer: handles commands & dynamic local file path autocomplete
135
+ const completer = (line) => {
136
+ const builtIn = [
137
+ "/help", "/mode", "/modes", "/attach", "/files", "/clear",
138
+ "/providers", "/export", "/status", "/copy", "/exit", "/quit",
139
+ "/theme", "/themes", "/history-clear", "/game", "/abort", "/cmd", "/write",
140
+ "/commit", "/run", "/history", "/autopilot", "/tokens", "/update",
141
+ "/review", "/diagnose", "/explain", "/refactor", "/bug", "/doc", "/translate",
142
+ "/search", "/git", "/dashboard"
143
+ ];
144
+ const customCmds = aiConfig.CUSTOM_COMMANDS || {};
145
+ const commands = [...builtIn, ...Object.keys(customCmds)];
146
+
147
+ // File path autocompletion on /attach
148
+ if (line.startsWith("/attach ")) {
149
+ const query = line.slice(8);
150
+ const lastSlash = Math.max(query.lastIndexOf("/"), query.lastIndexOf("\\"));
151
+ let searchDir = ".";
152
+ let searchPrefix = query;
153
+
154
+ if (lastSlash !== -1) {
155
+ searchDir = query.slice(0, lastSlash);
156
+ if (searchDir === "") {
157
+ searchDir = sep;
158
+ }
159
+ searchPrefix = query.slice(lastSlash + 1);
160
+ }
161
+
162
+ try {
163
+ const resolved = resolve(searchDir);
164
+ if (existsSync(resolved) && statSync(resolved).isDirectory()) {
165
+ const files = readdirSync(resolved);
166
+ const hits = files
167
+ .filter((f) => f.toLowerCase().startsWith(searchPrefix.toLowerCase()) && !f.startsWith("."))
168
+ .map((f) => {
169
+ const fullPath = searchDir === "." || searchDir === sep ? f : join(searchDir, f);
170
+ const fullResolved = resolve(fullPath);
171
+ const isDir = statSync(fullResolved).isDirectory();
172
+ return `/attach ${fullPath}${isDir ? "/" : ""}`;
173
+ });
174
+ return [hits.length ? hits : [], line];
175
+ }
176
+ } catch (e) {
177
+ // Fallback silently on fs errors
178
+ }
179
+ return [[], line];
180
+ }
181
+
182
+ // Sub-arguments autocomplete on /mode
183
+ if (line.startsWith("/mode ")) {
184
+ const query = line.slice(6).toLowerCase();
185
+ const modesList = Object.keys(MODES);
186
+ const hits = modesList
187
+ .filter((m) => m.startsWith(query))
188
+ .map((m) => `/mode ${m}`);
189
+ return [hits.length ? hits : [], line];
190
+ }
191
+
192
+ // Sub-arguments autocomplete on /theme
193
+ if (line.startsWith("/theme ")) {
194
+ const query = line.slice(7).toLowerCase();
195
+ const themesList = getThemesList();
196
+ const hits = themesList
197
+ .filter((t) => t.startsWith(query))
198
+ .map((t) => `/theme ${t}`);
199
+ return [hits.length ? hits : [], line];
200
+ }
201
+
202
+ // Sub-arguments autocomplete on /cmd
203
+ if (line.startsWith("/cmd ")) {
204
+ const query = line.slice(5).toLowerCase();
205
+ const subcmds = ["list", "add", "remove"];
206
+ const hits = subcmds
207
+ .filter((s) => s.startsWith(query))
208
+ .map((s) => `/cmd ${s}`);
209
+ return [hits.length ? hits : [], line];
210
+ }
211
+
212
+ const hits = commands.filter((c) => c.startsWith(line));
213
+ return [hits.length ? hits : [], line];
214
+ };
215
+
216
+ // Create readline interface
217
+ const rl = createInterface({
218
+ input: process.stdin,
219
+ output: process.stdout,
220
+ prompt: colors.accent(" ❯ "),
221
+ terminal: true,
222
+ completer
223
+ });
224
+
225
+ // Load persistent history entries directly into the shell up/down array
226
+ if (history.length > 0) {
227
+ const userQueries = history
228
+ .filter((h) => h.role === "user")
229
+ .map((h) => h.content);
230
+ // Readline history is structured newest first (index 0)
231
+ rl.history = [...new Set(userQueries)].reverse();
232
+ }
233
+
234
+ // ── AI Execution Helper ──────────────────────────────────
235
+ async function executeAIQuery(promptText, originalInput = promptText) {
236
+ // ── Build Prompt with Context ─────────────────────────
237
+ let fullPrompt = promptText;
238
+ if (attachedFiles.length > 0) {
239
+ const contexts = attachedFiles.map((f) => formatContext(f)).join("\n\n");
240
+ fullPrompt = `${contexts}\n\n${promptText}`;
241
+ }
242
+
243
+ // Append AGENT_INSTRUCTIONS to mode systemPrompt
244
+ const systemPrompt = currentMode.systemPrompt + "\n" + AGENT_INSTRUCTIONS;
245
+
246
+ let loopCount = 0;
247
+ const MAX_LOOPS = 5;
248
+ let currentQueryPrompt = fullPrompt;
249
+ let aiResponseText = "";
250
+ let lastResult = null;
251
+
252
+ try {
253
+ while (loopCount < MAX_LOOPS) {
254
+ const queryStartTime = Date.now();
255
+ let firstTokenTime = 0;
256
+
257
+ if (loopCount > 0) {
258
+ console.log(colors.accent(`\n🤖 [Aether Autopilot Mode - Iteration ${loopCount + 1}/${MAX_LOOPS}]`));
259
+ }
260
+
261
+ const spinner = createSpinner(
262
+ colors.muted(loopCount === 0 ? `Routing through mesh ${currentMode.label}...` : `Agent executing tasks...`)
263
+ );
264
+ spinner.start();
265
+
266
+ let hasStartedStreaming = false;
267
+ let streamedText = "";
268
+ const filter = new StreamFilter(process.stdout.write.bind(process.stdout));
269
+ const onToken = (token) => {
270
+ if (!hasStartedStreaming) {
271
+ hasStartedStreaming = true;
272
+ firstTokenTime = Date.now();
273
+ spinner.stop();
274
+ }
275
+ filter.write(token);
276
+ streamedText += token;
277
+ };
278
+
279
+ const result = await routePrompt(currentQueryPrompt, systemPrompt, aiConfig, onToken, history);
280
+ spinner.stop();
281
+ filter.flush();
282
+
283
+ aiResponseText = result.text;
284
+ lastResult = result;
285
+
286
+ if (hasStartedStreaming) {
287
+ clearStreamedText(filter.filteredText);
288
+ }
289
+
290
+ // Display response
291
+ console.log("");
292
+ console.log(label.aether + " " + providerBadge(result));
293
+ console.log(separator(""));
294
+ console.log("");
295
+
296
+ if (result.provider === "local" || result.provider === "krylo-fallback") {
297
+ console.log(colors.text(" " + result.text.split("\n").join("\n ")));
298
+ } else {
299
+ let displayText = result.text;
300
+ const rendered = getMarked().parse(displayText);
301
+ console.log(rendered);
302
+ }
303
+
304
+ const elapsedSec = ((Date.now() - queryStartTime) / 1000).toFixed(1);
305
+ let speedText = "";
306
+ if (firstTokenTime > 0) {
307
+ const streamElapsed = (Date.now() - firstTokenTime) / 1000;
308
+ if (streamElapsed > 0.05) {
309
+ const estimatedTokens = Math.max(1, Math.round(streamedText.length / 4));
310
+ const tps = (estimatedTokens / streamElapsed).toFixed(1);
311
+ speedText = ` • ${tps} tok/s`;
312
+ }
313
+ }
314
+
315
+ const showTokens = aiConfig.SHOW_TOKENS !== "false";
316
+ let tokensText = "";
317
+ if (showTokens && result.usage) {
318
+ const { promptTokens, completionTokens } = result.usage;
319
+ tokensText = ` • ${promptTokens.toLocaleString()} in / ${completionTokens.toLocaleString()} out tokens`;
320
+ }
321
+
322
+ console.log(separator("─"));
323
+ console.log(
324
+ " " + colors.dim(`Node ${result.node} • ${result.provider}`) +
325
+ (result.model ? colors.dim(` • ${result.model}`) : "") +
326
+ colors.dim(` • ${elapsedSec}s${speedText}`) +
327
+ colors.dim(tokensText) +
328
+ colors.dim(` • ${Math.floor(history.length / 2)} exchanges`)
329
+ );
330
+ console.log("");
331
+
332
+ // Process any agent tools output by the AI
333
+ const { processAgentBlocks } = await import("./agent.js");
334
+ const toolResults = await processAgentBlocks(aiResponseText, aiConfig, rl);
335
+
336
+ if (toolResults.length === 0) {
337
+ // No tools executed, end loop
338
+ break;
339
+ }
340
+
341
+ // Store this turn in history so AI knows what happened
342
+ history.push({ role: "user", content: currentQueryPrompt, timestamp: new Date() });
343
+ history.push({
344
+ role: "assistant",
345
+ content: aiResponseText,
346
+ provider: result.provider,
347
+ model: result.model,
348
+ node: result.node,
349
+ timestamp: new Date(),
350
+ });
351
+ await saveHistory(history, currentMode.name);
352
+
353
+ // Format tool outputs as next prompt
354
+ let formattedResults = "### Agent Tool Outputs:\n";
355
+ for (const tr of toolResults) {
356
+ if (tr.success) {
357
+ if (tr.tool === "RUN_COMMAND") {
358
+ formattedResults += `\n- RUN_COMMAND "${tr.arg}" succeeded. Output:\n\`\`\`\n${tr.stdout || ""}${tr.stderr || ""}\n\`\`\`;`;
359
+ } else if (tr.tool === "READ_FILE") {
360
+ formattedResults += `\n- READ_FILE "${tr.arg}" succeeded. File Content:\n\`\`\`\n${tr.content}\n\`\`\`;`;
361
+ } else if (tr.tool === "WRITE_FILE") {
362
+ formattedResults += `\n- WRITE_FILE "${tr.arg}" succeeded.`;
363
+ } else if (tr.tool === "SEARCH_WEB") {
364
+ const resultsList = tr.results.map((r, i) => `${i+1}. [${r.title}](${r.url})\n ${r.snippet}`).join("\n");
365
+ formattedResults += `\n- SEARCH_WEB "${tr.arg}" succeeded. Results:\n${resultsList}`;
366
+ }
367
+ } else {
368
+ formattedResults += `\n- ${tr.tool} "${tr.arg}" failed: ${tr.error}`;
369
+ }
370
+ }
371
+ formattedResults += "\n\nPlease continue and finalize your task or perform next steps.";
372
+
373
+ currentQueryPrompt = formattedResults;
374
+ loopCount++;
375
+ }
376
+
377
+ // Store final state in history
378
+ if (loopCount > 0) {
379
+ // Just save to disk to persist
380
+ await saveHistory(history, currentMode.name);
381
+ } else {
382
+ // Standard single-turn save
383
+ history.push({ role: "user", content: originalInput, timestamp: new Date() });
384
+ history.push({
385
+ role: "assistant",
386
+ content: aiResponseText,
387
+ provider: lastResult.provider,
388
+ model: lastResult.model,
389
+ node: lastResult.node,
390
+ timestamp: new Date(),
391
+ });
392
+ await saveHistory(history, currentMode.name);
393
+ }
394
+
395
+ } catch (err) {
396
+ console.log("\n" + label.error + " " + colors.danger(err.message) + "\n");
397
+ }
398
+
399
+ // Sync shell's recall history list
400
+ const userQueries = history
401
+ .filter((h) => h.role === "user")
402
+ .map((h) => h.content);
403
+ rl.history = [...new Set(userQueries)].reverse();
404
+ }
405
+
406
+ rl.prompt();
407
+
408
+ rl.on("line", async (line) => {
409
+ const input = line.trim();
410
+ if (!input) {
411
+ rl.prompt();
412
+ return;
413
+ }
414
+
415
+ // ── Handle Game Input ──────────────────────────────────
416
+ if (game.active && !input.startsWith("/")) {
417
+ handleGuess(input, game);
418
+ rl.prompt();
419
+ return;
420
+ }
421
+
422
+ // ── Handle Slash Commands ──────────────────────────────
423
+ if (input.startsWith("/")) {
424
+ const [cmd, ...args] = input.split(/\s+/);
425
+ const builtInList = [
426
+ "/", "/help", "/mode", "/modes", "/attach", "/files", "/clear",
427
+ "/providers", "/export", "/status", "/copy", "/exit", "/quit",
428
+ "/theme", "/themes", "/history-clear", "/game", "/abort", "/cmd",
429
+ "/guess", "/write", "/commit", "/run", "/history", "/autopilot", "/tokens",
430
+ "/update", "/review", "/diagnose", "/explain", "/refactor", "/bug", "/doc",
431
+ "/translate", "/search", "/git", "/dashboard"
432
+ ];
433
+
434
+ const customCmds = aiConfig.CUSTOM_COMMANDS || {};
435
+
436
+ if (!builtInList.includes(cmd.toLowerCase()) && customCmds[cmd]) {
437
+ const template = customCmds[cmd];
438
+ const userArg = args.join(" ");
439
+ const rewrittenPrompt = template + (userArg ? " " + userArg : "");
440
+
441
+ console.log("\n" + label.system + " " + colors.accent(`Executing custom command: `) + colors.text(cmd));
442
+ console.log(" " + colors.muted("Prompt: ") + colors.text(rewrittenPrompt) + "\n");
443
+
444
+ await executeAIQuery(rewrittenPrompt, input);
445
+ rl.prompt();
446
+ return;
447
+ }
448
+
449
+ const handled = await handleCommand(input, {
450
+ currentMode,
451
+ attachedFiles,
452
+ history,
453
+ aiConfig,
454
+ game,
455
+ setMode: (mode) => { currentMode = mode; },
456
+ addFile: (file) => { attachedFiles.push(file); },
457
+ clearFiles: () => { attachedFiles = []; },
458
+ rl,
459
+ });
460
+ if (handled !== "exit") {
461
+ rl.prompt();
462
+ }
463
+ return;
464
+ }
465
+
466
+ await executeAIQuery(input);
467
+ rl.prompt();
468
+ });
469
+
470
+ rl.on("close", () => {
471
+ console.log("\n" + label.system + " " + colors.muted("Session terminated. Stay cyberpunk. ⚡\n"));
472
+ process.exit(0);
473
+ });
474
+ }
475
+
476
+ /**
477
+ * Handles slash commands in the chat.
478
+ */
479
+ async function handleCommand(input, ctx) {
480
+ const [cmd, ...args] = input.split(/\s+/);
481
+
482
+ switch (cmd.toLowerCase()) {
483
+ case "/":
484
+ case "/help":
485
+ showHelp(ctx.aiConfig);
486
+ break;
487
+
488
+ case "/mode":
489
+ handleModeSwitch(args, ctx);
490
+ break;
491
+
492
+ case "/modes":
493
+ showModes();
494
+ break;
495
+
496
+ case "/attach":
497
+ await handleAttach(args, ctx);
498
+ break;
499
+
500
+ case "/files":
501
+ showAttachedFiles(ctx.attachedFiles);
502
+ break;
503
+
504
+ case "/clear":
505
+ // Actual screen clear & scrollback reset
506
+ process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
507
+ showBanner(ctx.currentMode.name);
508
+ break;
509
+
510
+ case "/export":
511
+ await handleExport(ctx.history);
512
+ break;
513
+
514
+ case "/status":
515
+ showStatus(ctx);
516
+ break;
517
+
518
+ case "/providers":
519
+ showActiveProviders(ctx.aiConfig);
520
+ break;
521
+
522
+ case "/update":
523
+ console.log("\n" + label.system + " " + colors.muted("Checking registry for updates..."));
524
+ await checkForUpdates(true);
525
+ console.log("");
526
+ break;
527
+
528
+ case "/review":
529
+ await handleReviewCommand(ctx);
530
+ break;
531
+
532
+ case "/diagnose":
533
+ await handleDiagnoseCommand(args, ctx);
534
+ break;
535
+
536
+ case "/explain":
537
+ case "/refactor":
538
+ case "/bug":
539
+ case "/doc":
540
+ case "/translate":
541
+ await handleFileAICommand(cmd, args, ctx);
542
+ break;
543
+
544
+ case "/search":
545
+ await handleSearchCommand(args, ctx);
546
+ break;
547
+
548
+ case "/theme":
549
+ await handleThemeSwitch(args);
550
+ break;
551
+
552
+ case "/themes":
553
+ showThemesList();
554
+ break;
555
+
556
+ case "/history-clear":
557
+ await handleHistoryClear(ctx.history, ctx.rl);
558
+ break;
559
+
560
+ case "/game":
561
+ handleGameStart(ctx.game);
562
+ break;
563
+
564
+ case "/abort":
565
+ handleGameAbort(ctx.game);
566
+ break;
567
+
568
+ case "/guess":
569
+ if (ctx.game.active) {
570
+ handleGuess(args[0] || "", ctx.game);
571
+ } else {
572
+ console.log("\n" + label.system + " " + colors.warning("Game is not active. Type /game to start.\n"));
573
+ }
574
+ break;
575
+
576
+ case "/copy":
577
+ await handleCopy(ctx.history);
578
+ break;
579
+
580
+ case "/cmd":
581
+ await handleCustomCommands(args, ctx);
582
+ break;
583
+
584
+ case "/write":
585
+ await handleWriteFile(args, ctx);
586
+ break;
587
+
588
+ case "/commit":
589
+ await handleCommitInsideChat(ctx);
590
+ break;
591
+
592
+ case "/run":
593
+ await handleRunCommand(args, ctx);
594
+ break;
595
+
596
+ case "/history":
597
+ await handleHistorySwitch(ctx);
598
+ break;
599
+
600
+ case "/autopilot":
601
+ await handleAutopilotSwitch(args, ctx);
602
+ break;
603
+
604
+ case "/git":
605
+ await handleGitTUI(ctx);
606
+ break;
607
+
608
+ case "/dashboard":
609
+ await handleDashboardCommand(ctx);
610
+ break;
611
+
612
+ case "/tokens":
613
+ await handleTokensDisplay(ctx);
614
+ break;
615
+
616
+ case "/exit":
617
+ case "/quit":
618
+ ctx.rl.close();
619
+ return "exit";
620
+
621
+ default:
622
+ console.log("\n" + label.system + " " + colors.warning(`Unknown command: ${cmd}. Type /help for available commands.\n`));
623
+ }
624
+ }
625
+
626
+ // ── Command Handlers ────────────────────────────────────────
627
+
628
+ function showHelp(aiConfig) {
629
+ console.log("");
630
+ console.log(colors.brand(" AETHER CLI COMMANDS"));
631
+ console.log(separator(""));
632
+ console.log("");
633
+ console.log(keyValue("/", "Show this help menu"));
634
+ console.log(keyValue("/help", "Show this help menu"));
635
+ console.log(keyValue("/mode <name>", "Switch mode (" + Object.keys(MODES).join(", ") + ")"));
636
+ console.log(keyValue("/modes", "List all modes with signal metrics"));
637
+ console.log(keyValue("/theme <name>", "Switch visual theme (cyberpunk, matrix, synthwave, crimson)"));
638
+ console.log(keyValue("/themes", "List available visual themes"));
639
+ console.log(keyValue("/attach <path>", "Attach a file for context (supports Tab path autocomplete!)"));
640
+ console.log(keyValue("/files", "List attached files"));
641
+ console.log(keyValue("/clear", "Clear terminal screen and reprint banner"));
642
+ console.log(keyValue("/providers", "Show active AI providers"));
643
+ console.log(keyValue("/export", "Export conversation to file"));
644
+ console.log(keyValue("/history", "List, switch, and resume past interactive chat sessions"));
645
+ console.log(keyValue("/history-clear", "Clear saved persistent chat history"));
646
+ console.log(keyValue("/autopilot <mode|debug [cmd]>", "View/switch autopilot level (off, safe, workspace, machine) or run autonomous debug loop"));
647
+ console.log(keyValue("/git", "Launch interactive Git branch tree, history, and file staging TUI"));
648
+ console.log(keyValue("/dashboard", "Spawn web-based local cyberpunk telemetry dashboard companion"));
649
+ console.log(keyValue("/tokens", "View detailed session token usage and exchanges telemetry"));
650
+ console.log(keyValue("/update", "Force check for updates and update Aether CLI manually"));
651
+ console.log(keyValue("/game", "Start the local mainframe hacking mini-game"));
652
+ console.log(keyValue("/copy", "Copy the last assistant response to clipboard"));
653
+ console.log(keyValue("/cmd <list|add|remove>", "Manage custom command shortcuts"));
654
+ console.log(keyValue("/write <filename>", "Extract last code block and save to file"));
655
+ console.log(keyValue("/commit", "Generate conventional commit message and commit changes"));
656
+ console.log(keyValue("/run <command>", "Execute a shell command interactively"));
657
+ console.log(keyValue("/review", "Run git diff and stream an AI code review"));
658
+ console.log(keyValue("/diagnose [cmd]", "Run build/tests and AI-debug any errors"));
659
+ console.log(keyValue("/explain <file>", "AI-explain the design and logic of a file"));
660
+ console.log(keyValue("/refactor <file>", "AI-refactor the code of a target file"));
661
+ console.log(keyValue("/bug <file>", "AI-audit a file to find potential logic bugs"));
662
+ console.log(keyValue("/doc <file>", "AI-generate documentation/docstrings for a file"));
663
+ console.log(keyValue("/translate <file> <lang>", "AI-translate code of a file to another language"));
664
+ console.log(keyValue("/search <query>", "Find matches in code files (use --ai for semantic search)"));
665
+ console.log(keyValue("/exit", "End session"));
666
+
667
+ if (aiConfig && aiConfig.CUSTOM_COMMANDS) {
668
+ const custom = aiConfig.CUSTOM_COMMANDS;
669
+ const entries = Object.entries(custom);
670
+ if (entries.length > 0) {
671
+ console.log("");
672
+ console.log(colors.brand(" ⚡ CUSTOM SHORTCUTS"));
673
+ console.log(separator("─"));
674
+ for (const [cmd, template] of entries) {
675
+ console.log(keyValue(cmd, `Shortcut for: "${template}"`));
676
+ }
677
+ }
678
+ }
679
+ console.log("");
680
+ }
681
+
682
+ function handleModeSwitch(args, ctx) {
683
+ const modeName = args[0];
684
+ if (!modeName) {
685
+ console.log("\n" + label.mode + " " + colors.warning("Usage: /mode <" + Object.keys(MODES).join("|") + ">\n"));
686
+ return;
687
+ }
688
+
689
+ const newMode = getModeByName(modeName);
690
+ if (!newMode) {
691
+ console.log("\n" + label.mode + " " + colors.danger(`Unknown mode: "${modeName}".`) + " " + colors.muted("Available: " + Object.keys(MODES).join(", ") + "\n"));
692
+ return;
693
+ }
694
+
695
+ ctx.setMode(newMode);
696
+ console.log("\n" + label.mode + " " + colors.accent("Switched to ") + modeBadge(newMode.name));
697
+ console.log(" " + colors.muted(newMode.description) + "\n");
698
+
699
+ const sig = newMode.signal;
700
+ console.log(" " + signalBar("Reasoning", sig.reasoning));
701
+ console.log(" " + signalBar("Clarity", sig.clarity));
702
+ console.log(" " + signalBar("System IQ", sig.systemIQ));
703
+ console.log(" " + signalBar("Delivery", sig.delivery));
704
+ console.log("");
705
+ }
706
+
707
+ function showModes() {
708
+ console.log("");
709
+ console.log(colors.brand(" ◈ AVAILABLE REASONING MODES"));
710
+ console.log(separator(""));
711
+ console.log("");
712
+
713
+ for (const mode of Object.values(MODES)) {
714
+ console.log(" " + modeBadge(mode.name) + " " + colors.muted(`(${mode.layer})`));
715
+ console.log(" " + colors.text(mode.description));
716
+ const sig = mode.signal;
717
+ console.log(" " + signalBar("RSN", sig.reasoning) + " " + signalBar("CLR", sig.clarity) + " " + signalBar("SIQ", sig.systemIQ) + " " + signalBar("DLV", sig.delivery));
718
+ console.log("");
719
+ }
720
+ }
721
+
722
+ async function handleAttach(args, ctx) {
723
+ const filePath = args.join(" ").trim();
724
+ if (!filePath) {
725
+ const { scanWorkspaceFiles } = await import("./file-parser.js");
726
+ const { interactiveCheckbox } = await import("./ui/theme.js");
727
+
728
+ const workspaceFiles = scanWorkspaceFiles(process.cwd());
729
+ if (workspaceFiles.length === 0) {
730
+ console.log("\n" + label.file + " " + colors.muted("No supported files found in this workspace.\n"));
731
+ return;
732
+ }
733
+
734
+ ctx.rl.pause();
735
+ const selected = await interactiveCheckbox(
736
+ "Attach files (Arrow Keys to navigate, Space to toggle, Enter to confirm, Esc/q to cancel):\n",
737
+ workspaceFiles,
738
+ ctx.attachedFiles.map(f => f.relativePath || f.name)
739
+ );
740
+ ctx.rl.resume();
741
+
742
+ if (selected === null) {
743
+ console.log("\n" + label.file + " " + colors.muted("Selection canceled.\n"));
744
+ return;
745
+ }
746
+
747
+ ctx.clearFiles();
748
+ if (selected.length === 0) {
749
+ console.log("\n" + label.file + " " + colors.success("Cleared all attachments.\n"));
750
+ return;
751
+ }
752
+
753
+ let successCount = 0;
754
+ for (const file of selected) {
755
+ try {
756
+ const fileData = await parseFile(file);
757
+ fileData.relativePath = file;
758
+ ctx.addFile(fileData);
759
+ successCount++;
760
+ } catch (err) {
761
+ console.log(label.error + " " + colors.danger(`Failed to attach ${file}: ${err.message}`));
762
+ }
763
+ }
764
+ console.log("\n" + label.file + " " + colors.success(`Successfully attached ${successCount} file(s).\n`));
765
+ return;
766
+ }
767
+
768
+ try {
769
+ const fileData = await parseFile(filePath);
770
+ ctx.addFile(fileData);
771
+ console.log("\n" + label.file + " " + colors.success(`Attached: ${fileData.name}`));
772
+ console.log(" " + colors.muted(`${formatBytes(fileData.size)} • ${fileData.extension} • ${ctx.attachedFiles.length} file(s) loaded\n`));
773
+ } catch (err) {
774
+ console.log("\n" + label.error + " " + colors.danger(err.message) + "\n");
775
+ }
776
+ }
777
+
778
+ function showAttachedFiles(files) {
779
+ if (files.length === 0) {
780
+ console.log("\n" + label.file + " " + colors.muted("No files attached. Use /attach <path> to add context.\n"));
781
+ return;
782
+ }
783
+
784
+ console.log("");
785
+ console.log(label.file + " " + colors.accent(`${files.length} file(s) attached:`));
786
+ for (const f of files) {
787
+ console.log(bullet(`${f.name} (${formatBytes(f.size)}, ${f.extension})`));
788
+ }
789
+ console.log("");
790
+ }
791
+
792
+ async function handleExport(history) {
793
+ if (history.length === 0) {
794
+ console.log("\n" + label.system + " " + colors.muted("No conversation to export.\n"));
795
+ return;
796
+ }
797
+
798
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
799
+ const filename = `aether-chat-${timestamp}.md`;
800
+ const filepath = resolve(filename);
801
+
802
+ let content = `# Aether AI Chat Export\n*Exported at ${new Date().toLocaleString()}*\n\n---\n\n`;
803
+
804
+ for (const entry of history) {
805
+ if (entry.role === "user") {
806
+ content += `## 👤 You\n${entry.content}\n\n`;
807
+ } else {
808
+ content += `## 🤖 Aether (${entry.provider || "unknown"})\n${entry.content}\n\n---\n\n`;
809
+ }
810
+ }
811
+
812
+ try {
813
+ await writeFile(filepath, content, "utf-8");
814
+ console.log("\n" + label.system + " " + colors.success(`Exported to: ${filepath}\n`));
815
+ } catch (err) {
816
+ console.log("\n" + label.error + " " + colors.danger(`Export failed: ${err.message}\n`));
817
+ }
818
+ }
819
+
820
+ function showStatus(ctx) {
821
+ const active = getActiveProviders(ctx.aiConfig);
822
+
823
+ console.log("");
824
+ console.log(colors.brand(" ◈ SESSION STATUS"));
825
+ console.log(separator("─"));
826
+ console.log(keyValue(" Theme", getActiveTheme().toUpperCase()));
827
+ console.log(keyValue(" Mode", ctx.currentMode.label));
828
+ console.log(keyValue(" Layer", ctx.currentMode.layer));
829
+ console.log(keyValue(" Exchanges", String(Math.floor(ctx.history.length / 2))));
830
+ console.log(keyValue(" Files", String(ctx.attachedFiles.length)));
831
+ console.log(keyValue(" Providers", String(active.length)));
832
+ console.log("");
833
+ }
834
+
835
+ function showActiveProviders(aiConfig) {
836
+ const active = getActiveProviders(aiConfig);
837
+
838
+ console.log("");
839
+ console.log(colors.brand(" ◈ ACTIVE PROVIDERS"));
840
+ console.log(separator("─"));
841
+
842
+ if (active.length === 0) {
843
+ console.log(" " + colors.warning("No providers. Run `aether setup` to configure.") + "\n");
844
+ return;
845
+ }
846
+
847
+ for (const { provider } of active) {
848
+ console.log(" " + colors.success("") + colors.text(provider.name) + colors.dim(` • ${provider.defaultModel}`));
849
+ }
850
+ console.log(" " + colors.success(" ") + colors.text("Krylo Companion") + colors.dim(" • Local fallback"));
851
+ console.log(" " + colors.success("✓ ") + colors.text("Math Solver") + colors.dim(" • Local"));
852
+ console.log("");
853
+ }
854
+
855
+ async function handleThemeSwitch(args) {
856
+ const themeName = args[0];
857
+ if (!themeName) {
858
+ console.log("\n" + label.system + " " + colors.warning("Usage: /theme <theme-name>. Type /themes to list themes.\n"));
859
+ return;
860
+ }
861
+
862
+ const success = setTheme(themeName);
863
+ if (success) {
864
+ await setConfigValue("THEME", themeName.toLowerCase().trim());
865
+ console.log("\n" + label.system + " " + colors.success(`✓ Theme switched to ${themeName.toUpperCase()}`));
866
+ console.log(" " + colors.muted("Visual grid modulates synchronized.\n"));
867
+ } else {
868
+ console.log("\n" + label.system + " " + colors.danger(`Unknown theme: "${themeName}".`) + " " + colors.muted(`Available: ${getThemesList().join(", ")}\n`));
869
+ }
870
+ }
871
+
872
+ function showThemesList() {
873
+ console.log("");
874
+ console.log(colors.brand(" ◈ AVAILABLE COLOR THEMES"));
875
+ console.log(separator("─"));
876
+ const active = getActiveTheme();
877
+ for (const t of getThemesList()) {
878
+ const activeText = t === active ? colors.success("★ ACTIVE") : "";
879
+ console.log(bullet(t.toUpperCase().padEnd(14) + activeText));
880
+ }
881
+ console.log("");
882
+ }
883
+
884
+ async function handleHistoryClear(history, rl) {
885
+ await clearHistory();
886
+ history.length = 0;
887
+ if (rl) rl.history = [];
888
+ console.log("\n" + label.system + " " + colors.success("✓ Persistent chat history and prompt history cleared.\n"));
889
+ }
890
+
891
+ async function handleAutopilotSwitch(args, ctx) {
892
+ const setting = args[0]?.toLowerCase().trim();
893
+ if (setting === "debug") {
894
+ await handleAutopilotDebug(args.slice(1).join(" "), ctx);
895
+ return;
896
+ }
897
+ if (!setting) {
898
+ const current = (ctx.aiConfig.AUTOPILOT || "off").toUpperCase();
899
+ console.log("\n" + label.system + " " + colors.brand("🤖 AUTOPILOT AGENT CONFIGURATION"));
900
+ console.log(separator("─"));
901
+ console.log(keyValue(" Current Setting", current));
902
+ console.log("");
903
+ console.log(" " + colors.muted("Available Modes:"));
904
+ console.log(" • " + colors.accent("off") + colors.text(" - Always ask user for confirmation before executing any actions."));
905
+ console.log(" • " + colors.accent("safe") + colors.text(" - Run read-only/safe terminal commands and searches automatically."));
906
+ console.log(" • " + colors.accent("workspace") + colors.text(" - Run any actions automatically if they stay inside the workspace."));
907
+ console.log("" + colors.accent("machine") + colors.text(" - Complete autopilot. Run any action automatically (Full access)."));
908
+ console.log("");
909
+ console.log(" " + colors.muted("To change setting: ") + colors.accent("/autopilot <mode>") + "\n");
910
+ return;
911
+ }
912
+
913
+ const valid = ["off", "safe", "workspace", "machine"];
914
+ if (!valid.includes(setting)) {
915
+ console.log("\n" + label.system + " " + colors.danger(`ERROR: Unknown autopilot mode "${setting}".`) + " " + colors.muted("Choose from: off, safe, workspace, machine.\n"));
916
+ return;
917
+ }
918
+
919
+ await setConfigValue("AUTOPILOT", setting);
920
+ ctx.aiConfig.AUTOPILOT = setting;
921
+ console.log("\n" + label.system + " " + colors.success(`✓ Autopilot setting updated to ${setting.toUpperCase()} successfully.\n`));
922
+ }
923
+
924
+ async function handleHistorySwitch(ctx) {
925
+ const sessions = listSessions();
926
+ if (sessions.length === 0) {
927
+ console.log("\n" + label.system + " " + colors.muted("No past chat sessions found.\n"));
928
+ return;
929
+ }
930
+
931
+ const items = sessions.map((s) => {
932
+ const dateStr = new Date(s.timestamp).toLocaleString();
933
+ const count = s.messages.length;
934
+ const exchanges = Math.floor(count / 2);
935
+ // Find first user query preview
936
+ const firstQuery = s.messages.find((m) => m.role === "user")?.content || "Empty conversation";
937
+ const preview = firstQuery.length > 50 ? firstQuery.slice(0, 47) + "..." : firstQuery;
938
+ const modeBadgeText = `[${s.mode}]`;
939
+ return `${colors.dim(dateStr)} ${colors.brand(modeBadgeText.padEnd(12))} ${colors.muted(exchanges + " exch")} • ${colors.text(preview)}`;
940
+ });
941
+
942
+ // Add an option to start a new session
943
+ items.push(colors.accent("➕ Start a new chat session"));
944
+
945
+ ctx.rl.pause();
946
+ const selectedIndex = await interactiveMenu(
947
+ "Select a past chat session to resume (Arrow Keys to navigate, Enter to select, Esc/q to cancel):\n",
948
+ items
949
+ );
950
+ ctx.rl.resume();
951
+
952
+ if (selectedIndex === null) {
953
+ console.log("\n" + label.system + " " + colors.muted("Selection canceled.\n"));
954
+ return;
955
+ }
956
+
957
+ // Clear screen and load the selected session
958
+ process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
959
+
960
+ if (selectedIndex === sessions.length) {
961
+ // Start new session
962
+ const newSessionFile = startNewSession();
963
+ ctx.history.length = 0;
964
+ showBanner(ctx.currentMode.name);
965
+ console.log("\n" + label.system + " " + colors.success("Started a new chat session.\n"));
966
+ } else {
967
+ const selectedSession = sessions[selectedIndex];
968
+ switchSession(selectedSession.file);
969
+
970
+ // Load history
971
+ const loadedHistory = await loadHistory();
972
+ ctx.history.length = 0;
973
+ for (const msg of loadedHistory) {
974
+ ctx.history.push(msg);
975
+ }
976
+
977
+ showBanner(ctx.currentMode.name);
978
+ console.log("\n" + label.system + " " + colors.success(`✓ Switched to chat session from ${new Date(selectedSession.timestamp).toLocaleString()}`));
979
+ console.log(" " + colors.muted(`Restored ${Math.floor(ctx.history.length / 2)} message exchanges.\n`));
980
+ }
981
+
982
+ // Sync shell's recall history list
983
+ const userQueries = ctx.history
984
+ .filter((h) => h.role === "user")
985
+ .map((h) => h.content);
986
+ ctx.rl.history = [...new Set(userQueries)].reverse();
987
+ }
988
+
989
+ function handleGameStart(game) {
990
+ if (game.active) {
991
+ console.log("\n" + label.system + " " + colors.warning("Mainframe breach is already in progress. Type /abort to cancel.\n"));
992
+ return;
993
+ }
994
+
995
+ // Set up game
996
+ game.active = true;
997
+ game.attempts = 0;
998
+
999
+ // Generate random 4-digit code
1000
+ const code = Array.from({ length: 4 }, () => Math.floor(Math.random() * 10)).join("");
1001
+ game.code = code;
1002
+
1003
+ const rules = runMainframeHack();
1004
+ console.log("\n" + rules.text + "\n");
1005
+ }
1006
+
1007
+ function handleGameAbort(game) {
1008
+ if (!game.active) {
1009
+ console.log("\n" + label.system + " " + colors.warning("No security breach in progress.\n"));
1010
+ return;
1011
+ }
1012
+ game.active = false;
1013
+ console.log("\n" + label.system + " " + colors.warning("Breach protocol aborted. Connection terminated.\n"));
1014
+ }
1015
+
1016
+ function handleGuess(input, game) {
1017
+ const guess = input.trim();
1018
+ if (!/^\d{4}$/.test(guess)) {
1019
+ console.log("\n" + label.error + " " + colors.danger("BREACH ERROR: Code must be exactly 4 digits (0-9).") + "\n");
1020
+ return;
1021
+ }
1022
+
1023
+ game.attempts++;
1024
+
1025
+ const codeArr = game.code.split("");
1026
+ const guessArr = guess.split("");
1027
+
1028
+ let hits = 0;
1029
+ let closes = 0;
1030
+
1031
+ const codeUsed = [false, false, false, false];
1032
+ const guessUsed = [false, false, false, false];
1033
+
1034
+ // First pass: Hits
1035
+ for (let i = 0; i < 4; i++) {
1036
+ if (guessArr[i] === codeArr[i]) {
1037
+ hits++;
1038
+ codeUsed[i] = true;
1039
+ guessUsed[i] = true;
1040
+ }
1041
+ }
1042
+
1043
+ // Second pass: Closes
1044
+ for (let i = 0; i < 4; i++) {
1045
+ if (guessUsed[i]) continue;
1046
+ for (let j = 0; j < 4; j++) {
1047
+ if (codeUsed[j]) continue;
1048
+ if (guessArr[i] === codeArr[j]) {
1049
+ closes++;
1050
+ codeUsed[j] = true;
1051
+ break;
1052
+ }
1053
+ }
1054
+ }
1055
+
1056
+ console.log("");
1057
+ console.log(colors.magenta(` [BREACH ATTEMPT #${game.attempts} / ${game.maxAttempts}]`));
1058
+ console.log(colors.text(` BREACH INPUT: ${guess.split("").join(" ")}`));
1059
+ console.log(colors.success(` HITS (Pos): ${"█ ".repeat(hits)}${"░ ".repeat(4 - hits)} (${hits})`));
1060
+ console.log(colors.warning(` CLOSE (Val): ${"█ ".repeat(closes)}${"░ ".repeat(4 - closes)} (${closes})`));
1061
+ console.log("");
1062
+
1063
+ if (hits === 4) {
1064
+ console.log(label.system + " " + colors.success("MAINFRAME BYPASSED! Access granted. Decryption complete. 🔓\n"));
1065
+ game.active = false;
1066
+ } else if (game.attempts >= game.maxAttempts) {
1067
+ console.log(label.error + " " + colors.danger("SECURITY SHUTDOWN! Mainframe locked out. Intrusion logged. 🔒"));
1068
+ console.log(" Intrusion PIN was: " + colors.accent(game.code) + "\n");
1069
+ game.active = false;
1070
+ } else {
1071
+ console.log(colors.muted(" Recalibrating security bypass codes...") + "\n");
1072
+ }
1073
+ }
1074
+
1075
+ async function handleCopy(history) {
1076
+ const lastResponse = [...history].reverse().find((h) => h.role === "assistant");
1077
+ if (!lastResponse) {
1078
+ console.log("\n" + label.system + " " + colors.muted("No response to copy yet.\n"));
1079
+ return;
1080
+ }
1081
+
1082
+ try {
1083
+ await copyToClipboard(lastResponse.content);
1084
+ console.log("\n" + label.system + " " + colors.success("✓ Last response copied to OS Clipboard successfully!\n"));
1085
+ } catch (err) {
1086
+ console.log("\n" + label.system + " " + colors.muted("Unable to copy automatically. Displaying content below:"));
1087
+ console.log(colors.text(lastResponse.content.slice(0, 800)));
1088
+ if (lastResponse.content.length > 800) {
1089
+ console.log(colors.dim(" [... truncated, use /export to save full conversation]"));
1090
+ }
1091
+ console.log("");
1092
+ }
1093
+ }
1094
+
1095
+ function copyToClipboard(text) {
1096
+ return new Promise((resolve, reject) => {
1097
+ let command;
1098
+ if (process.platform === "win32") {
1099
+ command = "clip";
1100
+ } else if (process.platform === "darwin") {
1101
+ command = "pbcopy";
1102
+ } else {
1103
+ command = "xclip -selection clipboard || xsel -ib";
1104
+ }
1105
+
1106
+ try {
1107
+ const child = exec(command, (err) => {
1108
+ if (err) reject(err);
1109
+ else resolve();
1110
+ });
1111
+ child.stdin.write(text);
1112
+ child.stdin.end();
1113
+ } catch (e) {
1114
+ reject(e);
1115
+ }
1116
+ });
1117
+ }
1118
+
1119
+ // ── Box / Badges / Theme helpers ─────────────────────────────
1120
+
1121
+ function providerBadge(result) {
1122
+ const badges = {
1123
+ "groq": chalk.bgHex("#1a2a1a").hex("#67ffb0")(" Groq "),
1124
+ "together ai": chalk.bgHex("#1a2a1a").hex("#67ffb0")(" Together "),
1125
+ "cerebras": chalk.bgHex("#1a2a1a").hex("#67ffb0")(" Cerebras "),
1126
+ "openai": chalk.bgHex("#1a2a1a").hex("#67ffb0")(" OpenAI "),
1127
+ "google": chalk.bgHex("#1a1a2a").hex("#2d7dff")(" Gemini "),
1128
+ "anthropic": chalk.bgHex("#2a1a2a").hex("#b06cff")(" Claude "),
1129
+ "xai": chalk.bgHex("#1a2a1a").hex("#67ffb0")(" Grok "),
1130
+ "mistral ai": chalk.bgHex("#1a1a2a").hex("#ffb900")(" Mistral "),
1131
+ "openrouter": chalk.bgHex("#1a1a2a").hex("#6ce8ff")(" OpenRouter "),
1132
+ "cohere": chalk.bgHex("#1a2a2a").hex("#6ce8ff")(" Cohere "),
1133
+ "deepseek": chalk.bgHex("#1a1a2a").hex("#2d7dff")(" DeepSeek "),
1134
+ "perplexity": chalk.bgHex("#1a2a2a").hex("#6ce8ff")(" Perplexity "),
1135
+ "fireworks ai": chalk.bgHex("#2a1a1a").hex("#ff6b8d")(" Fireworks "),
1136
+ "local": chalk.bgHex("#1a2a1a").hex("#67ffb0")(" Math Solver "),
1137
+ "krylo-fallback": chalk.bgHex("#0c1825").hex("#6ce8ff")(" Krylo "),
1138
+ };
1139
+
1140
+ const badge = badges[result.provider] || colors.muted(` ${result.provider} `);
1141
+ return badge + colors.dim(` Node ${result.node}`);
1142
+ }
1143
+
1144
+ function signalBar(name, value) {
1145
+ const filled = Math.round(value / 10);
1146
+ const empty = 10 - filled;
1147
+ const bar = colors.accent("".repeat(filled)) + colors.dim("░".repeat(empty));
1148
+ return `${colors.muted(name.padEnd(10))} ${bar} ${colors.muted(value + "%")}`;
1149
+ }
1150
+
1151
+ function formatBytes(bytes) {
1152
+ if (bytes < 1024) return `${bytes}B`;
1153
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
1154
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
1155
+ }
1156
+
1157
+ /**
1158
+ * Handles the management of custom slash command shortcuts.
1159
+ */
1160
+ async function handleCustomCommands(args, ctx) {
1161
+ const sub = args[0]?.toLowerCase();
1162
+
1163
+ if (sub === "list") {
1164
+ const custom = ctx.aiConfig.CUSTOM_COMMANDS || {};
1165
+ const entries = Object.entries(custom);
1166
+
1167
+ console.log("");
1168
+ console.log(colors.brand(" ⚡ CUSTOM SHORTCUT COMMANDS"));
1169
+ console.log(separator(""));
1170
+
1171
+ if (entries.length === 0) {
1172
+ console.log(" " + colors.muted("No custom commands registered."));
1173
+ console.log(" " + colors.muted("Create one: ") + colors.accent("/cmd add /explain \"Explain this code:\"") + "\n");
1174
+ return;
1175
+ }
1176
+
1177
+ for (const [cmd, template] of entries) {
1178
+ console.log(` ${colors.accent(cmd.padEnd(16))} ${colors.text(template)}`);
1179
+ }
1180
+ console.log("");
1181
+ return;
1182
+ }
1183
+
1184
+ if (sub === "add") {
1185
+ const name = args[1];
1186
+ const template = args.slice(2).join(" ");
1187
+
1188
+ if (!name || !template) {
1189
+ console.log("\n" + label.system + " " + colors.warning("Usage: /cmd add <name> <template>"));
1190
+ console.log(" " + colors.muted("Example: /cmd add /explain \"Explain this code in detail:\"") + "\n");
1191
+ return;
1192
+ }
1193
+
1194
+ if (!name.startsWith("/")) {
1195
+ console.log("\n" + label.system + " " + colors.danger("ERROR: Command name must start with a slash '/' (e.g. /explain)") + "\n");
1196
+ return;
1197
+ }
1198
+
1199
+ const builtIn = [
1200
+ "/help", "/mode", "/modes", "/attach", "/files", "/clear",
1201
+ "/providers", "/export", "/status", "/copy", "/exit", "/quit",
1202
+ "/theme", "/themes", "/history-clear", "/game", "/abort", "/cmd", "/guess", "/tokens"
1203
+ ];
1204
+
1205
+ if (builtIn.includes(name.toLowerCase())) {
1206
+ console.log("\n" + label.system + " " + colors.danger(`ERROR: Cannot override system command "${name}"`) + "\n");
1207
+ return;
1208
+ }
1209
+
1210
+ const custom = ctx.aiConfig.CUSTOM_COMMANDS || {};
1211
+ custom[name] = template;
1212
+
1213
+ await setConfigValue("CUSTOM_COMMANDS", custom);
1214
+ ctx.aiConfig.CUSTOM_COMMANDS = custom; // sync context
1215
+
1216
+ console.log("\n" + label.system + " " + colors.success(`✓ Command registered successfully!`));
1217
+ console.log(` ${colors.accent(name)} ➔ "${template}"\n`);
1218
+ return;
1219
+ }
1220
+
1221
+ if (sub === "remove") {
1222
+ const name = args[1];
1223
+ if (!name) {
1224
+ console.log("\n" + label.system + " " + colors.warning("Usage: /cmd remove <name>") + "\n");
1225
+ return;
1226
+ }
1227
+
1228
+ const custom = ctx.aiConfig.CUSTOM_COMMANDS || {};
1229
+ if (!custom[name]) {
1230
+ console.log("\n" + label.system + " " + colors.warning(`No custom command named "${name}" exists.`) + "\n");
1231
+ return;
1232
+ }
1233
+
1234
+ delete custom[name];
1235
+ await setConfigValue("CUSTOM_COMMANDS", custom);
1236
+ ctx.aiConfig.CUSTOM_COMMANDS = custom; // sync context
1237
+
1238
+ console.log("\n" + label.system + " " + colors.success(`✓ Removed custom command: "${name}"\n`));
1239
+ return;
1240
+ }
1241
+
1242
+ console.log("\n" + label.system + " " + colors.warning("Usage: /cmd <list|add|remove> [args]"));
1243
+ console.log(" " + colors.muted("Type /help for help or /cmd list to see existing shortcuts.\n"));
1244
+ }
1245
+
1246
+ /**
1247
+ * Extracts all code blocks from a markdown string.
1248
+ */
1249
+ function extractCodeBlocks(markdown) {
1250
+ const regex = /```[\w-]*\n([\s\S]*?)\n```/g;
1251
+ const blocks = [];
1252
+ let match;
1253
+ while ((match = regex.exec(markdown)) !== null) {
1254
+ blocks.push(match[1]);
1255
+ }
1256
+ return blocks;
1257
+ }
1258
+
1259
+ /**
1260
+ * Manual file writing command. Extracts the last code block of the previous
1261
+ * assistant response and writes it to a file.
1262
+ */
1263
+ async function handleWriteFile(args, ctx) {
1264
+ const filename = args.join(" ");
1265
+ if (!filename) {
1266
+ console.log("\n" + label.system + " " + colors.warning("Usage: /write <filename>") + "\n");
1267
+ return;
1268
+ }
1269
+
1270
+ const lastResponse = [...ctx.history].reverse().find((h) => h.role === "assistant");
1271
+ if (!lastResponse) {
1272
+ console.log("\n" + label.system + " " + colors.muted("No assistant response available to write.\n"));
1273
+ return;
1274
+ }
1275
+
1276
+ const codeBlocks = extractCodeBlocks(lastResponse.content);
1277
+ if (codeBlocks.length === 0) {
1278
+ console.log("\n" + label.system + " " + colors.warning("No code blocks found in the last response.\n"));
1279
+ return;
1280
+ }
1281
+
1282
+ const blockContent = codeBlocks[codeBlocks.length - 1];
1283
+ const filepath = resolve(filename);
1284
+
1285
+ try {
1286
+ const { dirname } = await import("node:path");
1287
+ const { mkdir } = await import("node:fs/promises");
1288
+ const dir = dirname(filepath);
1289
+ await mkdir(dir, { recursive: true });
1290
+ await writeFile(filepath, blockContent, "utf-8");
1291
+ console.log("\n" + label.system + " " + colors.success(`✓ Code block successfully written to: ${filepath}\n`));
1292
+ } catch (err) {
1293
+ console.log("\n" + label.error + " " + colors.danger(`Write failed: ${err.message}\n`));
1294
+ }
1295
+ }
1296
+
1297
+ /**
1298
+ * Interactive git commit command inside chat loop.
1299
+ */
1300
+ async function handleCommitInsideChat(ctx) {
1301
+ const { getGitDiff, runGitCommit } = await import("./git.js");
1302
+ const { exec } = await import("node:child_process");
1303
+ const { promisify } = await import("node:util");
1304
+ const execAsync = promisify(exec);
1305
+
1306
+ try {
1307
+ const { diff, isStaged } = await getGitDiff();
1308
+ if (!diff) {
1309
+ console.log("\n" + label.system + " " + colors.warning("No staged or unstaged changes detected. Stage your files using 'git add' first.\n"));
1310
+ return;
1311
+ }
1312
+
1313
+ if (!isStaged) {
1314
+ ctx.rl.pause();
1315
+ const stageAnswer = await new Promise((resolve) => {
1316
+ ctx.rl.question(colors.warning("\nNo staged changes found. Do you want to stage all changes automatically? [y/N]: "), resolve);
1317
+ });
1318
+ ctx.rl.resume();
1319
+
1320
+ if (stageAnswer.toLowerCase().trim() === "y" || stageAnswer.toLowerCase().trim() === "yes") {
1321
+ await execAsync("git add .");
1322
+ console.log(label.system + " " + colors.success("Staged all changes successfully."));
1323
+ } else {
1324
+ console.log("\n" + label.system + " " + colors.muted("Aborted. Please stage files using 'git add' first.\n"));
1325
+ return;
1326
+ }
1327
+ }
1328
+
1329
+ console.log("\n" + label.system + " " + colors.brand("Reading git diff and generating conventional commit message..."));
1330
+ console.log("");
1331
+
1332
+ const systemPrompt = "You are an expert developer assistant. Generate a concise, clear, and professional conventional commit message (e.g., 'feat: add login page', 'fix: resolve buffer overflow') based on the provided git diff. Output ONLY the commit message itself on a single line, with absolutely no backticks, markdown, explanations, prefix, or introductory text.";
1333
+ const userPrompt = `Here is the git diff:\n\n${diff}`;
1334
+
1335
+ let firstToken = true;
1336
+ let commitMessage = "";
1337
+ const onToken = (token) => {
1338
+ if (firstToken) {
1339
+ firstToken = false;
1340
+ process.stdout.write(label.aether + " Suggested Commit Message: " + colors.success(token));
1341
+ } else {
1342
+ process.stdout.write(colors.success(token));
1343
+ }
1344
+ commitMessage += token;
1345
+ };
1346
+
1347
+ const result = await routePrompt(userPrompt, systemPrompt, ctx.aiConfig, onToken);
1348
+ console.log("\n");
1349
+
1350
+ const cleanMessage = result.text.trim().replace(/^`+|`+$/g, ""); // strip quotes/backticks
1351
+
1352
+ ctx.rl.pause();
1353
+ const answer = await new Promise((resolve) => {
1354
+ ctx.rl.question(colors.muted("Commit with this message? [Y/n]: "), resolve);
1355
+ });
1356
+ ctx.rl.resume();
1357
+
1358
+ if (answer.toLowerCase().trim() === "n" || answer.toLowerCase().trim() === "no") {
1359
+ console.log("\n" + label.system + " " + colors.muted("Commit aborted.\n"));
1360
+ return;
1361
+ }
1362
+
1363
+ console.log("\n" + label.system + " " + colors.brand("Executing git commit..."));
1364
+ const output = await runGitCommit(cleanMessage);
1365
+ console.log("\n" + colors.success(output) + "\n");
1366
+
1367
+ } catch (err) {
1368
+ console.log("\n" + label.error + " " + colors.danger(err.message) + "\n");
1369
+ }
1370
+ }
1371
+
1372
+ /**
1373
+ * Sandboxed interactive shell command execution.
1374
+ */
1375
+ async function handleRunCommand(args, ctx) {
1376
+ const command = args.join(" ").trim();
1377
+ if (!command) {
1378
+ console.log("\n" + label.system + " " + colors.warning("Usage: /run <command>\n"));
1379
+ return;
1380
+ }
1381
+
1382
+ const { spawn } = await import("node:child_process");
1383
+
1384
+ console.log("\n" + label.system + " " + colors.brand(`Running command: ${command}`));
1385
+ console.log(separator("─") + "\n");
1386
+
1387
+ ctx.rl.pause();
1388
+
1389
+ return new Promise((resolve) => {
1390
+ const isWindows = process.platform === "win32";
1391
+ const shell = isWindows ? "cmd.exe" : "/bin/sh";
1392
+ const shellArgs = isWindows ? ["/c", command] : ["-c", command];
1393
+
1394
+ const child = spawn(shell, shellArgs, { stdio: "inherit" });
1395
+
1396
+ child.on("close", (code) => {
1397
+ ctx.rl.resume();
1398
+ console.log("\n" + separator("─"));
1399
+ if (code === 0) {
1400
+ console.log(label.system + " " + colors.success(`✓ Command exited successfully (code 0).\n`));
1401
+ } else {
1402
+ console.log(label.system + " " + colors.danger(`✗ Command failed with exit status ${code}.\n`));
1403
+ }
1404
+ resolve();
1405
+ });
1406
+
1407
+ child.on("error", (err) => {
1408
+ ctx.rl.resume();
1409
+ console.log("\n" + label.error + " " + colors.danger(`Failed to start command: ${err.message}\n`));
1410
+ resolve();
1411
+ });
1412
+ });
1413
+ }
1414
+
1415
+ /**
1416
+ * Interactive display of session token usage statistics.
1417
+ */
1418
+ async function handleTokensDisplay(ctx) {
1419
+ const stats = getSessionTokenStats();
1420
+ const breakdown = getBreakdownByModel();
1421
+
1422
+ console.log("\n" + separator("━"));
1423
+ console.log(colors.accent.bold(" ★ AETHER SESSION TOKEN TELEMETRY ★"));
1424
+ console.log(separator("─"));
1425
+
1426
+ const models = Object.keys(breakdown);
1427
+ if (models.length === 0) {
1428
+ console.log(colors.muted(" No queries executed in this session yet."));
1429
+ } else {
1430
+ // Print header
1431
+ console.log(
1432
+ colors.brand(" " + "Model".padEnd(35) + "Prompt".padStart(10) + "Completion".padStart(12) + "Total".padStart(10))
1433
+ );
1434
+ console.log(colors.dim(" " + "─".repeat(67)));
1435
+ for (const [model, data] of Object.entries(breakdown)) {
1436
+ const truncatedModel = model.length > 33 ? model.slice(0, 30) + "..." : model;
1437
+ console.log(
1438
+ " " + colors.text(truncatedModel.padEnd(35)) +
1439
+ colors.brand(data.prompt.toLocaleString().padStart(10)) +
1440
+ colors.brand(data.completion.toLocaleString().padStart(12)) +
1441
+ colors.accent.bold(data.total.toLocaleString().padStart(10))
1442
+ );
1443
+ }
1444
+ }
1445
+
1446
+ console.log(separator("─"));
1447
+ console.log(" " + colors.accent("Total Exchanges:") + colors.text(` ${stats.exchanges}`));
1448
+ console.log(" " + colors.accent("Total Tokens:") + colors.text(` Prompt: ${stats.prompt.toLocaleString()} | Completion: ${stats.completion.toLocaleString()} | Sum: `) + colors.brand.bold(stats.total.toLocaleString()));
1449
+ console.log(separator("━") + "\n");
1450
+ }
1451
+
1452
+ /**
1453
+ * Streams an AI query prompt and prints telemetry details at the end.
1454
+ */
1455
+ async function executeAISpecialCommand(prompt, specialLabel, ctx) {
1456
+ const systemPrompt = ctx.currentMode.systemPrompt + "\n" + AGENT_INSTRUCTIONS;
1457
+ let hasStarted = false;
1458
+ let responseText = "";
1459
+ const queryStartTime = Date.now();
1460
+ let firstTokenTime = 0;
1461
+
1462
+ const onToken = (token) => {
1463
+ if (!hasStarted) {
1464
+ hasStarted = true;
1465
+ firstTokenTime = Date.now();
1466
+ process.stdout.write("\n" + label.aether + " " + colors.accent(specialLabel) + "\n" + separator("─") + "\n\n");
1467
+ }
1468
+ process.stdout.write(colors.success(token));
1469
+ responseText += token;
1470
+ };
1471
+
1472
+ const result = await routePrompt(prompt, systemPrompt, ctx.aiConfig, onToken);
1473
+ console.log("\n");
1474
+
1475
+ const elapsedSec = ((Date.now() - queryStartTime) / 1000).toFixed(1);
1476
+ let speedText = "";
1477
+ if (firstTokenTime > 0) {
1478
+ const streamElapsed = (Date.now() - firstTokenTime) / 1000;
1479
+ if (streamElapsed > 0.05) {
1480
+ const estimatedTokens = Math.max(1, Math.round(responseText.length / 4));
1481
+ const tps = (estimatedTokens / streamElapsed).toFixed(1);
1482
+ speedText = ` • ${tps} tok/s`;
1483
+ }
1484
+ }
1485
+
1486
+ const showTokens = ctx.aiConfig.SHOW_TOKENS !== "false";
1487
+ let tokensText = "";
1488
+ if (showTokens && result.usage) {
1489
+ const { promptTokens, completionTokens } = result.usage;
1490
+ tokensText = ` • ${promptTokens.toLocaleString()} in / ${completionTokens.toLocaleString()} out tokens`;
1491
+ }
1492
+
1493
+ console.log(separator("─"));
1494
+ console.log(
1495
+ " " + colors.dim(`Node ${result.node} ${result.provider}`) +
1496
+ (result.model ? colors.dim(` ${result.model}`) : "") +
1497
+ colors.dim(` • ${elapsedSec}s${speedText}`) +
1498
+ colors.dim(tokensText)
1499
+ );
1500
+ console.log("");
1501
+ }
1502
+
1503
+ /**
1504
+ * Handler for the /review command (git diff analysis).
1505
+ */
1506
+ async function handleReviewCommand(ctx) {
1507
+ console.log("\n" + label.system + " " + colors.muted("Running git diff to fetch repository changes..."));
1508
+ try {
1509
+ const { diff, isStaged } = await getGitDiff();
1510
+ if (!diff) {
1511
+ console.log(label.system + " " + colors.success("✓ No changes detected in the repository to review.\n"));
1512
+ return;
1513
+ }
1514
+
1515
+ const specialLabel = `Reviewing ${isStaged ? "staged" : "unstaged"} changes...`;
1516
+ const prompt = `Review the following git diff. Identify potential bugs, logical issues, security concerns, performance problems, and recommend optimization or code cleanup. Keep it concise, practical, and highly technical:\n\n\`\`\`diff\n${diff}\n\`\`\``;
1517
+
1518
+ await executeAISpecialCommand(prompt, specialLabel, ctx);
1519
+ } catch (err) {
1520
+ console.log(label.system + " " + colors.danger(`Error: ${err.message}\n`));
1521
+ }
1522
+ }
1523
+
1524
+ /**
1525
+ * Handler for the /diagnose command (build & test diagnostics execution).
1526
+ */
1527
+ async function handleDiagnoseCommand(args, ctx) {
1528
+ const defaultCmd = ctx.aiConfig.DIAGNOSE_CMD || "npm test";
1529
+ const cmdToRun = args.join(" ").trim() || defaultCmd;
1530
+
1531
+ console.log("\n" + label.system + " " + colors.muted(`Running diagnostics command: "${cmdToRun}"...`));
1532
+
1533
+ const spinner = createSpinner("Executing diagnostics").start();
1534
+ try {
1535
+ const { exec } = await import("node:child_process");
1536
+ const { promisify } = await import("node:util");
1537
+ const execAsync = promisify(exec);
1538
+ await execAsync(cmdToRun);
1539
+ spinner.succeed("Diagnostics complete!");
1540
+ console.log("\n" + label.system + " " + colors.success("✓ Diagnostics clean! Build and tests passed successfully.\n"));
1541
+ } catch (err) {
1542
+ spinner.fail("Diagnostics failed!");
1543
+
1544
+ const output = (err.stdout || "") + "\n" + (err.stderr || "");
1545
+ console.log("\n" + label.system + " " + colors.warning(`Diagnostics returned exit code ${err.code}.`));
1546
+ console.log(colors.muted("Analyzing compiler/test output logs...\n"));
1547
+
1548
+ const prompt = `The diagnostics command "${cmdToRun}" failed with exit code ${err.code}. Analyze the following stdout and stderr logs to determine the root cause, identify the files/lines causing the failure, and provide a step-by-step resolution and debugging plan:\n\n\`\`\`\n${output.slice(0, 15000)}\n\`\`\``;
1549
+
1550
+ await executeAISpecialCommand(prompt, "Analyzing diagnostics logs...", ctx);
1551
+ }
1552
+ }
1553
+
1554
+ /**
1555
+ * Handler for file analysis commands: /explain, /refactor, /bug, /doc, /translate.
1556
+ */
1557
+ async function handleFileAICommand(cmdName, args, ctx) {
1558
+ const filePath = args[0];
1559
+ if (!filePath) {
1560
+ console.log("\n" + label.system + " " + colors.warning(`Usage: ${cmdName} <file_path>\n`));
1561
+ return;
1562
+ }
1563
+
1564
+ // Resolve path
1565
+ const resolvedPath = resolve(process.cwd(), filePath);
1566
+
1567
+ // Verify path is inside the workspace
1568
+ const { isInsideWorkspace } = await import("./agent.js");
1569
+ if (!isInsideWorkspace(resolvedPath)) {
1570
+ console.log("\n" + label.system + " " + colors.danger("Error: Path is outside the current workspace sandbox.\n"));
1571
+ return;
1572
+ }
1573
+
1574
+ if (!existsSync(resolvedPath)) {
1575
+ console.log("\n" + label.system + " " + colors.danger(`Error: File does not exist at "${filePath}"\n`));
1576
+ return;
1577
+ }
1578
+
1579
+ const stat = statSync(resolvedPath);
1580
+ if (stat.isDirectory()) {
1581
+ console.log("\n" + label.system + " " + colors.danger(`Error: "${filePath}" is a directory. File path required.\n`));
1582
+ return;
1583
+ }
1584
+
1585
+ if (stat.size > 150 * 1024) { // 150KB limit
1586
+ console.log("\n" + label.system + " " + colors.warning(`Warning: File "${filePath}" is too large (${Math.round(stat.size / 1024)}KB). Limits are 150KB to protect context limit.\n`));
1587
+ return;
1588
+ }
1589
+
1590
+ // Read file content
1591
+ let content;
1592
+ try {
1593
+ const { parseFile } = await import("./file-parser.js");
1594
+ const parsed = await parseFile(resolvedPath);
1595
+ content = parsed.content;
1596
+ } catch (err) {
1597
+ console.log("\n" + label.system + " " + colors.danger(`Error parsing file: ${err.message}\n`));
1598
+ return;
1599
+ }
1600
+
1601
+ let prompt = "";
1602
+ let labelText = "";
1603
+
1604
+ switch (cmdName.toLowerCase()) {
1605
+ case "/explain":
1606
+ labelText = `Explaining ${filePath}...`;
1607
+ prompt = `Explain the architecture, design patterns, logic flow, and purpose of the following code. Be clear, technical, and structured:\n\n\`\`\`\n${content}\n\`\`\``;
1608
+ break;
1609
+ case "/refactor":
1610
+ labelText = `Refactoring ${filePath}...`;
1611
+ prompt = `Suggest refactoring improvements for the following code. Focus on clean code design principles, optimization, readability, reducing complexity, and fixing potential logic bugs. Return both the refactored code block and explanations:\n\n\`\`\`\n${content}\n\`\`\``;
1612
+ break;
1613
+ case "/bug":
1614
+ labelText = `Auditing bugs in ${filePath}...`;
1615
+ prompt = `Perform a thorough static analysis and code review of the following code. Identify potential logical bugs, race conditions, edge case failures, performance bottlenecks, and security hazards. Suggest fixes:\n\n\`\`\`\n${content}\n\`\`\``;
1616
+ break;
1617
+ case "/doc":
1618
+ labelText = `Generating documentation for ${filePath}...`;
1619
+ prompt = `Generate comprehensive API documentation, JSDoc/docstrings, and comments for the following code. Ensure code parameters, return values, and types are documented:\n\n\`\`\`\n${content}\n\`\`\``;
1620
+ break;
1621
+ case "/translate":
1622
+ const targetLang = args[1];
1623
+ if (!targetLang) {
1624
+ console.log("\n" + label.system + " " + colors.warning(`Usage: /translate <file_path> <target_language>\n`));
1625
+ return;
1626
+ }
1627
+ labelText = `Translating ${filePath} to ${targetLang}...`;
1628
+ prompt = `Translate the following code into ${targetLang}. Return a clean, syntactically correct, and beautifully structured code block of the translated code:\n\n\`\`\`\n${content}\n\`\`\``;
1629
+ break;
1630
+ }
1631
+
1632
+ try {
1633
+ await executeAISpecialCommand(prompt, labelText, ctx);
1634
+ } catch (err) {
1635
+ console.log("\n" + label.system + " " + colors.danger(`Error: ${err.message}\n`));
1636
+ }
1637
+ }
1638
+
1639
+ /**
1640
+ * Handler for the /search command (workspace file crawler and AI semantic finder).
1641
+ */
1642
+ async function handleSearchCommand(args, ctx) {
1643
+ const isAi = args[0] === "--ai";
1644
+ const queryArgs = isAi ? args.slice(1) : args;
1645
+ const query = queryArgs.join(" ").trim();
1646
+
1647
+ if (!query) {
1648
+ console.log("\n" + label.system + " " + colors.warning("Usage: /search [--ai] <query_string>\n"));
1649
+ return;
1650
+ }
1651
+
1652
+ const { workspaceSearch, crawlDirectory } = await import("./search.js");
1653
+
1654
+ if (isAi) {
1655
+ console.log("\n" + label.system + " " + colors.muted("Scanning workspace project tree for semantic search..."));
1656
+ const files = crawlDirectory(process.cwd());
1657
+ const { relative } = await import("node:path");
1658
+ const relativePaths = files.map((f) => relative(process.cwd(), f).replace(/\\/g, "/"));
1659
+
1660
+ // Construct semantic prompt
1661
+ const prompt = `Here is the directory structure / file listing of the current workspace:\n\n${relativePaths.slice(0, 100).join("\n")}\n\nBased on this file listing, identify and explain where the following logic or system is implemented, listing the relevant files: ${query}`;
1662
+
1663
+ await executeAISpecialCommand(prompt, `Semantic search: "${query}"`, ctx);
1664
+ return;
1665
+ }
1666
+
1667
+ console.log("\n" + label.system + " " + colors.muted(`Searching workspace for "${query}"...`));
1668
+ const results = workspaceSearch(query);
1669
+
1670
+ if (results.length === 0) {
1671
+ console.log("\n" + label.system + " " + colors.warning(`✓ No matches found for "${query}" in workspace.\n`));
1672
+ return;
1673
+ }
1674
+
1675
+ console.log("\n" + separator("━"));
1676
+ console.log(colors.accent.bold(` ★ WORKSPACE SEARCH RESULTS FOR "${query.toUpperCase()}" ★`));
1677
+ console.log(separator("─"));
1678
+
1679
+ // Print header
1680
+ console.log(
1681
+ colors.brand(" " + "File Path".padEnd(45) + "Line".padStart(6) + " " + "Preview")
1682
+ );
1683
+ console.log(colors.dim(" " + "─".repeat(80)));
1684
+
1685
+ // Display matches (limit to top 50 to prevent terminal overflow)
1686
+ const displayLimit = 50;
1687
+ const visibleResults = results.slice(0, displayLimit);
1688
+
1689
+ for (const match of visibleResults) {
1690
+ const truncatedPath = match.relativePath.length > 43 ? "..." + match.relativePath.slice(-40) : match.relativePath;
1691
+ const truncatedLine = match.lineContent.length > 50 ? match.lineContent.slice(0, 47) + "..." : match.lineContent;
1692
+ console.log(
1693
+ " " + colors.text(truncatedPath.padEnd(45)) +
1694
+ colors.brand(match.lineNumber.toString().padStart(6)) +
1695
+ " " + colors.muted(truncatedLine)
1696
+ );
1697
+ }
1698
+
1699
+ console.log(separator("─"));
1700
+ if (results.length > displayLimit) {
1701
+ console.log(" " + colors.warning(`⚠ Showing first ${displayLimit} of ${results.length} total matches.`));
1702
+ } else {
1703
+ console.log(" " + colors.success(`✓ Found ${results.length} matches across the workspace.`));
1704
+ }
1705
+ console.log(separator("━") + "\n");
1706
+ }
1707
+
1708
+ /**
1709
+ * Runs an autonomous, self-correcting debug/test feedback loop.
1710
+ */
1711
+ export async function handleAutopilotDebug(cmdArg, ctx) {
1712
+ const { exec } = await import("node:child_process");
1713
+ const { promisify } = await import("node:util");
1714
+ const execAsync = promisify(exec);
1715
+
1716
+ const testCmd = cmdArg.trim() || ctx.aiConfig.DIAGNOSE_CMD || "npm test";
1717
+
1718
+ console.log("\n" + label.system + " " + colors.brand("🤖 AUTOPILOT AUTONOMOUS DEBUG LOOP"));
1719
+ console.log(separator("─"));
1720
+ console.log(keyValue(" Diagnostic Command", testCmd));
1721
+ console.log("");
1722
+
1723
+ console.log(colors.cyan(`⚡ Running initial diagnostics: ${testCmd}`));
1724
+
1725
+ let stdout = "";
1726
+ let stderr = "";
1727
+ let passed = false;
1728
+ let runErr = null;
1729
+
1730
+ try {
1731
+ const res = await execAsync(testCmd);
1732
+ stdout = res.stdout;
1733
+ stderr = res.stderr;
1734
+ passed = true;
1735
+ } catch (err) {
1736
+ stdout = err.stdout || "";
1737
+ stderr = err.stderr || "";
1738
+ runErr = err;
1739
+ passed = false;
1740
+ }
1741
+
1742
+ if (passed) {
1743
+ console.log("\n" + label.system + " " + colors.success(`✓ Diagnostics passed successfully on the first run!\n`));
1744
+ return;
1745
+ }
1746
+
1747
+ console.log("\n" + label.system + " " + colors.danger(`❌ Initial run failed. Starting self-correcting debug loop...\n`));
1748
+
1749
+ let iteration = 1;
1750
+ const maxIterations = 3;
1751
+ let currentPrompt = `
1752
+ The test/build command "${testCmd}" failed.
1753
+ Here is the execution output:
1754
+ --- STDOUT ---
1755
+ ${stdout}
1756
+ --- STDERR ---
1757
+ ${stderr}
1758
+ --- ERROR ---
1759
+ ${runErr ? runErr.message : ""}
1760
+
1761
+ Please inspect the logs. If you need to read any files first to locate the bug, use the [READ_FILE: path] tool. If you know how to fix it, write the corrected files using [WRITE_FILE: path]...[END_WRITE].
1762
+ After you output your edits or read operations, we will apply them and re-run the command.
1763
+ `;
1764
+
1765
+ const debugSystemPrompt = `
1766
+ You are Aether Autopilot in Autonomous Debug Mode.
1767
+ A terminal command failed. Your goal is to analyze the error logs, read relevant source files to find the bug, write fixes to those files, and make sure the diagnostics pass.
1768
+ You can read files using: [READ_FILE: path/to/file]
1769
+ You can write files using:
1770
+ [WRITE_FILE: path/to/file]
1771
+ <new file content>
1772
+ [END_WRITE]
1773
+
1774
+ Rules:
1775
+ - You must identify the root cause of the error.
1776
+ - First, read the relevant file(s) that might have caused the error.
1777
+ - Then, output the corrected file content.
1778
+ - Do not run any command blocks yourself. The environment will automatically re-run the test command for you after you output your modifications.
1779
+ - Keep your changes minimal and target only the bug.
1780
+ `;
1781
+
1782
+ while (iteration <= maxIterations) {
1783
+ console.log(colors.accent(`\n🤖 [Autopilot Debug - Iteration ${iteration}/${maxIterations}]`));
1784
+
1785
+ const spinner = createSpinner(colors.muted(`Aether analyzing diagnostics & planning fixes...`));
1786
+ spinner.start();
1787
+
1788
+ let streamedText = "";
1789
+ let hasStartedStreaming = false;
1790
+ const filter = new StreamFilter(process.stdout.write.bind(process.stdout));
1791
+ const onToken = (token) => {
1792
+ if (!hasStartedStreaming) {
1793
+ hasStartedStreaming = true;
1794
+ spinner.stop();
1795
+ }
1796
+ filter.write(token);
1797
+ streamedText += token;
1798
+ };
1799
+
1800
+ let result;
1801
+ try {
1802
+ result = await routePrompt(currentPrompt, debugSystemPrompt, ctx.aiConfig, onToken, ctx.history);
1803
+ spinner.stop();
1804
+ filter.flush();
1805
+ } catch (routeErr) {
1806
+ spinner.stop();
1807
+ console.log("\n" + label.error + " " + colors.danger(`AI Routing Failed: ${routeErr.message}`));
1808
+ break;
1809
+ }
1810
+
1811
+ if (hasStartedStreaming) {
1812
+ clearStreamedText(filter.filteredText);
1813
+ }
1814
+
1815
+ console.log("");
1816
+ console.log(label.aether + " " + providerBadge(result));
1817
+ console.log(separator("─"));
1818
+ console.log("");
1819
+
1820
+ const rendered = getMarked().parse(result.text);
1821
+ console.log(rendered);
1822
+ console.log(separator("─"));
1823
+
1824
+ ctx.history.push({ role: "user", content: currentPrompt, timestamp: new Date() });
1825
+ ctx.history.push({
1826
+ role: "assistant",
1827
+ content: result.text,
1828
+ provider: result.provider,
1829
+ model: result.model,
1830
+ node: result.node,
1831
+ timestamp: new Date(),
1832
+ });
1833
+ await saveHistory(ctx.history, ctx.currentMode.name);
1834
+
1835
+ const { processAgentBlocks } = await import("./agent.js");
1836
+ const toolResults = await processAgentBlocks(result.text, ctx.aiConfig, ctx.rl);
1837
+
1838
+ console.log(colors.cyan(`\n⚡ Re-running diagnostic command (Attempt ${iteration}/${maxIterations}): ${testCmd}`));
1839
+
1840
+ let testStdout = "";
1841
+ let testStderr = "";
1842
+ let testPassed = false;
1843
+ let testRunErr = null;
1844
+
1845
+ try {
1846
+ const res = await execAsync(testCmd);
1847
+ testStdout = res.stdout;
1848
+ testStderr = res.stderr;
1849
+ testPassed = true;
1850
+ } catch (err) {
1851
+ testStdout = err.stdout || "";
1852
+ testStderr = err.stderr || "";
1853
+ testRunErr = err;
1854
+ testPassed = false;
1855
+ }
1856
+
1857
+ if (testPassed) {
1858
+ console.log("\n" + label.system + " " + colors.success(`✓ Diagnostics passed successfully after autopilot debug corrections!\n`));
1859
+ break;
1860
+ } else {
1861
+ console.log("\n" + label.system + " " + colors.danger(`❌ Diagnostic check still failing (Attempt ${iteration}/${maxIterations}).`));
1862
+
1863
+ let toolOutputs = "### Agent Tool Outputs:\n";
1864
+ for (const tr of toolResults) {
1865
+ if (tr.success) {
1866
+ if (tr.tool === "READ_FILE") {
1867
+ toolOutputs += `\n- READ_FILE "${tr.arg}" succeeded. Content:\n\`\`\`\n${tr.content}\n\`\`\`;`;
1868
+ } else if (tr.tool === "WRITE_FILE") {
1869
+ toolOutputs += `\n- WRITE_FILE "${tr.arg}" succeeded.`;
1870
+ } else if (tr.tool === "SEARCH_WEB") {
1871
+ const list = tr.results.map((r, i) => `${i+1}. [${r.title}](${r.url})\n ${r.snippet}`).join("\n");
1872
+ toolOutputs += `\n- SEARCH_WEB "${tr.arg}" succeeded. Results:\n${list}`;
1873
+ }
1874
+ } else {
1875
+ toolOutputs += `\n- ${tr.tool} "${tr.arg}" failed: ${tr.error}`;
1876
+ }
1877
+ }
1878
+
1879
+ currentPrompt = `
1880
+ ${toolOutputs}
1881
+
1882
+ The test/build command "${testCmd}" is still failing.
1883
+ Here is the new execution output:
1884
+ --- STDOUT ---
1885
+ ${testStdout}
1886
+ --- STDERR ---
1887
+ ${testStderr}
1888
+ --- ERROR ---
1889
+ ${testRunErr ? testRunErr.message : ""}
1890
+
1891
+ Please analyze the remaining issues, read any other files you need, and apply further fixes.
1892
+ `;
1893
+ iteration++;
1894
+ }
1895
+ }
1896
+
1897
+ if (iteration > maxIterations) {
1898
+ console.log("\n" + label.system + " " + colors.warning(`⚠️ Max autopilot debug iterations reached. Review the diagnostics manually.\n`));
1899
+ }
1900
+ }
1901
+
1902
+ /**
1903
+ * Renders the custom interactive Git TUI file stager and branch tree.
1904
+ */
1905
+ export async function handleGitTUI(ctx) {
1906
+ const { execSync } = await import("node:child_process");
1907
+
1908
+ try {
1909
+ execSync("git rev-parse --is-inside-work-tree", { stdio: "ignore" });
1910
+ } catch (e) {
1911
+ console.log("\n" + label.error + " " + colors.danger("Not a git repository (or git is not installed).\n"));
1912
+ return;
1913
+ }
1914
+
1915
+ const stdin = process.stdin;
1916
+ const stdout = process.stdout;
1917
+ const wasRaw = stdin.isRaw;
1918
+
1919
+ stdin.setRawMode(true);
1920
+ stdin.resume();
1921
+ stdin.setEncoding("utf8");
1922
+ stdout.write("\x1b[?25l"); // Hide cursor
1923
+
1924
+ let files = getGitStatusFiles();
1925
+ let activeIndex = 0;
1926
+ let renderedLines = 0;
1927
+
1928
+ function getGitStatusFiles() {
1929
+ try {
1930
+ const out = execSync("git status --porcelain", { encoding: "utf8" }).trim();
1931
+ if (!out) return [];
1932
+ return out.split("\n").map(line => {
1933
+ const status = line.slice(0, 2);
1934
+ const file = line.slice(3).trim();
1935
+ const isStaged = status[0] !== " " && status[0] !== "?";
1936
+ const isUnstaged = status[1] !== " " || status[0] === "?";
1937
+ return {
1938
+ path: file,
1939
+ status,
1940
+ staged: isStaged,
1941
+ unstaged: isUnstaged
1942
+ };
1943
+ });
1944
+ } catch (e) {
1945
+ return [];
1946
+ }
1947
+ }
1948
+
1949
+ function render() {
1950
+ if (renderedLines > 0) {
1951
+ stdout.write(`\x1b[${renderedLines}A\x1b[J`);
1952
+ }
1953
+
1954
+ let lines = [];
1955
+ lines.push(colors.brand("🌿 AETHER INTERACTIVE GIT TUI"));
1956
+ lines.push(separator("─"));
1957
+
1958
+ let branchGraph = "";
1959
+ try {
1960
+ branchGraph = execSync("git log --graph --oneline --decorate -n 6", { encoding: "utf8" }).trim();
1961
+ } catch (e) {
1962
+ branchGraph = " No git history found.";
1963
+ }
1964
+
1965
+ lines.push(colors.accent("Commit Graph & History:"));
1966
+ if (branchGraph) {
1967
+ lines.push(branchGraph.split("\n").map(l => " " + colors.muted(l)).join("\n"));
1968
+ }
1969
+ lines.push(separator("─"));
1970
+
1971
+ lines.push(colors.accent("Modified Files:"));
1972
+
1973
+ if (files.length === 0) {
1974
+ lines.push(colors.success(" Clean working directory. Nothing to stage/commit."));
1975
+ } else {
1976
+ files.forEach((file, index) => {
1977
+ const isActive = index === activeIndex;
1978
+ const pointer = isActive ? colors.accent("❯ ") : " ";
1979
+ const checkbox = file.staged ? colors.success("[⬢] ") : colors.muted("[⬡] ");
1980
+
1981
+ let statusColor = colors.text;
1982
+ if (file.status[0] === "?" || file.status[1] === "?") {
1983
+ statusColor = colors.warning;
1984
+ } else if (file.staged && !file.unstaged) {
1985
+ statusColor = colors.success;
1986
+ } else if (file.unstaged) {
1987
+ statusColor = colors.danger;
1988
+ }
1989
+
1990
+ const pathText = isActive ? colors.brand(file.path) : statusColor(file.path);
1991
+ const statusText = colors.dim(`(${file.status})`);
1992
+ lines.push(pointer + checkbox + pathText + " " + statusText);
1993
+ });
1994
+ }
1995
+
1996
+ lines.push(separator("─"));
1997
+ lines.push(colors.muted("Hotkeys: [Space] Stage/Unstage | [D] Discard | [C] Commit | [P] Push | [Q/Esc] Quit"));
1998
+
1999
+ const outputStr = lines.join("\n") + "\n";
2000
+ stdout.write(outputStr);
2001
+ renderedLines = lines.length;
2002
+ }
2003
+
2004
+ render();
2005
+
2006
+ return new Promise((resolve) => {
2007
+ async function handleKey(key) {
2008
+ console.log("TUI_KEY:", JSON.stringify(key));
2009
+ if (key === "\u0003" || key === "q" || key === "Q" || key === "\u001b") {
2010
+ cleanup();
2011
+ resolve();
2012
+ return;
2013
+ }
2014
+
2015
+ if (key === "\u001b[A") { // Up Arrow
2016
+ if (files.length > 0) {
2017
+ activeIndex = (activeIndex - 1 + files.length) % files.length;
2018
+ render();
2019
+ }
2020
+ return;
2021
+ }
2022
+ if (key === "\u001b[B") { // Down Arrow
2023
+ if (files.length > 0) {
2024
+ activeIndex = (activeIndex + 1) % files.length;
2025
+ render();
2026
+ }
2027
+ return;
2028
+ }
2029
+
2030
+ if (key === " ") { // Stage/Unstage
2031
+ if (files.length > 0) {
2032
+ const file = files[activeIndex];
2033
+ try {
2034
+ if (file.staged) {
2035
+ execSync(`git restore --staged "${file.path}"`);
2036
+ } else {
2037
+ execSync(`git add "${file.path}"`);
2038
+ }
2039
+ } catch (err) {
2040
+ // Ignore
2041
+ }
2042
+ files = getGitStatusFiles();
2043
+ if (activeIndex >= files.length) {
2044
+ activeIndex = Math.max(0, files.length - 1);
2045
+ }
2046
+ render();
2047
+ }
2048
+ return;
2049
+ }
2050
+
2051
+ if (key === "d" || key === "D") { // Discard
2052
+ if (files.length > 0) {
2053
+ const file = files[activeIndex];
2054
+ try {
2055
+ if (file.status[0] === "?" || file.status[1] === "?") {
2056
+ const fs = await import("node:fs");
2057
+ fs.rmSync(file.path, { force: true });
2058
+ } else {
2059
+ execSync(`git restore "${file.path}"`);
2060
+ }
2061
+ } catch (err) {
2062
+ // Ignore
2063
+ }
2064
+ files = getGitStatusFiles();
2065
+ if (activeIndex >= files.length) {
2066
+ activeIndex = Math.max(0, files.length - 1);
2067
+ }
2068
+ render();
2069
+ }
2070
+ return;
2071
+ }
2072
+
2073
+ if (key === "c" || key === "C") { // Commit
2074
+ cleanup();
2075
+
2076
+ const hasStaged = files.some(f => f.staged);
2077
+ if (!hasStaged) {
2078
+ console.log("\n" + label.warning + " " + colors.warning("No staged changes to commit. Stage some files first!\n"));
2079
+ await new Promise(r => setTimeout(r, 1500));
2080
+
2081
+ stdin.setRawMode(true);
2082
+ stdin.resume();
2083
+ stdout.write("\x1b[?25l");
2084
+ renderedLines = 0;
2085
+ files = getGitStatusFiles();
2086
+ render();
2087
+ return;
2088
+ }
2089
+
2090
+ ctx.rl.pause();
2091
+ const commitMsg = await new Promise((resMsg) => {
2092
+ ctx.rl.question(
2093
+ colors.accent("\n💬 Enter commit message: "),
2094
+ resMsg
2095
+ );
2096
+ });
2097
+ ctx.rl.resume();
2098
+
2099
+ if (commitMsg.trim()) {
2100
+ try {
2101
+ execSync(`git commit -m "${commitMsg.trim()}"`);
2102
+ console.log("\n" + label.system + " " + colors.success("✓ Changes committed successfully!\n"));
2103
+ } catch (err) {
2104
+ console.log("\n" + label.error + " " + colors.danger("Failed to commit changes: " + err.message + "\n"));
2105
+ }
2106
+ } else {
2107
+ console.log("\n" + label.warning + " " + colors.muted("Commit aborted (empty message).\n"));
2108
+ }
2109
+
2110
+ await new Promise(r => setTimeout(r, 1500));
2111
+
2112
+ stdin.setRawMode(true);
2113
+ stdin.resume();
2114
+ stdout.write("\x1b[?25l");
2115
+ renderedLines = 0;
2116
+ files = getGitStatusFiles();
2117
+ activeIndex = 0;
2118
+ render();
2119
+ return;
2120
+ }
2121
+
2122
+ if (key === "p" || key === "P") { // Push
2123
+ cleanup();
2124
+ console.log("\n" + label.system + " " + colors.brand("🚀 Pushing changes to remote branch..."));
2125
+ try {
2126
+ const out = execSync("git push", { encoding: "utf8" });
2127
+ console.log(colors.muted(out));
2128
+ console.log("\n" + label.system + " " + colors.success("✓ Push completed successfully!\n"));
2129
+ } catch (err) {
2130
+ console.log("\n" + label.error + " " + colors.danger("Failed to push changes: " + err.message + "\n"));
2131
+ }
2132
+
2133
+ await new Promise(r => setTimeout(r, 2000));
2134
+
2135
+ stdin.setRawMode(true);
2136
+ stdin.resume();
2137
+ stdout.write("\x1b[?25l");
2138
+ renderedLines = 0;
2139
+ files = getGitStatusFiles();
2140
+ render();
2141
+ return;
2142
+ }
2143
+ }
2144
+
2145
+ function cleanup() {
2146
+ stdin.removeListener("data", handleKey);
2147
+ stdin.setRawMode(wasRaw);
2148
+ stdin.pause();
2149
+ stdout.write("\x1b[?25h"); // Show cursor
2150
+ }
2151
+
2152
+ stdin.on("data", handleKey);
2153
+ });
2154
+ }
2155
+
2156
+ /**
2157
+ * Handles spawning and launching the local telemetry dashboard.
2158
+ */
2159
+ export async function handleDashboardCommand(ctx) {
2160
+ const { startDashboardServer } = await import("./dashboard.js");
2161
+ const { exec } = await import("node:child_process");
2162
+
2163
+ try {
2164
+ const { port } = await startDashboardServer();
2165
+ console.log("\n" + label.system + " " + colors.brand("📊 AETHER WEB TELEMETRY DASHBOARD"));
2166
+ console.log(separator("─"));
2167
+ console.log(keyValue(" Status", colors.success("ONLINE")));
2168
+ console.log(keyValue(" Local URL", `http://localhost:${port}`));
2169
+ console.log("");
2170
+ console.log(" " + colors.muted("Launching browser companion automatically..."));
2171
+
2172
+ let startCmd = `start http://localhost:${port}`;
2173
+ if (process.platform === "darwin") {
2174
+ startCmd = `open http://localhost:${port}`;
2175
+ } else if (process.platform === "linux") {
2176
+ startCmd = `xdg-open http://localhost:${port}`;
2177
+ }
2178
+ exec(startCmd);
2179
+ console.log("\n" + label.system + " " + colors.success("✓ Dashboard launched. Press Ctrl+C in this session to stop dashboard at exit.\n"));
2180
+ } catch (err) {
2181
+ console.log("\n" + label.error + " " + colors.danger("Failed to start dashboard server: " + err.message + "\n"));
2182
+ }
2183
+ }
2184
+