aether-ai-agent-cli 1.1.4__py3-none-any.whl

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.
@@ -0,0 +1,1018 @@
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
+ getActiveTheme,
24
+ setTheme,
25
+ getThemesList
26
+ } from "./ui/theme.js";
27
+ import { createSpinner } from "./ui/spinner.js";
28
+ import { showBanner } from "./ui/banner.js";
29
+ import { routePrompt } from "./ai/router.js";
30
+ import { getActiveProviders } from "./ai/providers.js";
31
+ import {
32
+ getAIConfig,
33
+ loadHistory,
34
+ saveHistory,
35
+ clearHistory,
36
+ setConfigValue
37
+ } from "./config.js";
38
+ import { MODES, DEFAULT_MODE, getModeByName } from "./modes.js";
39
+ import { parseFile, formatContext } from "./file-parser.js";
40
+ import { runMainframeHack } from "./ai/fallback.js";
41
+
42
+ // Configure marked dynamically for terminal output
43
+ const getMarked = () => new Marked(markedTerminal({
44
+ reflowText: true,
45
+ width: process.stdout.columns ? Math.max(20, process.stdout.columns - 4) : 80,
46
+ showSectionPrefix: false,
47
+ code: (c) => colors.orange(c),
48
+ codespan: (c) => colors.accent3(c),
49
+ heading: (h) => colors.accent.bold(h),
50
+ strong: (s) => colors.magenta.bold(s),
51
+ em: chalk.italic,
52
+ hr: (h) => colors.dim(h),
53
+ }));
54
+
55
+ /**
56
+ * Starts the interactive Aether chat session.
57
+ * @param {{ mode?: string, preferredProvider?: string }} [options={}]
58
+ */
59
+ export async function startChat(options = {}) {
60
+ // Load AI config
61
+ const aiConfig = await getAIConfig();
62
+
63
+ // Set theme from configuration
64
+ const theme = aiConfig.THEME || "cyberpunk";
65
+ setTheme(theme);
66
+
67
+ let currentMode = getModeByName(options.mode) || getModeByName(aiConfig.DEFAULT_MODE) || MODES[DEFAULT_MODE];
68
+ let attachedFiles = [];
69
+
70
+ // Persistent history loader
71
+ const history = await loadHistory();
72
+
73
+ // Mini-game state
74
+ const game = {
75
+ active: false,
76
+ code: "",
77
+ attempts: 0,
78
+ maxAttempts: 6,
79
+ };
80
+
81
+ // Show banner
82
+ showBanner(currentMode.name);
83
+
84
+ // Active providers diagnostic check
85
+ const active = getActiveProviders(aiConfig);
86
+ if (active.length === 0) {
87
+ console.log(
88
+ "\n" + label.system + " " +
89
+ colors.warning("No API keys configured. Using local fallback solvers.") + "\n" +
90
+ " " + colors.muted("Run ") + colors.accent("aether setup") +
91
+ colors.muted(" to configure providers (free options available!).\n")
92
+ );
93
+ } else {
94
+ const providerNames = active.map((a) => a.provider.name);
95
+ const unique = [...new Set(providerNames)];
96
+ console.log(
97
+ label.mesh + " " +
98
+ colors.accent("Failover mesh online: ") +
99
+ colors.text(unique.join(" → ")) +
100
+ colors.muted(" → Krylo fallback")
101
+ );
102
+ console.log(
103
+ " " + colors.dim(`${active.length} node(s) active across ${unique.length} provider(s)`) + "\n"
104
+ );
105
+ }
106
+
107
+ // Display loaded history message if any
108
+ if (history.length > 0) {
109
+ console.log(
110
+ " " + label.info + " " +
111
+ colors.muted(`Restored ${Math.floor(history.length / 2)} message exchanges from persistent logs.`) + "\n"
112
+ );
113
+ }
114
+
115
+ // Completer: handles commands & dynamic local file path autocomplete
116
+ const completer = (line) => {
117
+ const builtIn = [
118
+ "/help", "/mode", "/modes", "/attach", "/files", "/clear",
119
+ "/providers", "/export", "/status", "/copy", "/exit", "/quit",
120
+ "/theme", "/themes", "/history-clear", "/game", "/abort", "/cmd", "/write"
121
+ ];
122
+ const customCmds = aiConfig.CUSTOM_COMMANDS || {};
123
+ const commands = [...builtIn, ...Object.keys(customCmds)];
124
+
125
+ // File path autocompletion on /attach
126
+ if (line.startsWith("/attach ")) {
127
+ const query = line.slice(8);
128
+ const lastSlash = Math.max(query.lastIndexOf("/"), query.lastIndexOf("\\"));
129
+ let searchDir = ".";
130
+ let searchPrefix = query;
131
+
132
+ if (lastSlash !== -1) {
133
+ searchDir = query.slice(0, lastSlash);
134
+ if (searchDir === "") {
135
+ searchDir = sep;
136
+ }
137
+ searchPrefix = query.slice(lastSlash + 1);
138
+ }
139
+
140
+ try {
141
+ const resolved = resolve(searchDir);
142
+ if (existsSync(resolved) && statSync(resolved).isDirectory()) {
143
+ const files = readdirSync(resolved);
144
+ const hits = files
145
+ .filter((f) => f.toLowerCase().startsWith(searchPrefix.toLowerCase()) && !f.startsWith("."))
146
+ .map((f) => {
147
+ const fullPath = searchDir === "." || searchDir === sep ? f : join(searchDir, f);
148
+ const fullResolved = resolve(fullPath);
149
+ const isDir = statSync(fullResolved).isDirectory();
150
+ return `/attach ${fullPath}${isDir ? "/" : ""}`;
151
+ });
152
+ return [hits.length ? hits : [], line];
153
+ }
154
+ } catch (e) {
155
+ // Fallback silently on fs errors
156
+ }
157
+ return [[], line];
158
+ }
159
+
160
+ // Sub-arguments autocomplete on /mode
161
+ if (line.startsWith("/mode ")) {
162
+ const query = line.slice(6).toLowerCase();
163
+ const modesList = Object.keys(MODES);
164
+ const hits = modesList
165
+ .filter((m) => m.startsWith(query))
166
+ .map((m) => `/mode ${m}`);
167
+ return [hits.length ? hits : [], line];
168
+ }
169
+
170
+ // Sub-arguments autocomplete on /theme
171
+ if (line.startsWith("/theme ")) {
172
+ const query = line.slice(7).toLowerCase();
173
+ const themesList = getThemesList();
174
+ const hits = themesList
175
+ .filter((t) => t.startsWith(query))
176
+ .map((t) => `/theme ${t}`);
177
+ return [hits.length ? hits : [], line];
178
+ }
179
+
180
+ // Sub-arguments autocomplete on /cmd
181
+ if (line.startsWith("/cmd ")) {
182
+ const query = line.slice(5).toLowerCase();
183
+ const subcmds = ["list", "add", "remove"];
184
+ const hits = subcmds
185
+ .filter((s) => s.startsWith(query))
186
+ .map((s) => `/cmd ${s}`);
187
+ return [hits.length ? hits : [], line];
188
+ }
189
+
190
+ const hits = commands.filter((c) => c.startsWith(line));
191
+ return [hits.length ? hits : [], line];
192
+ };
193
+
194
+ // Create readline interface
195
+ const rl = createInterface({
196
+ input: process.stdin,
197
+ output: process.stdout,
198
+ prompt: colors.accent(" ❯ "),
199
+ terminal: true,
200
+ completer
201
+ });
202
+
203
+ // Load persistent history entries directly into the shell up/down array
204
+ if (history.length > 0) {
205
+ const userQueries = history
206
+ .filter((h) => h.role === "user")
207
+ .map((h) => h.content);
208
+ // Readline history is structured newest first (index 0)
209
+ rl.history = [...new Set(userQueries)].reverse();
210
+ }
211
+
212
+ // ── AI Execution Helper ──────────────────────────────────
213
+ async function executeAIQuery(promptText, originalInput = promptText) {
214
+ // ── Build Prompt with Context ─────────────────────────
215
+ let fullPrompt = promptText;
216
+ if (attachedFiles.length > 0) {
217
+ const contexts = attachedFiles.map((f) => formatContext(f)).join("\n\n");
218
+ fullPrompt = `${contexts}\n\n${promptText}`;
219
+ }
220
+
221
+ // ── Query AI ──────────────────────────────────────────
222
+ const queryStartTime = Date.now();
223
+ let firstTokenTime = 0;
224
+ const spinner = createSpinner(
225
+ colors.muted(`Routing through mesh ${currentMode.label}...`)
226
+ );
227
+ spinner.start();
228
+
229
+ let hasStartedStreaming = false;
230
+ let streamedText = "";
231
+ const onToken = (token) => {
232
+ if (!hasStartedStreaming) {
233
+ hasStartedStreaming = true;
234
+ firstTokenTime = Date.now();
235
+ spinner.stop();
236
+ }
237
+ process.stdout.write(token);
238
+ streamedText += token;
239
+ };
240
+
241
+ try {
242
+ const result = await routePrompt(fullPrompt, currentMode.systemPrompt, aiConfig, onToken, history);
243
+ spinner.stop();
244
+
245
+ // Store in history
246
+ history.push({ role: "user", content: originalInput, timestamp: new Date() });
247
+ history.push({
248
+ role: "assistant",
249
+ content: result.text,
250
+ provider: result.provider,
251
+ model: result.model,
252
+ node: result.node,
253
+ timestamp: new Date(),
254
+ });
255
+
256
+ // Save to persistent file
257
+ await saveHistory(history);
258
+
259
+ if (hasStartedStreaming) {
260
+ clearStreamedText(streamedText);
261
+ }
262
+
263
+ // Display response
264
+ console.log("");
265
+ console.log(label.aether + " " + providerBadge(result));
266
+ console.log(separator("─"));
267
+ console.log("");
268
+
269
+ if (result.provider === "local" || result.provider === "krylo-fallback") {
270
+ console.log(colors.text(" " + result.text.split("\n").join("\n ")));
271
+ } else {
272
+ let displayText = result.text;
273
+ const cleanedText = displayText.replace(/\[WRITE_FILE:\s*([^\n\]]+)\][\s\S]*?\[END_WRITE\]/g, (match, p1) => {
274
+ return `\n\n${colors.brand("⚡ [File creation request: " + p1 + "]")}\n\n`;
275
+ });
276
+ const rendered = getMarked().parse(cleanedText);
277
+ console.log(rendered);
278
+ }
279
+
280
+ const elapsedSec = ((Date.now() - queryStartTime) / 1000).toFixed(1);
281
+ let speedText = "";
282
+ if (firstTokenTime > 0) {
283
+ const streamElapsed = (Date.now() - firstTokenTime) / 1000;
284
+ if (streamElapsed > 0.05) {
285
+ const estimatedTokens = Math.max(1, Math.round(streamedText.length / 4));
286
+ const tps = (estimatedTokens / streamElapsed).toFixed(1);
287
+ speedText = ` • ${tps} tok/s`;
288
+ }
289
+ }
290
+
291
+ console.log(separator("─"));
292
+ console.log(
293
+ " " + colors.dim(`Node ${result.node} • ${result.provider}`) +
294
+ (result.model ? colors.dim(` • ${result.model}`) : "") +
295
+ colors.dim(` • ${elapsedSec}s${speedText}`) +
296
+ colors.dim(` • ${Math.floor(history.length / 2)} exchanges`)
297
+ );
298
+ console.log("");
299
+
300
+ // Parse file write blocks
301
+ const writeRegex = /\[WRITE_FILE:\s*([^\n\]]+)\]\n([\s\S]*?)\n\[END_WRITE\]/g;
302
+ let match;
303
+ const fileWrites = [];
304
+ while ((match = writeRegex.exec(result.text)) !== null) {
305
+ fileWrites.push({ path: match[1].trim(), content: match[2] });
306
+ }
307
+
308
+ if (fileWrites.length > 0) {
309
+ const { dirname } = await import("node:path");
310
+ const { mkdir } = await import("node:fs/promises");
311
+
312
+ for (const fileWrite of fileWrites) {
313
+ const finalPath = resolve(fileWrite.path);
314
+ console.log("");
315
+ console.log(label.system + " " + colors.warning(`Auto-Writing File: ${colors.accent(finalPath)} (${fileWrite.content.length} bytes)`));
316
+ try {
317
+ const dir = dirname(finalPath);
318
+ await mkdir(dir, { recursive: true });
319
+ await writeFile(finalPath, fileWrite.content, "utf-8");
320
+ console.log(" " + colors.success(`✓ File created successfully!\n`));
321
+ } catch (err) {
322
+ console.log(" " + colors.danger(`✗ Write failed: ${err.message}\n`));
323
+ }
324
+ }
325
+ }
326
+ } catch (err) {
327
+ spinner.fail("Request failed");
328
+ console.log("\n" + label.error + " " + colors.danger(err.message) + "\n");
329
+ }
330
+
331
+ // Sync shell's recall history list
332
+ const userQueries = history
333
+ .filter((h) => h.role === "user")
334
+ .map((h) => h.content);
335
+ rl.history = [...new Set(userQueries)].reverse();
336
+ }
337
+
338
+ rl.prompt();
339
+
340
+ rl.on("line", async (line) => {
341
+ const input = line.trim();
342
+ if (!input) {
343
+ rl.prompt();
344
+ return;
345
+ }
346
+
347
+ // ── Handle Game Input ──────────────────────────────────
348
+ if (game.active && !input.startsWith("/")) {
349
+ handleGuess(input, game);
350
+ rl.prompt();
351
+ return;
352
+ }
353
+
354
+ // ── Handle Slash Commands ──────────────────────────────
355
+ if (input.startsWith("/")) {
356
+ const [cmd, ...args] = input.split(/\s+/);
357
+ const builtInList = [
358
+ "/", "/help", "/mode", "/modes", "/attach", "/files", "/clear",
359
+ "/providers", "/export", "/status", "/copy", "/exit", "/quit",
360
+ "/theme", "/themes", "/history-clear", "/game", "/abort", "/cmd",
361
+ "/guess", "/write"
362
+ ];
363
+
364
+ const customCmds = aiConfig.CUSTOM_COMMANDS || {};
365
+
366
+ if (!builtInList.includes(cmd.toLowerCase()) && customCmds[cmd]) {
367
+ const template = customCmds[cmd];
368
+ const userArg = args.join(" ");
369
+ const rewrittenPrompt = template + (userArg ? " " + userArg : "");
370
+
371
+ console.log("\n" + label.system + " " + colors.accent(`Executing custom command: `) + colors.text(cmd));
372
+ console.log(" " + colors.muted("Prompt: ") + colors.text(rewrittenPrompt) + "\n");
373
+
374
+ await executeAIQuery(rewrittenPrompt, input);
375
+ rl.prompt();
376
+ return;
377
+ }
378
+
379
+ const handled = await handleCommand(input, {
380
+ currentMode,
381
+ attachedFiles,
382
+ history,
383
+ aiConfig,
384
+ game,
385
+ setMode: (mode) => { currentMode = mode; },
386
+ addFile: (file) => { attachedFiles.push(file); },
387
+ clearFiles: () => { attachedFiles = []; },
388
+ rl,
389
+ });
390
+ if (handled !== "exit") {
391
+ rl.prompt();
392
+ }
393
+ return;
394
+ }
395
+
396
+ await executeAIQuery(input);
397
+ rl.prompt();
398
+ });
399
+
400
+ rl.on("close", () => {
401
+ console.log("\n" + label.system + " " + colors.muted("Session terminated. Stay cyberpunk. ⚡\n"));
402
+ process.exit(0);
403
+ });
404
+ }
405
+
406
+ /**
407
+ * Handles slash commands in the chat.
408
+ */
409
+ async function handleCommand(input, ctx) {
410
+ const [cmd, ...args] = input.split(/\s+/);
411
+
412
+ switch (cmd.toLowerCase()) {
413
+ case "/":
414
+ case "/help":
415
+ showHelp(ctx.aiConfig);
416
+ break;
417
+
418
+ case "/mode":
419
+ handleModeSwitch(args, ctx);
420
+ break;
421
+
422
+ case "/modes":
423
+ showModes();
424
+ break;
425
+
426
+ case "/attach":
427
+ await handleAttach(args, ctx);
428
+ break;
429
+
430
+ case "/files":
431
+ showAttachedFiles(ctx.attachedFiles);
432
+ break;
433
+
434
+ case "/clear":
435
+ // Actual screen clear & scrollback reset
436
+ process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
437
+ showBanner(ctx.currentMode.name);
438
+ break;
439
+
440
+ case "/export":
441
+ await handleExport(ctx.history);
442
+ break;
443
+
444
+ case "/status":
445
+ showStatus(ctx);
446
+ break;
447
+
448
+ case "/providers":
449
+ showActiveProviders(ctx.aiConfig);
450
+ break;
451
+
452
+ case "/theme":
453
+ await handleThemeSwitch(args);
454
+ break;
455
+
456
+ case "/themes":
457
+ showThemesList();
458
+ break;
459
+
460
+ case "/history-clear":
461
+ await handleHistoryClear(ctx.history, ctx.rl);
462
+ break;
463
+
464
+ case "/game":
465
+ handleGameStart(ctx.game);
466
+ break;
467
+
468
+ case "/abort":
469
+ handleGameAbort(ctx.game);
470
+ break;
471
+
472
+ case "/guess":
473
+ if (ctx.game.active) {
474
+ handleGuess(args[0] || "", ctx.game);
475
+ } else {
476
+ console.log("\n" + label.system + " " + colors.warning("Game is not active. Type /game to start.\n"));
477
+ }
478
+ break;
479
+
480
+ case "/copy":
481
+ await handleCopy(ctx.history);
482
+ break;
483
+
484
+ case "/cmd":
485
+ await handleCustomCommands(args, ctx);
486
+ break;
487
+
488
+ case "/write":
489
+ await handleWriteFile(args, ctx);
490
+ break;
491
+
492
+ case "/exit":
493
+ case "/quit":
494
+ ctx.rl.close();
495
+ return "exit";
496
+
497
+ default:
498
+ console.log("\n" + label.system + " " + colors.warning(`Unknown command: ${cmd}. Type /help for available commands.\n`));
499
+ }
500
+ }
501
+
502
+ // ── Command Handlers ────────────────────────────────────────
503
+
504
+ function showHelp(aiConfig) {
505
+ console.log("");
506
+ console.log(colors.brand(" ⚡ AETHER CLI COMMANDS"));
507
+ console.log(separator("─"));
508
+ console.log("");
509
+ console.log(keyValue("/", "Show this help menu"));
510
+ console.log(keyValue("/help", "Show this help menu"));
511
+ console.log(keyValue("/mode <name>", "Switch mode (" + Object.keys(MODES).join(", ") + ")"));
512
+ console.log(keyValue("/modes", "List all modes with signal metrics"));
513
+ console.log(keyValue("/theme <name>", "Switch visual theme (cyberpunk, matrix, synthwave, crimson)"));
514
+ console.log(keyValue("/themes", "List available visual themes"));
515
+ console.log(keyValue("/attach <path>", "Attach a file for context (supports Tab path autocomplete!)"));
516
+ console.log(keyValue("/files", "List attached files"));
517
+ console.log(keyValue("/clear", "Clear terminal screen and reprint banner"));
518
+ console.log(keyValue("/providers", "Show active AI providers"));
519
+ console.log(keyValue("/export", "Export conversation to file"));
520
+ console.log(keyValue("/history-clear", "Clear saved persistent chat history"));
521
+ console.log(keyValue("/game", "Start the local mainframe hacking mini-game"));
522
+ console.log(keyValue("/copy", "Copy the last assistant response to clipboard"));
523
+ console.log(keyValue("/cmd <list|add|remove>", "Manage custom command shortcuts"));
524
+ console.log(keyValue("/write <filename>", "Extract last code block and save to file"));
525
+ console.log(keyValue("/exit", "End session"));
526
+
527
+ if (aiConfig && aiConfig.CUSTOM_COMMANDS) {
528
+ const custom = aiConfig.CUSTOM_COMMANDS;
529
+ const entries = Object.entries(custom);
530
+ if (entries.length > 0) {
531
+ console.log("");
532
+ console.log(colors.brand(" ⚡ CUSTOM SHORTCUTS"));
533
+ console.log(separator("─"));
534
+ for (const [cmd, template] of entries) {
535
+ console.log(keyValue(cmd, `Shortcut for: "${template}"`));
536
+ }
537
+ }
538
+ }
539
+ console.log("");
540
+ }
541
+
542
+ function handleModeSwitch(args, ctx) {
543
+ const modeName = args[0];
544
+ if (!modeName) {
545
+ console.log("\n" + label.mode + " " + colors.warning("Usage: /mode <" + Object.keys(MODES).join("|") + ">\n"));
546
+ return;
547
+ }
548
+
549
+ const newMode = getModeByName(modeName);
550
+ if (!newMode) {
551
+ console.log("\n" + label.mode + " " + colors.danger(`Unknown mode: "${modeName}".`) + " " + colors.muted("Available: " + Object.keys(MODES).join(", ") + "\n"));
552
+ return;
553
+ }
554
+
555
+ ctx.setMode(newMode);
556
+ console.log("\n" + label.mode + " " + colors.accent("Switched to ") + modeBadge(newMode.name));
557
+ console.log(" " + colors.muted(newMode.description) + "\n");
558
+
559
+ const sig = newMode.signal;
560
+ console.log(" " + signalBar("Reasoning", sig.reasoning));
561
+ console.log(" " + signalBar("Clarity", sig.clarity));
562
+ console.log(" " + signalBar("System IQ", sig.systemIQ));
563
+ console.log(" " + signalBar("Delivery", sig.delivery));
564
+ console.log("");
565
+ }
566
+
567
+ function showModes() {
568
+ console.log("");
569
+ console.log(colors.brand(" ◈ AVAILABLE REASONING MODES"));
570
+ console.log(separator("─"));
571
+ console.log("");
572
+
573
+ for (const mode of Object.values(MODES)) {
574
+ console.log(" " + modeBadge(mode.name) + " " + colors.muted(`(${mode.layer})`));
575
+ console.log(" " + colors.text(mode.description));
576
+ const sig = mode.signal;
577
+ console.log(" " + signalBar("RSN", sig.reasoning) + " " + signalBar("CLR", sig.clarity) + " " + signalBar("SIQ", sig.systemIQ) + " " + signalBar("DLV", sig.delivery));
578
+ console.log("");
579
+ }
580
+ }
581
+
582
+ async function handleAttach(args, ctx) {
583
+ const filePath = args.join(" ");
584
+ if (!filePath) {
585
+ console.log("\n" + label.file + " " + colors.warning("Usage: /attach <path-to-file>\n"));
586
+ return;
587
+ }
588
+
589
+ try {
590
+ const fileData = await parseFile(filePath);
591
+ ctx.addFile(fileData);
592
+ console.log("\n" + label.file + " " + colors.success(`Attached: ${fileData.name}`));
593
+ console.log(" " + colors.muted(`${formatBytes(fileData.size)} • ${fileData.extension} • ${ctx.attachedFiles.length} file(s) loaded\n`));
594
+ } catch (err) {
595
+ console.log("\n" + label.error + " " + colors.danger(err.message) + "\n");
596
+ }
597
+ }
598
+
599
+ function showAttachedFiles(files) {
600
+ if (files.length === 0) {
601
+ console.log("\n" + label.file + " " + colors.muted("No files attached. Use /attach <path> to add context.\n"));
602
+ return;
603
+ }
604
+
605
+ console.log("");
606
+ console.log(label.file + " " + colors.accent(`${files.length} file(s) attached:`));
607
+ for (const f of files) {
608
+ console.log(bullet(`${f.name} (${formatBytes(f.size)}, ${f.extension})`));
609
+ }
610
+ console.log("");
611
+ }
612
+
613
+ async function handleExport(history) {
614
+ if (history.length === 0) {
615
+ console.log("\n" + label.system + " " + colors.muted("No conversation to export.\n"));
616
+ return;
617
+ }
618
+
619
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
620
+ const filename = `aether-chat-${timestamp}.md`;
621
+ const filepath = resolve(filename);
622
+
623
+ let content = `# Aether AI Chat Export\n*Exported at ${new Date().toLocaleString()}*\n\n---\n\n`;
624
+
625
+ for (const entry of history) {
626
+ if (entry.role === "user") {
627
+ content += `## 👤 You\n${entry.content}\n\n`;
628
+ } else {
629
+ content += `## 🤖 Aether (${entry.provider || "unknown"})\n${entry.content}\n\n---\n\n`;
630
+ }
631
+ }
632
+
633
+ try {
634
+ await writeFile(filepath, content, "utf-8");
635
+ console.log("\n" + label.system + " " + colors.success(`Exported to: ${filepath}\n`));
636
+ } catch (err) {
637
+ console.log("\n" + label.error + " " + colors.danger(`Export failed: ${err.message}\n`));
638
+ }
639
+ }
640
+
641
+ function showStatus(ctx) {
642
+ const active = getActiveProviders(ctx.aiConfig);
643
+
644
+ console.log("");
645
+ console.log(colors.brand(" ◈ SESSION STATUS"));
646
+ console.log(separator("─"));
647
+ console.log(keyValue(" Theme", getActiveTheme().toUpperCase()));
648
+ console.log(keyValue(" Mode", ctx.currentMode.label));
649
+ console.log(keyValue(" Layer", ctx.currentMode.layer));
650
+ console.log(keyValue(" Exchanges", String(Math.floor(ctx.history.length / 2))));
651
+ console.log(keyValue(" Files", String(ctx.attachedFiles.length)));
652
+ console.log(keyValue(" Providers", String(active.length)));
653
+ console.log("");
654
+ }
655
+
656
+ function showActiveProviders(aiConfig) {
657
+ const active = getActiveProviders(aiConfig);
658
+
659
+ console.log("");
660
+ console.log(colors.brand(" ◈ ACTIVE PROVIDERS"));
661
+ console.log(separator("─"));
662
+
663
+ if (active.length === 0) {
664
+ console.log(" " + colors.warning("No providers. Run `aether setup` to configure.") + "\n");
665
+ return;
666
+ }
667
+
668
+ for (const { provider } of active) {
669
+ console.log(" " + colors.success("✓ ") + colors.text(provider.name) + colors.dim(` • ${provider.defaultModel}`));
670
+ }
671
+ console.log(" " + colors.success("✓ ") + colors.text("Krylo Companion") + colors.dim(" • Local fallback"));
672
+ console.log(" " + colors.success("✓ ") + colors.text("Math Solver") + colors.dim(" • Local"));
673
+ console.log("");
674
+ }
675
+
676
+ async function handleThemeSwitch(args) {
677
+ const themeName = args[0];
678
+ if (!themeName) {
679
+ console.log("\n" + label.system + " " + colors.warning("Usage: /theme <theme-name>. Type /themes to list themes.\n"));
680
+ return;
681
+ }
682
+
683
+ const success = setTheme(themeName);
684
+ if (success) {
685
+ await setConfigValue("THEME", themeName.toLowerCase().trim());
686
+ console.log("\n" + label.system + " " + colors.success(`✓ Theme switched to ${themeName.toUpperCase()}`));
687
+ console.log(" " + colors.muted("Visual grid modulates synchronized.\n"));
688
+ } else {
689
+ console.log("\n" + label.system + " " + colors.danger(`Unknown theme: "${themeName}".`) + " " + colors.muted(`Available: ${getThemesList().join(", ")}\n`));
690
+ }
691
+ }
692
+
693
+ function showThemesList() {
694
+ console.log("");
695
+ console.log(colors.brand(" ◈ AVAILABLE COLOR THEMES"));
696
+ console.log(separator("─"));
697
+ const active = getActiveTheme();
698
+ for (const t of getThemesList()) {
699
+ const activeText = t === active ? colors.success("★ ACTIVE") : "";
700
+ console.log(bullet(t.toUpperCase().padEnd(14) + activeText));
701
+ }
702
+ console.log("");
703
+ }
704
+
705
+ async function handleHistoryClear(history, rl) {
706
+ await clearHistory();
707
+ history.length = 0;
708
+ if (rl) rl.history = [];
709
+ console.log("\n" + label.system + " " + colors.success("✓ Persistent chat history and prompt history cleared.\n"));
710
+ }
711
+
712
+ function handleGameStart(game) {
713
+ if (game.active) {
714
+ console.log("\n" + label.system + " " + colors.warning("Mainframe breach is already in progress. Type /abort to cancel.\n"));
715
+ return;
716
+ }
717
+
718
+ // Set up game
719
+ game.active = true;
720
+ game.attempts = 0;
721
+
722
+ // Generate random 4-digit code
723
+ const code = Array.from({ length: 4 }, () => Math.floor(Math.random() * 10)).join("");
724
+ game.code = code;
725
+
726
+ const rules = runMainframeHack();
727
+ console.log("\n" + rules.text + "\n");
728
+ }
729
+
730
+ function handleGameAbort(game) {
731
+ if (!game.active) {
732
+ console.log("\n" + label.system + " " + colors.warning("No security breach in progress.\n"));
733
+ return;
734
+ }
735
+ game.active = false;
736
+ console.log("\n" + label.system + " " + colors.warning("Breach protocol aborted. Connection terminated.\n"));
737
+ }
738
+
739
+ function handleGuess(input, game) {
740
+ const guess = input.trim();
741
+ if (!/^\d{4}$/.test(guess)) {
742
+ console.log("\n" + label.error + " " + colors.danger("BREACH ERROR: Code must be exactly 4 digits (0-9).") + "\n");
743
+ return;
744
+ }
745
+
746
+ game.attempts++;
747
+
748
+ const codeArr = game.code.split("");
749
+ const guessArr = guess.split("");
750
+
751
+ let hits = 0;
752
+ let closes = 0;
753
+
754
+ const codeUsed = [false, false, false, false];
755
+ const guessUsed = [false, false, false, false];
756
+
757
+ // First pass: Hits
758
+ for (let i = 0; i < 4; i++) {
759
+ if (guessArr[i] === codeArr[i]) {
760
+ hits++;
761
+ codeUsed[i] = true;
762
+ guessUsed[i] = true;
763
+ }
764
+ }
765
+
766
+ // Second pass: Closes
767
+ for (let i = 0; i < 4; i++) {
768
+ if (guessUsed[i]) continue;
769
+ for (let j = 0; j < 4; j++) {
770
+ if (codeUsed[j]) continue;
771
+ if (guessArr[i] === codeArr[j]) {
772
+ closes++;
773
+ codeUsed[j] = true;
774
+ break;
775
+ }
776
+ }
777
+ }
778
+
779
+ console.log("");
780
+ console.log(colors.magenta(` [BREACH ATTEMPT #${game.attempts} / ${game.maxAttempts}]`));
781
+ console.log(colors.text(` BREACH INPUT: ${guess.split("").join(" ")}`));
782
+ console.log(colors.success(` HITS (Pos): ${"█ ".repeat(hits)}${"░ ".repeat(4 - hits)} (${hits})`));
783
+ console.log(colors.warning(` CLOSE (Val): ${"█ ".repeat(closes)}${"░ ".repeat(4 - closes)} (${closes})`));
784
+ console.log("");
785
+
786
+ if (hits === 4) {
787
+ console.log(label.system + " " + colors.success("MAINFRAME BYPASSED! Access granted. Decryption complete. 🔓\n"));
788
+ game.active = false;
789
+ } else if (game.attempts >= game.maxAttempts) {
790
+ console.log(label.error + " " + colors.danger("SECURITY SHUTDOWN! Mainframe locked out. Intrusion logged. 🔒"));
791
+ console.log(" Intrusion PIN was: " + colors.accent(game.code) + "\n");
792
+ game.active = false;
793
+ } else {
794
+ console.log(colors.muted(" Recalibrating security bypass codes...") + "\n");
795
+ }
796
+ }
797
+
798
+ async function handleCopy(history) {
799
+ const lastResponse = [...history].reverse().find((h) => h.role === "assistant");
800
+ if (!lastResponse) {
801
+ console.log("\n" + label.system + " " + colors.muted("No response to copy yet.\n"));
802
+ return;
803
+ }
804
+
805
+ try {
806
+ await copyToClipboard(lastResponse.content);
807
+ console.log("\n" + label.system + " " + colors.success("✓ Last response copied to OS Clipboard successfully!\n"));
808
+ } catch (err) {
809
+ console.log("\n" + label.system + " " + colors.muted("Unable to copy automatically. Displaying content below:"));
810
+ console.log(colors.text(lastResponse.content.slice(0, 800)));
811
+ if (lastResponse.content.length > 800) {
812
+ console.log(colors.dim(" [... truncated, use /export to save full conversation]"));
813
+ }
814
+ console.log("");
815
+ }
816
+ }
817
+
818
+ function copyToClipboard(text) {
819
+ return new Promise((resolve, reject) => {
820
+ let command;
821
+ if (process.platform === "win32") {
822
+ command = "clip";
823
+ } else if (process.platform === "darwin") {
824
+ command = "pbcopy";
825
+ } else {
826
+ command = "xclip -selection clipboard || xsel -ib";
827
+ }
828
+
829
+ try {
830
+ const child = exec(command, (err) => {
831
+ if (err) reject(err);
832
+ else resolve();
833
+ });
834
+ child.stdin.write(text);
835
+ child.stdin.end();
836
+ } catch (e) {
837
+ reject(e);
838
+ }
839
+ });
840
+ }
841
+
842
+ // ── Box / Badges / Theme helpers ─────────────────────────────
843
+
844
+ function providerBadge(result) {
845
+ const badges = {
846
+ "groq": chalk.bgHex("#1a2a1a").hex("#67ffb0")(" Groq "),
847
+ "together ai": chalk.bgHex("#1a2a1a").hex("#67ffb0")(" Together "),
848
+ "cerebras": chalk.bgHex("#1a2a1a").hex("#67ffb0")(" Cerebras "),
849
+ "openai": chalk.bgHex("#1a2a1a").hex("#67ffb0")(" OpenAI "),
850
+ "google": chalk.bgHex("#1a1a2a").hex("#2d7dff")(" Gemini "),
851
+ "anthropic": chalk.bgHex("#2a1a2a").hex("#b06cff")(" Claude "),
852
+ "xai": chalk.bgHex("#1a2a1a").hex("#67ffb0")(" Grok "),
853
+ "mistral ai": chalk.bgHex("#1a1a2a").hex("#ffb900")(" Mistral "),
854
+ "openrouter": chalk.bgHex("#1a1a2a").hex("#6ce8ff")(" OpenRouter "),
855
+ "cohere": chalk.bgHex("#1a2a2a").hex("#6ce8ff")(" Cohere "),
856
+ "deepseek": chalk.bgHex("#1a1a2a").hex("#2d7dff")(" DeepSeek "),
857
+ "perplexity": chalk.bgHex("#1a2a2a").hex("#6ce8ff")(" Perplexity "),
858
+ "fireworks ai": chalk.bgHex("#2a1a1a").hex("#ff6b8d")(" Fireworks "),
859
+ "local": chalk.bgHex("#1a2a1a").hex("#67ffb0")(" Math Solver "),
860
+ "krylo-fallback": chalk.bgHex("#0c1825").hex("#6ce8ff")(" Krylo "),
861
+ };
862
+
863
+ const badge = badges[result.provider] || colors.muted(` ${result.provider} `);
864
+ return badge + colors.dim(` Node ${result.node}`);
865
+ }
866
+
867
+ function signalBar(name, value) {
868
+ const filled = Math.round(value / 10);
869
+ const empty = 10 - filled;
870
+ const bar = colors.accent("█".repeat(filled)) + colors.dim("░".repeat(empty));
871
+ return `${colors.muted(name.padEnd(10))} ${bar} ${colors.muted(value + "%")}`;
872
+ }
873
+
874
+ function formatBytes(bytes) {
875
+ if (bytes < 1024) return `${bytes}B`;
876
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
877
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
878
+ }
879
+
880
+ /**
881
+ * Handles the management of custom slash command shortcuts.
882
+ */
883
+ async function handleCustomCommands(args, ctx) {
884
+ const sub = args[0]?.toLowerCase();
885
+
886
+ if (sub === "list") {
887
+ const custom = ctx.aiConfig.CUSTOM_COMMANDS || {};
888
+ const entries = Object.entries(custom);
889
+
890
+ console.log("");
891
+ console.log(colors.brand(" ⚡ CUSTOM SHORTCUT COMMANDS"));
892
+ console.log(separator("─"));
893
+
894
+ if (entries.length === 0) {
895
+ console.log(" " + colors.muted("No custom commands registered."));
896
+ console.log(" " + colors.muted("Create one: ") + colors.accent("/cmd add /explain \"Explain this code:\"") + "\n");
897
+ return;
898
+ }
899
+
900
+ for (const [cmd, template] of entries) {
901
+ console.log(` ${colors.accent(cmd.padEnd(16))} ${colors.text(template)}`);
902
+ }
903
+ console.log("");
904
+ return;
905
+ }
906
+
907
+ if (sub === "add") {
908
+ const name = args[1];
909
+ const template = args.slice(2).join(" ");
910
+
911
+ if (!name || !template) {
912
+ console.log("\n" + label.system + " " + colors.warning("Usage: /cmd add <name> <template>"));
913
+ console.log(" " + colors.muted("Example: /cmd add /explain \"Explain this code in detail:\"") + "\n");
914
+ return;
915
+ }
916
+
917
+ if (!name.startsWith("/")) {
918
+ console.log("\n" + label.system + " " + colors.danger("ERROR: Command name must start with a slash '/' (e.g. /explain)") + "\n");
919
+ return;
920
+ }
921
+
922
+ const builtIn = [
923
+ "/help", "/mode", "/modes", "/attach", "/files", "/clear",
924
+ "/providers", "/export", "/status", "/copy", "/exit", "/quit",
925
+ "/theme", "/themes", "/history-clear", "/game", "/abort", "/cmd", "/guess"
926
+ ];
927
+
928
+ if (builtIn.includes(name.toLowerCase())) {
929
+ console.log("\n" + label.system + " " + colors.danger(`ERROR: Cannot override system command "${name}"`) + "\n");
930
+ return;
931
+ }
932
+
933
+ const custom = ctx.aiConfig.CUSTOM_COMMANDS || {};
934
+ custom[name] = template;
935
+
936
+ await setConfigValue("CUSTOM_COMMANDS", custom);
937
+ ctx.aiConfig.CUSTOM_COMMANDS = custom; // sync context
938
+
939
+ console.log("\n" + label.system + " " + colors.success(`✓ Command registered successfully!`));
940
+ console.log(` ${colors.accent(name)} ➔ "${template}"\n`);
941
+ return;
942
+ }
943
+
944
+ if (sub === "remove") {
945
+ const name = args[1];
946
+ if (!name) {
947
+ console.log("\n" + label.system + " " + colors.warning("Usage: /cmd remove <name>") + "\n");
948
+ return;
949
+ }
950
+
951
+ const custom = ctx.aiConfig.CUSTOM_COMMANDS || {};
952
+ if (!custom[name]) {
953
+ console.log("\n" + label.system + " " + colors.warning(`No custom command named "${name}" exists.`) + "\n");
954
+ return;
955
+ }
956
+
957
+ delete custom[name];
958
+ await setConfigValue("CUSTOM_COMMANDS", custom);
959
+ ctx.aiConfig.CUSTOM_COMMANDS = custom; // sync context
960
+
961
+ console.log("\n" + label.system + " " + colors.success(`✓ Removed custom command: "${name}"\n`));
962
+ return;
963
+ }
964
+
965
+ console.log("\n" + label.system + " " + colors.warning("Usage: /cmd <list|add|remove> [args]"));
966
+ console.log(" " + colors.muted("Type /help for help or /cmd list to see existing shortcuts.\n"));
967
+ }
968
+
969
+ /**
970
+ * Extracts all code blocks from a markdown string.
971
+ */
972
+ function extractCodeBlocks(markdown) {
973
+ const regex = /```[\w-]*\n([\s\S]*?)\n```/g;
974
+ const blocks = [];
975
+ let match;
976
+ while ((match = regex.exec(markdown)) !== null) {
977
+ blocks.push(match[1]);
978
+ }
979
+ return blocks;
980
+ }
981
+
982
+ /**
983
+ * Manual file writing command. Extracts the last code block of the previous
984
+ * assistant response and writes it to a file.
985
+ */
986
+ async function handleWriteFile(args, ctx) {
987
+ const filename = args.join(" ");
988
+ if (!filename) {
989
+ console.log("\n" + label.system + " " + colors.warning("Usage: /write <filename>") + "\n");
990
+ return;
991
+ }
992
+
993
+ const lastResponse = [...ctx.history].reverse().find((h) => h.role === "assistant");
994
+ if (!lastResponse) {
995
+ console.log("\n" + label.system + " " + colors.muted("No assistant response available to write.\n"));
996
+ return;
997
+ }
998
+
999
+ const codeBlocks = extractCodeBlocks(lastResponse.content);
1000
+ if (codeBlocks.length === 0) {
1001
+ console.log("\n" + label.system + " " + colors.warning("No code blocks found in the last response.\n"));
1002
+ return;
1003
+ }
1004
+
1005
+ const blockContent = codeBlocks[codeBlocks.length - 1];
1006
+ const filepath = resolve(filename);
1007
+
1008
+ try {
1009
+ const { dirname } = await import("node:path");
1010
+ const { mkdir } = await import("node:fs/promises");
1011
+ const dir = dirname(filepath);
1012
+ await mkdir(dir, { recursive: true });
1013
+ await writeFile(filepath, blockContent, "utf-8");
1014
+ console.log("\n" + label.system + " " + colors.success(`✓ Code block successfully written to: ${filepath}\n`));
1015
+ } catch (err) {
1016
+ console.log("\n" + label.error + " " + colors.danger(`Write failed: ${err.message}\n`));
1017
+ }
1018
+ }