@krishivpb60/aether-ai-cli 1.3.7 → 1.3.9

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/HIGHLIGHTS.md CHANGED
@@ -1,3 +1,16 @@
1
+ # Aether CLI v1.3.9 Highlights
2
+ - **Microphone Audio Input & Transcription (`/mic`)**:
3
+ - Adds `/mic` voice command to record audio directly from your microphone inside the terminal session.
4
+ - Implements native zero-dependency audio recording on Windows using the WinMM Multimedia Control Interface (MCI) via PowerShell.
5
+ - Automatically transcribes speech using Google Gemini (base64 inlineData), Groq Whisper, or OpenAI Whisper.
6
+ - Populates the active readline prompt buffer directly with the transcribed text so you can review, edit, and send it.
7
+
8
+ # Aether CLI v1.3.8 Highlights
9
+ - **OpenCode TUI Welcome & Navigation**:
10
+ - Implements a stunning, responsive OpenCode-style TUI System State dashboard.
11
+ - Adds `/cd <path>` workspace directory navigation command with directory-only Tab autocomplete.
12
+ - Automatically displays packaging environment info (`npm` vs `pip`).
13
+
1
14
  # Aether CLI v1.3.7 Highlights
2
15
  - **Readme Updates**:
3
16
  - Updates documentation to display interactive Git TUI, Autopilot debug loop, and Web Telemetry Dashboard companion commands.
package/README.md CHANGED
@@ -32,6 +32,7 @@
32
32
  - 🤖 **Autopilot Debug Loop** — Automatically correct build/test failures using AI self-correcting feedback loop
33
33
  - 🌿 **Interactive Git TUI** — Beautiful cyberpunk ASCII branch tree commit history & interactive file staging checkbox menu
34
34
  - 📊 **Web HUD Dashboard** — Companion local zero-dependency telemetry dashboard displaying real-time latencies & provider status
35
+ - 🎤 **Voice Microphone Input** — Record voice input directly from your terminal and transcribe it to text using Google Gemini or Whisper
35
36
  - 🔄 **Failover Mesh** — Automatic failback across all configured providers
36
37
  - 🔢 **Local Math Solver** — Evaluates mathematical expressions without an API call
37
38
  - 🤖 **Krylo Companion** — Offline cyberpunk companion bot when no API keys are configured
@@ -197,6 +198,7 @@ Inside interactive chat mode, use these slash commands:
197
198
  | `/autopilot <mode\|debug [cmd]>` | View/switch autopilot safety level or run autonomous debug loop |
198
199
  | `/git` | Launch interactive cyberpunk Git TUI and file stager checkbox menu |
199
200
  | `/dashboard` | Spawn zero-dependency local web server and launch telemetry dashboard HUD |
201
+ | `/mic` | Record audio voice input from microphone and transcribe to text |
200
202
  | `/tokens` | View detailed session token usage and exchanges telemetry |
201
203
  | `/update` | Force check for updates and update Aether CLI manually |
202
204
  | `/review` | Run git diff and stream an AI code review |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krishivpb60/aether-ai-cli",
3
- "version": "1.3.7",
3
+ "version": "1.3.9",
4
4
  "description": "Aether Core AI — A cyberpunk command-line AI assistant with multi-mode reasoning, 12-node failover mesh, file context injection, and offline fallbacks.",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
package/src/chat.js CHANGED
@@ -139,14 +139,16 @@ export async function startChat(options = {}) {
139
139
  "/theme", "/themes", "/history-clear", "/game", "/abort", "/cmd", "/write",
140
140
  "/commit", "/run", "/history", "/autopilot", "/tokens", "/update",
141
141
  "/review", "/diagnose", "/explain", "/refactor", "/bug", "/doc", "/translate",
142
- "/search", "/git", "/dashboard"
142
+ "/search", "/git", "/dashboard", "/cd", "/mic"
143
143
  ];
144
144
  const customCmds = aiConfig.CUSTOM_COMMANDS || {};
145
145
  const commands = [...builtIn, ...Object.keys(customCmds)];
146
146
 
147
- // File path autocompletion on /attach
148
- if (line.startsWith("/attach ")) {
149
- const query = line.slice(8);
147
+ // File path autocompletion on /attach or /cd
148
+ if (line.startsWith("/attach ") || line.startsWith("/cd ")) {
149
+ const isCd = line.startsWith("/cd ");
150
+ const prefix = isCd ? "/cd " : "/attach ";
151
+ const query = line.slice(prefix.length);
150
152
  const lastSlash = Math.max(query.lastIndexOf("/"), query.lastIndexOf("\\"));
151
153
  let searchDir = ".";
152
154
  let searchPrefix = query;
@@ -169,8 +171,10 @@ export async function startChat(options = {}) {
169
171
  const fullPath = searchDir === "." || searchDir === sep ? f : join(searchDir, f);
170
172
  const fullResolved = resolve(fullPath);
171
173
  const isDir = statSync(fullResolved).isDirectory();
172
- return `/attach ${fullPath}${isDir ? "/" : ""}`;
173
- });
174
+ if (isCd && !isDir) return null;
175
+ return `${prefix}${fullPath}${isDir ? "/" : ""}`;
176
+ })
177
+ .filter(Boolean);
174
178
  return [hits.length ? hits : [], line];
175
179
  }
176
180
  } catch (e) {
@@ -428,7 +432,7 @@ export async function startChat(options = {}) {
428
432
  "/theme", "/themes", "/history-clear", "/game", "/abort", "/cmd",
429
433
  "/guess", "/write", "/commit", "/run", "/history", "/autopilot", "/tokens",
430
434
  "/update", "/review", "/diagnose", "/explain", "/refactor", "/bug", "/doc",
431
- "/translate", "/search", "/git", "/dashboard"
435
+ "/translate", "/search", "/git", "/dashboard", "/cd", "/mic"
432
436
  ];
433
437
 
434
438
  const customCmds = aiConfig.CUSTOM_COMMANDS || {};
@@ -507,6 +511,10 @@ async function handleCommand(input, ctx) {
507
511
  showBanner(ctx.currentMode.name);
508
512
  break;
509
513
 
514
+ case "/cd":
515
+ await handleCd(args, ctx);
516
+ break;
517
+
510
518
  case "/export":
511
519
  await handleExport(ctx.history);
512
520
  break;
@@ -609,6 +617,10 @@ async function handleCommand(input, ctx) {
609
617
  await handleDashboardCommand(ctx);
610
618
  break;
611
619
 
620
+ case "/mic":
621
+ await handleMicInput(ctx);
622
+ break;
623
+
612
624
  case "/tokens":
613
625
  await handleTokensDisplay(ctx);
614
626
  break;
@@ -638,6 +650,7 @@ function showHelp(aiConfig) {
638
650
  console.log(keyValue("/themes", "List available visual themes"));
639
651
  console.log(keyValue("/attach <path>", "Attach a file for context (supports Tab path autocomplete!)"));
640
652
  console.log(keyValue("/files", "List attached files"));
653
+ console.log(keyValue("/cd <path>", "Change current working directory of this session (supports Tab path autocomplete!)"));
641
654
  console.log(keyValue("/clear", "Clear terminal screen and reprint banner"));
642
655
  console.log(keyValue("/providers", "Show active AI providers"));
643
656
  console.log(keyValue("/export", "Export conversation to file"));
@@ -646,6 +659,7 @@ function showHelp(aiConfig) {
646
659
  console.log(keyValue("/autopilot <mode|debug [cmd]>", "View/switch autopilot level (off, safe, workspace, machine) or run autonomous debug loop"));
647
660
  console.log(keyValue("/git", "Launch interactive Git branch tree, history, and file staging TUI"));
648
661
  console.log(keyValue("/dashboard", "Spawn web-based local cyberpunk telemetry dashboard companion"));
662
+ console.log(keyValue("/mic", "Record audio voice input from microphone and transcribe to text"));
649
663
  console.log(keyValue("/tokens", "View detailed session token usage and exchanges telemetry"));
650
664
  console.log(keyValue("/update", "Force check for updates and update Aether CLI manually"));
651
665
  console.log(keyValue("/game", "Start the local mainframe hacking mini-game"));
@@ -719,6 +733,41 @@ function showModes() {
719
733
  }
720
734
  }
721
735
 
736
+ async function handleCd(args, ctx) {
737
+ const { homedir } = await import("node:os");
738
+ if (args.length === 0) {
739
+ try {
740
+ const home = homedir();
741
+ process.chdir(home);
742
+ console.log("\n" + label.system + " " + colors.success(`Changed directory to: `) + colors.text(home) + "\n");
743
+ } catch (err) {
744
+ console.log("\n" + label.error + " " + colors.danger(`Failed to change directory: ${err.message}`) + "\n");
745
+ }
746
+ return;
747
+ }
748
+
749
+ const targetPath = args.join(" ").trim();
750
+ const resolvedPath = resolve(targetPath);
751
+
752
+ try {
753
+ if (!existsSync(resolvedPath)) {
754
+ console.log("\n" + label.error + " " + colors.danger(`Directory does not exist: ${targetPath}`) + "\n");
755
+ return;
756
+ }
757
+
758
+ const stat = statSync(resolvedPath);
759
+ if (!stat.isDirectory()) {
760
+ console.log("\n" + label.error + " " + colors.danger(`Path is not a directory: ${targetPath}`) + "\n");
761
+ return;
762
+ }
763
+
764
+ process.chdir(resolvedPath);
765
+ console.log("\n" + label.system + " " + colors.success(`Changed directory to: `) + colors.text(resolvedPath) + "\n");
766
+ } catch (err) {
767
+ console.log("\n" + label.error + " " + colors.danger(`Failed to change directory: ${err.message}`) + "\n");
768
+ }
769
+ }
770
+
722
771
  async function handleAttach(args, ctx) {
723
772
  const filePath = args.join(" ").trim();
724
773
  if (!filePath) {
@@ -2182,3 +2231,81 @@ export async function handleDashboardCommand(ctx) {
2182
2231
  }
2183
2232
  }
2184
2233
 
2234
+ /**
2235
+ * Handles recording audio voice from microphone and transcribing to text input.
2236
+ */
2237
+ export async function handleMicInput(ctx) {
2238
+ const { startRecording, transcribeAudioFile } = await import("./mic.js");
2239
+ const { join } = await import("node:path");
2240
+ const { tmpdir } = await import("node:os");
2241
+ const fs = await import("node:fs");
2242
+
2243
+ const apiKeyExists = ctx.aiConfig.GOOGLE_API_KEY || ctx.aiConfig.GROQ_API_KEY || ctx.aiConfig.OPENAI_API_KEY;
2244
+ if (!apiKeyExists) {
2245
+ console.log("\n" + label.error + " " + colors.danger("No API keys found for speech-to-text. Please configure GOOGLE_API_KEY, GROQ_API_KEY, or OPENAI_API_KEY.\n"));
2246
+ return;
2247
+ }
2248
+
2249
+ const wavPath = join(tmpdir(), `aether_mic_${Date.now()}.wav`);
2250
+ let handle;
2251
+
2252
+ try {
2253
+ handle = await startRecording(wavPath);
2254
+ } catch (err) {
2255
+ console.log("\n" + label.error + " " + colors.danger(`Failed to start recording: ${err.message}\n`));
2256
+ return;
2257
+ }
2258
+
2259
+ console.log("\n" + label.system + " " + colors.brand("🎤 AUDIO VOICE INPUT"));
2260
+ console.log(separator("─"));
2261
+ console.log(colors.accent(" Recording started..."));
2262
+ console.log(" " + colors.muted("Speak into your microphone."));
2263
+ console.log(" " + colors.brand("Press [Enter] to STOP and transcribe..."));
2264
+ console.log(separator("─"));
2265
+
2266
+ ctx.rl.pause();
2267
+
2268
+ await new Promise((resolve) => {
2269
+ function onData(chunk) {
2270
+ if (chunk === "\r" || chunk === "\n" || chunk === "\r\n") {
2271
+ process.stdin.removeListener("data", onData);
2272
+ resolve();
2273
+ }
2274
+ }
2275
+ process.stdin.on("data", onData);
2276
+ });
2277
+
2278
+ ctx.rl.resume();
2279
+
2280
+ console.log("");
2281
+ const spinner = createSpinner("transcribe");
2282
+ spinner.start("Stopping recording and transcribing...");
2283
+
2284
+ try {
2285
+ await handle.stop();
2286
+ const text = await transcribeAudioFile(wavPath, ctx.aiConfig);
2287
+ spinner.stop();
2288
+
2289
+ if (fs.existsSync(wavPath)) {
2290
+ try { fs.unlinkSync(wavPath); } catch (e) {}
2291
+ }
2292
+
2293
+ if (!text.trim()) {
2294
+ console.log("\n" + label.system + " " + colors.warning("No speech detected or transcription was empty.\n"));
2295
+ return;
2296
+ }
2297
+
2298
+ console.log("\n" + label.system + " " + colors.success("✓ Transcribed text:"));
2299
+ console.log(" " + colors.text(`"${text}"`));
2300
+ console.log("");
2301
+
2302
+ ctx.rl.write(text);
2303
+ } catch (err) {
2304
+ spinner.stop();
2305
+ if (fs.existsSync(wavPath)) {
2306
+ try { fs.unlinkSync(wavPath); } catch (e) {}
2307
+ }
2308
+ console.log("\n" + label.error + " " + colors.danger(`Transcription failed: ${err.message}\n`));
2309
+ }
2310
+ }
2311
+
package/src/mic.js ADDED
@@ -0,0 +1,220 @@
1
+ // ═══════════════════════════════════════════════════════════
2
+ // AETHER AI CLI — Voice Input / Microphone Engine
3
+ // ═══════════════════════════════════════════════════════════
4
+
5
+ import { spawn } from "node:child_process";
6
+ import { platform } from "node:os";
7
+ import fs from "node:fs";
8
+
9
+ /**
10
+ * Starts audio recording from the microphone and returns a handle to stop it.
11
+ * @param {string} wavPath - Path where the .wav file will be saved
12
+ * @returns {Promise<{ stop: () => Promise<void> }>}
13
+ */
14
+ export async function startRecording(wavPath) {
15
+ if (fs.existsSync(wavPath)) {
16
+ try {
17
+ fs.unlinkSync(wavPath);
18
+ } catch (e) {
19
+ // Ignore
20
+ }
21
+ }
22
+
23
+ const isWin = platform() === "win32";
24
+
25
+ if (isWin) {
26
+ // Windows: Use native WinMM MCI API via a background PowerShell process
27
+ const ps = spawn("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", "-"], {
28
+ stdio: ["pipe", "pipe", "ignore"]
29
+ });
30
+
31
+ ps.stdin.write(`Add-Type -MemberDefinition '[DllImport("winmm.dll", CharSet = CharSet.Ansi)] public static extern int mciSendString(string cmd, System.Text.StringBuilder ret, int len, IntPtr cb);' -Name WinMM -Namespace Win32\r\n`);
32
+ ps.stdin.write(`[Win32.WinMM]::mciSendString("open new Type waveaudio Alias myRecorder", $null, 0, [IntPtr]::Zero)\r\n`);
33
+ ps.stdin.write(`[Win32.WinMM]::mciSendString("record myRecorder", $null, 0, [IntPtr]::Zero)\r\n`);
34
+
35
+ return {
36
+ stop: () => {
37
+ return new Promise((resolve) => {
38
+ ps.on("close", () => {
39
+ resolve();
40
+ });
41
+ const escapedPath = wavPath.replace(/\\/g, "\\\\");
42
+ ps.stdin.write(`[Win32.WinMM]::mciSendString('save myRecorder "${escapedPath}"', $null, 0, [IntPtr]::Zero)\r\n`);
43
+ ps.stdin.write(`[Win32.WinMM]::mciSendString("close myRecorder", $null, 0, [IntPtr]::Zero)\r\n`);
44
+ ps.stdin.write("exit\r\n");
45
+ ps.stdin.end();
46
+ });
47
+ }
48
+ };
49
+ } else {
50
+ // macOS / Linux: Try spawning standard command-line recording tools
51
+ let cmd = "";
52
+ let args = [];
53
+
54
+ // Check if sox/rec is available (highest quality/reliability)
55
+ if (await commandExists("rec")) {
56
+ cmd = "rec";
57
+ args = ["-q", wavPath];
58
+ } else if (await commandExists("arecord")) {
59
+ cmd = "arecord";
60
+ args = ["-f", "cd", "-t", "wav", wavPath];
61
+ } else if (await commandExists("ffmpeg")) {
62
+ cmd = "ffmpeg";
63
+ const isMac = platform() === "darwin";
64
+ args = isMac
65
+ ? ["-y", "-f", "avfoundation", "-i", ":0", wavPath]
66
+ : ["-y", "-f", "alsa", "-i", "default", wavPath];
67
+ } else {
68
+ throw new Error("No recording utility found. On Windows, recording is native. On macOS/Linux, please install 'sox', 'arecord', or 'ffmpeg'.");
69
+ }
70
+
71
+ const proc = spawn(cmd, args, { stdio: "ignore" });
72
+
73
+ return {
74
+ stop: () => {
75
+ return new Promise((resolve) => {
76
+ proc.on("close", () => {
77
+ resolve();
78
+ });
79
+ proc.kill("SIGTERM");
80
+ });
81
+ }
82
+ };
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Helper to check if a command exists in the environment PATH.
88
+ */
89
+ function commandExists(name) {
90
+ return new Promise((resolve) => {
91
+ const isWin = platform() === "win32";
92
+ const checkCmd = isWin ? "where" : "which";
93
+ const check = spawn(checkCmd, [name], { stdio: "ignore" });
94
+ check.on("close", (code) => {
95
+ resolve(code === 0);
96
+ });
97
+ });
98
+ }
99
+
100
+ /**
101
+ * Transcribes a local audio WAV file using the configured AI providers.
102
+ * Priority: Google Gemini -> Groq Whisper -> OpenAI Whisper.
103
+ * @param {string} wavPath - Path to the WAV file
104
+ * @param {object} aiConfig - Active AI configuration
105
+ * @returns {Promise<string>} Transcribed text
106
+ */
107
+ export async function transcribeAudioFile(wavPath, aiConfig) {
108
+ if (!fs.existsSync(wavPath)) {
109
+ throw new Error(`Audio file not found: ${wavPath}`);
110
+ }
111
+
112
+ const fileBuffer = fs.readFileSync(wavPath);
113
+
114
+ // 1. Google Gemini Transcription
115
+ if (aiConfig.GOOGLE_API_KEY) {
116
+ const base64Audio = fileBuffer.toString("base64");
117
+ const model = "gemini-2.5-flash";
118
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${aiConfig.GOOGLE_API_KEY}`;
119
+
120
+ const body = {
121
+ contents: [
122
+ {
123
+ role: "user",
124
+ parts: [
125
+ {
126
+ inlineData: {
127
+ mimeType: "audio/wav",
128
+ data: base64Audio
129
+ }
130
+ },
131
+ {
132
+ text: "Transcribe this audio file exactly as spoken. Output ONLY the plain transcription text, with no extra formatting, conversational filler, timestamps, or commentary. If there is no speech, output an empty string."
133
+ }
134
+ ]
135
+ }
136
+ ]
137
+ };
138
+
139
+ const response = await fetch(url, {
140
+ method: "POST",
141
+ headers: { "Content-Type": "application/json" },
142
+ body: JSON.stringify(body)
143
+ });
144
+
145
+ if (!response.ok) {
146
+ const errorText = await response.text();
147
+ throw new Error(`Gemini transcription error: ${response.statusText}. ${errorText}`);
148
+ }
149
+
150
+ const data = await response.json();
151
+ const candidate = data.candidates?.[0];
152
+ if (!candidate) {
153
+ return "";
154
+ }
155
+
156
+ const text = candidate.content?.parts
157
+ ?.map((p) => p.text)
158
+ .filter(Boolean)
159
+ .join("") || "";
160
+
161
+ // Clean up timestamps if returned (e.g. 00:00:23)
162
+ const cleaned = text.trim();
163
+ if (/^\d{2}:\d{2}:\d{2}$/.test(cleaned)) {
164
+ return "";
165
+ }
166
+ return cleaned;
167
+ }
168
+
169
+ // 2. Groq Whisper / OpenAI Whisper
170
+ let apiKey = aiConfig.GROQ_API_KEY;
171
+ let url = "https://api.groq.com/openai/v1/audio/transcriptions";
172
+ let modelName = "whisper-large-v3";
173
+
174
+ if (!apiKey) {
175
+ apiKey = aiConfig.OPENAI_API_KEY;
176
+ url = "https://api.openai.com/v1/audio/transcriptions";
177
+ modelName = "whisper-1";
178
+ }
179
+
180
+ if (!apiKey) {
181
+ throw new Error("No API key configured for speech-to-text. Please configure GOOGLE_API_KEY, GROQ_API_KEY, or OPENAI_API_KEY.");
182
+ }
183
+
184
+ const boundary = "----WebKitFormBoundary" + Math.random().toString(36).substring(2);
185
+
186
+ const header =
187
+ `--${boundary}\r\n` +
188
+ `Content-Disposition: form-data; name="file"; filename="audio.wav"\r\n` +
189
+ `Content-Type: audio/wav\r\n\r\n`;
190
+
191
+ const modelPart =
192
+ `\r\n--${boundary}\r\n` +
193
+ `Content-Disposition: form-data; name="model"\r\n\r\n${modelName}\r\n`;
194
+
195
+ const footer = `--${boundary}--\r\n`;
196
+
197
+ const body = Buffer.concat([
198
+ Buffer.from(header, 'utf-8'),
199
+ fileBuffer,
200
+ Buffer.from(modelPart, 'utf-8'),
201
+ Buffer.from(footer, 'utf-8')
202
+ ]);
203
+
204
+ const response = await fetch(url, {
205
+ method: "POST",
206
+ headers: {
207
+ "Authorization": `Bearer ${apiKey}`,
208
+ "Content-Type": `multipart/form-data; boundary=${boundary}`
209
+ },
210
+ body: body
211
+ });
212
+
213
+ if (!response.ok) {
214
+ const errorText = await response.text();
215
+ throw new Error(`Whisper transcription error: ${response.statusText}. ${errorText}`);
216
+ }
217
+
218
+ const data = await response.json();
219
+ return (data.text || "").trim();
220
+ }
package/src/ui/banner.js CHANGED
@@ -2,11 +2,37 @@
2
2
  // AETHER AI CLI — ASCII Art Welcome Banner
3
3
  // ═══════════════════════════════════════════════════════════
4
4
 
5
+ import os from "node:os";
6
+ import { readFileSync, existsSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { homedir } from "node:os";
5
9
  import chalk from "chalk";
6
- import { colors, separator, bullet } from "./theme.js";
10
+ import { colors, separator, modeBadge } from "./theme.js";
11
+ import { getActiveProviders } from "../ai/providers.js";
12
+ import { MODES } from "../modes.js";
13
+
14
+ // ANSI escape code strip regex
15
+ const ANSI_REGEX = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
16
+
17
+ function getVisibleLength(str) {
18
+ return str.replace(ANSI_REGEX, "").length;
19
+ }
20
+
21
+ function loadConfigSync() {
22
+ try {
23
+ const configPath = join(homedir(), ".aether", "config.json");
24
+ if (existsSync(configPath)) {
25
+ const raw = readFileSync(configPath, "utf-8");
26
+ return JSON.parse(raw);
27
+ }
28
+ } catch (e) {
29
+ // Ignore
30
+ }
31
+ return {};
32
+ }
7
33
 
8
34
  /**
9
- * Displays the cyberpunk-styled Aether ASCII art banner.
35
+ * Displays the cyberpunk-styled Aether ASCII art banner and OpenCode-style system info.
10
36
  * @param {string} [currentMode='titan'] - The currently active mode name
11
37
  */
12
38
  export function showBanner(currentMode = "titan") {
@@ -15,7 +41,8 @@ export function showBanner(currentMode = "titan") {
15
41
  const c3 = colors.accent3;
16
42
  const dim = colors.dim;
17
43
 
18
- const art = [
44
+ // 1. ASCII Art Logo
45
+ const logo = [
19
46
  "",
20
47
  c1(" ╔═══════════════════════════════════════════════════════════╗"),
21
48
  c1(" ║") + c2(" █████╗ ███████╗████████╗██╗ ██╗███████╗██████╗ ") + c1("║"),
@@ -25,36 +52,152 @@ export function showBanner(currentMode = "titan") {
25
52
  c1(" ║") + c3(" ██║ ██║███████╗ ██║ ██║ ██║███████╗██║ ██║ ") + c1("║"),
26
53
  c1(" ║") + dim(" ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ") + c1("║"),
27
54
  c1(" ╚═══════════════════════════════════════════════════════════╝"),
28
- "",
29
- c1(" ⚡ ") + colors.text.bold("Aether Core AI v110") + colors.dim(" — Fusion Command Station"),
30
- c2(" ◈ ") + colors.muted(`Active Mode: `) + modeLabel(currentMode),
31
- "",
32
- separator("─"),
33
- "",
34
- bullet("Type your prompt and press " + colors.accent("Enter") + " to query."),
35
- bullet("Use " + colors.accent("/help") + " for all commands."),
36
- bullet("Use " + colors.accent("/mode <name>") + " to switch reasoning mode."),
37
- bullet("Use " + colors.accent("/attach <file>") + " to add file context."),
38
- bullet("Use " + colors.accent("/exit") + " or " + colors.accent("Ctrl+C") + " to quit."),
39
- "",
40
- separator("─"),
41
- "",
42
- ];
55
+ ].join("\n");
43
56
 
44
- console.log(art.join("\n"));
45
- }
57
+ console.log(logo);
46
58
 
47
- /**
48
- * Gets a styled label for the given mode.
49
- * @param {string} mode - Mode name
50
- * @returns {string} Styled mode label
51
- */
52
- function modeLabel(mode) {
53
- const labels = {
54
- synthesis: colors.accent3.bold("Synthesis v2.5"),
55
- research: colors.accent2.bold("Research v104"),
56
- architect: colors.magenta.bold("Architect v55"),
57
- titan: colors.accent.bold("Titan Fusion v110"),
59
+ // 2. Fetch User & System Context
60
+ let username = "Explorer";
61
+ try {
62
+ username = os.userInfo().username;
63
+ } catch (e) {
64
+ username = process.env.USER || process.env.USERNAME || "Explorer";
65
+ }
66
+
67
+ const cwd = process.cwd();
68
+ const home = homedir();
69
+ let displayCwd = cwd;
70
+ if (cwd.startsWith(home)) {
71
+ displayCwd = "~" + cwd.slice(home.length);
72
+ }
73
+
74
+ const config = loadConfigSync();
75
+ const active = getActiveProviders(config);
76
+
77
+ const activeNames = active.length > 0
78
+ ? [...new Set(active.map(a => a.provider.name))].join(", ")
79
+ : "Local math & offline logic only";
80
+
81
+ const meshStatusText = active.length > 0
82
+ ? colors.success(`● Online (${active.length} active node${active.length === 1 ? "" : "s"})`)
83
+ : colors.warning(`○ Offline (Local fallbacks active)`);
84
+
85
+ const modeDetails = MODES[currentMode.toLowerCase()] || MODES.titan;
86
+ const modeText = modeBadge(currentMode) || colors.accent.bold(modeDetails.label);
87
+
88
+ const shortDescriptions = {
89
+ synthesis: "Balanced reasoning with clean structure.",
90
+ research: "Deep analysis with evidence-based reasoning.",
91
+ architect: "Systems thinking with build plans.",
92
+ titan: "Ultimate reasoning fusion of Codex & Claude Code.",
58
93
  };
59
- return labels[mode?.toLowerCase()] || labels.titan;
94
+ const desc = shortDescriptions[currentMode.toLowerCase()] || shortDescriptions.titan;
95
+
96
+ let version = "1.3.8";
97
+ try {
98
+ const pkgUrl = new URL("../../package.json", import.meta.url);
99
+ const pkg = JSON.parse(readFileSync(pkgUrl, "utf-8"));
100
+ version = pkg.version || "1.3.8";
101
+ } catch (e) {
102
+ // Fallback
103
+ }
104
+
105
+ // 3. Render System Status Box
106
+ const columns = process.stdout.columns || 80;
107
+ const boxWidth = Math.max(76, Math.min(90, columns - 4));
108
+ const maxValueWidth = boxWidth - 20;
109
+
110
+ // Truncate values to fit the box cleanly
111
+ let workspaceValue = displayCwd;
112
+ if (workspaceValue.length > maxValueWidth) {
113
+ workspaceValue = "..." + workspaceValue.slice(-(maxValueWidth - 3));
114
+ }
115
+
116
+ let engineValue = activeNames;
117
+ if (engineValue.length > maxValueWidth) {
118
+ engineValue = engineValue.slice(0, maxValueWidth - 3) + "...";
119
+ }
120
+
121
+ // Truncate description to ensure total mode value (badge + sep + desc) fits within maxValueWidth
122
+ const modeBadgeText = getVisibleLength(modeText);
123
+ const maxDescWidth = maxValueWidth - modeBadgeText - 3; // 3 for " — "
124
+ let descValue = desc;
125
+ if (descValue.length > maxDescWidth) {
126
+ descValue = descValue.slice(0, Math.max(10, maxDescWidth - 3)) + "...";
127
+ }
128
+ const modeRowValue = `${modeText} ${colors.dim(`— ${descValue}`)}`;
129
+
130
+ function formatRow(label, value) {
131
+ const spaces = " ".repeat(Math.max(0, 14 - getVisibleLength(label)));
132
+ return `${label}${spaces}${value}`;
133
+ }
134
+
135
+ const packagerText = process.env.AETHER_PACKAGER === "pip"
136
+ ? "pip (aether-ai-agent-cli)"
137
+ : "npm (@krishivpb60/aether-ai-cli)";
138
+
139
+ const rows = [
140
+ formatRow(` ${colors.muted("📂 Workspace")}`, colors.text(workspaceValue)),
141
+ formatRow(` ${colors.muted("🧠 Mode")}`, modeRowValue),
142
+ formatRow(` ${colors.muted("🟢 Network")}`, meshStatusText),
143
+ formatRow(` ${colors.muted("⚙️ Engine")}`, colors.text(engineValue)),
144
+ formatRow(` ${colors.muted("📦 Packager")}`, colors.text(packagerText)),
145
+ ];
146
+
147
+ console.log(`\n ⚡ ${colors.brand("AETHER COMMAND STATION v" + version)} • Welcome back, ${colors.accent(username)}`);
148
+
149
+ // Draw Box
150
+ const leftLine = "═".repeat(2);
151
+ const rightLine = "═".repeat(boxWidth - 21);
152
+ const top = dim(` ╔${leftLine} `) + colors.brand("SYSTEM STATE") + dim(` ${rightLine}╗`);
153
+ console.log(top);
154
+ for (const row of rows) {
155
+ const visibleLen = getVisibleLength(row);
156
+ const padding = " ".repeat(Math.max(0, boxWidth - visibleLen - 2));
157
+ console.log(dim(" ║ ") + row + padding + dim(" ║"));
158
+ }
159
+ console.log(dim(` ╚${"═".repeat(boxWidth - 2)}╝`));
160
+
161
+ // 4. Quick Starter Cards (two columns)
162
+ const starters = [
163
+ { cmd: "/explain", desc: "Analyze files in workspace" },
164
+ { cmd: "/review", desc: "Review git changes (diffs)" },
165
+ { cmd: "/diagnose",desc: "Auto-heal failing tests" },
166
+ { cmd: "/dashboard",desc: "Launch visual telemetry HUD" },
167
+ { cmd: "/autopilot",desc: "Start autonomous debug loop" },
168
+ { cmd: "/game", desc: "Play mainframe hacking game" },
169
+ ];
170
+
171
+ console.log(`\n ⚡ ${colors.brand("QUICK COMMAND STARTERS")}`);
172
+ if (boxWidth >= 80) {
173
+ // Print 2 columns
174
+ for (let i = 0; i < starters.length; i += 2) {
175
+ const s1 = starters[i];
176
+ const s2 = starters[i + 1];
177
+
178
+ const col1 = ` ${colors.accent(s1.cmd.padEnd(11))} ${colors.text(s1.desc.padEnd(28))}`;
179
+ const col2 = s2 ? ` ${colors.accent(s2.cmd.padEnd(11))} ${colors.text(s2.desc)}` : "";
180
+
181
+ console.log(col1 + col2);
182
+ }
183
+ } else {
184
+ // Print 1 column
185
+ for (const s of starters) {
186
+ console.log(` ${colors.accent(s.cmd.padEnd(11))} ${colors.text(s.desc)}`);
187
+ }
188
+ }
189
+
190
+ // 5. Help / Footer info
191
+ console.log("\n" + separator("─"));
192
+ const footer = [
193
+ colors.dim(" Press "),
194
+ colors.accent("Tab"),
195
+ colors.dim(" to autocomplete file paths • Type "),
196
+ colors.accent("/help"),
197
+ colors.dim(" for all commands • "),
198
+ colors.accent("Ctrl+C"),
199
+ colors.dim(" to exit session")
200
+ ].join("");
201
+ console.log(footer);
202
+ console.log(separator("─") + "\n");
60
203
  }
@@ -76,7 +76,7 @@
76
76
  }
77
77
 
78
78
  .hud-frame::after {
79
- content: "AETHER CLI V1.3.7";
79
+ content: "AETHER CLI V1.3.9";
80
80
  position: absolute;
81
81
  bottom: -12px;
82
82
  right: 20px;