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,679 @@
1
+ // ═══════════════════════════════════════════════════════════
2
+ // AETHER AI CLI — Main CLI Logic & Command Routing
3
+ // Universal AI Gateway — Supports 13+ providers
4
+ // ═══════════════════════════════════════════════════════════
5
+
6
+ import { readFileSync } from "node:fs";
7
+ import { fileURLToPath } from "node:url";
8
+ import { dirname, join } from "node:path";
9
+ import { Command } from "commander";
10
+ import chalk from "chalk";
11
+ import { Marked } from "marked";
12
+ import { markedTerminal } from "marked-terminal";
13
+
14
+ import {
15
+ colors,
16
+ label,
17
+ separator,
18
+ keyValue,
19
+ bullet,
20
+ clearStreamedText,
21
+ getActiveTheme,
22
+ setTheme,
23
+ getThemesList,
24
+ } from "./ui/theme.js";
25
+ import { createSpinner } from "./ui/spinner.js";
26
+ import { routePrompt } from "./ai/router.js";
27
+ import { PROVIDERS, getProvidersByTier, getActiveProviders } from "./ai/providers.js";
28
+ import { startChat } from "./chat.js";
29
+ import { parseFile, formatContext } from "./file-parser.js";
30
+ import { MODES, DEFAULT_MODE, getModeByName } from "./modes.js";
31
+ import {
32
+ getAIConfig,
33
+ setConfigValue,
34
+ getConfigValue,
35
+ listConfig,
36
+ resetConfig,
37
+ deleteConfigValue,
38
+ getConfigPath,
39
+ configExists,
40
+ isValidConfigKey,
41
+ } from "./config.js";
42
+
43
+ // Configure marked dynamically for terminal output
44
+ const getMarked = () => new Marked(markedTerminal({
45
+ reflowText: true,
46
+ width: process.stdout.columns ? Math.max(20, process.stdout.columns - 4) : 80,
47
+ showSectionPrefix: false,
48
+ code: (c) => colors.orange(c),
49
+ codespan: (c) => colors.accent3(c),
50
+ heading: (h) => colors.accent.bold(h),
51
+ strong: (s) => colors.magenta.bold(s),
52
+ em: chalk.italic,
53
+ hr: (h) => colors.dim(h),
54
+ }));
55
+
56
+ const __filename = fileURLToPath(import.meta.url);
57
+ const __dirname = dirname(__filename);
58
+ const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf8"));
59
+ const VERSION = pkg.version;
60
+
61
+ /**
62
+ * Sets up and runs the Aether CLI.
63
+ * @param {string[]} argv - Process arguments
64
+ */
65
+ export function createCLI(argv) {
66
+ const program = new Command();
67
+
68
+ program
69
+ .name("aether")
70
+ .description("Aether Core AI v110 — Universal AI Gateway CLI\n Supports 13+ AI providers • Free & paid models • Local fallbacks")
71
+ .version(VERSION, "-v, --version");
72
+
73
+ // ── Chat Command ────────────────────────────────────────
74
+ program
75
+ .command("chat")
76
+ .description("Start an interactive chat session")
77
+ .option("-m, --mode <mode>", `Reasoning mode (${Object.keys(MODES).filter(m => m !== "claude-code").join(", ")})`, DEFAULT_MODE)
78
+ .option("-p, --provider <provider>", "Preferred AI provider (openai, groq, google, etc.)")
79
+ .action(async (opts) => {
80
+ await startChat({ mode: opts.mode, preferredProvider: opts.provider });
81
+ });
82
+
83
+ // ── Ask Command ─────────────────────────────────────────
84
+ program
85
+ .command("ask <prompt...>")
86
+ .description("Send a single prompt and get a response")
87
+ .option("-m, --mode <mode>", "Reasoning mode", DEFAULT_MODE)
88
+ .option("-f, --file <path>", "Attach a file for context")
89
+ .option("-p, --provider <provider>", "Preferred AI provider")
90
+ .option("--model <model>", "Override the AI model")
91
+ .option("--raw", "Output raw text without formatting")
92
+ .action(async (promptParts, opts) => {
93
+ const prompt = promptParts.join(" ");
94
+ await handleAsk(prompt, opts);
95
+ });
96
+
97
+ // ── Config Command ──────────────────────────────────────
98
+ const configCmd = program
99
+ .command("config")
100
+ .description("Manage API keys and settings");
101
+
102
+ configCmd
103
+ .command("set <key> <value>")
104
+ .description("Set a config value (e.g., GROQ_API_KEY, OPENAI_API_KEY)")
105
+ .action(async (key, value) => {
106
+ await handleConfigSet(key, value);
107
+ });
108
+
109
+ configCmd
110
+ .command("get <key>")
111
+ .description("Get a configuration value")
112
+ .action(async (key) => {
113
+ await handleConfigGet(key);
114
+ });
115
+
116
+ configCmd
117
+ .command("list")
118
+ .description("List all configuration (keys masked)")
119
+ .action(async () => {
120
+ await handleConfigList();
121
+ });
122
+
123
+ configCmd
124
+ .command("delete <key>")
125
+ .description("Delete a configuration key")
126
+ .action(async (key) => {
127
+ await handleConfigDelete(key);
128
+ });
129
+
130
+ configCmd
131
+ .command("reset")
132
+ .description("Delete all configuration")
133
+ .action(async () => {
134
+ await handleConfigReset();
135
+ });
136
+
137
+ configCmd
138
+ .command("path")
139
+ .description("Show config file location")
140
+ .action(() => {
141
+ console.log("\n" + label.config + " " + colors.text(getConfigPath()) + "\n");
142
+ });
143
+
144
+ // ── Providers Command ───────────────────────────────────
145
+ program
146
+ .command("providers")
147
+ .description("List all supported AI providers and their status")
148
+ .option("--free", "Show only free-tier providers")
149
+ .action(async (opts) => {
150
+ await handleProviders(opts);
151
+ });
152
+
153
+ // ── Models Command ──────────────────────────────────────
154
+ program
155
+ .command("models [provider]")
156
+ .description("List available models for a provider")
157
+ .action((provider) => {
158
+ handleModels(provider);
159
+ });
160
+
161
+ // ── Modes Command ───────────────────────────────────────
162
+ program
163
+ .command("modes")
164
+ .description("List all reasoning modes")
165
+ .action(() => {
166
+ handleModes();
167
+ });
168
+ // ── Theme Command ───────────────────────────────────────
169
+ program
170
+ .command("theme [name]")
171
+ .description("Show active visual theme or switch to a new theme")
172
+ .action(async (name) => {
173
+ if (!name) {
174
+ await handleThemeGet();
175
+ } else {
176
+ await handleThemeSet(name);
177
+ }
178
+ });
179
+
180
+ // ── Themes Command ──────────────────────────────────────
181
+ program
182
+ .command("themes")
183
+ .description("List all available color themes")
184
+ .action(() => {
185
+ handleThemesList();
186
+ });
187
+ // ── Status Command ──────────────────────────────────────
188
+ program
189
+ .command("status")
190
+ .description("Show system status & configured providers")
191
+ .action(async () => {
192
+ await handleStatus();
193
+ });
194
+
195
+ // ── Setup Command ───────────────────────────────────────
196
+ program
197
+ .command("setup")
198
+ .description("Interactive guided setup for API keys")
199
+ .action(async () => {
200
+ await handleSetup();
201
+ });
202
+
203
+ // ── Default: Show help ──────────────────────────────────
204
+ program.action(() => {
205
+ showMiniBanner();
206
+ program.help();
207
+ });
208
+
209
+ program.parse(argv);
210
+ }
211
+
212
+ // ═══════════════════════════════════════════════════════════
213
+ // COMMAND HANDLERS
214
+ // ═══════════════════════════════════════════════════════════
215
+
216
+ async function handleAsk(prompt, opts) {
217
+ const mode = getModeByName(opts.mode) || MODES[DEFAULT_MODE];
218
+ const aiConfig = await getAIConfig();
219
+
220
+ // Set theme from configuration
221
+ const theme = aiConfig.THEME || "cyberpunk";
222
+ setTheme(theme);
223
+
224
+ // Override model if specified
225
+ if (opts.model) {
226
+ // Set it for all providers
227
+ for (const p of Object.values(PROVIDERS)) {
228
+ aiConfig[`${p.key.replace("_API_KEY", "")}_MODEL`] = opts.model;
229
+ }
230
+ }
231
+
232
+ // Attach file context if specified
233
+ let fullPrompt = prompt;
234
+ if (opts.file) {
235
+ try {
236
+ const fileData = await parseFile(opts.file);
237
+ fullPrompt = formatContext(fileData) + "\n\n" + prompt;
238
+ console.log(label.file + " " + colors.accent(`Attached: ${fileData.name}`) + colors.dim(` (${formatBytes(fileData.size)})`));
239
+ } catch (err) {
240
+ console.log(label.error + " " + colors.danger(err.message));
241
+ process.exit(1);
242
+ }
243
+ }
244
+
245
+ if (!opts.raw) {
246
+ console.log(label.mode + " " + colors.muted(`${mode.label} • ${mode.layer}`));
247
+ }
248
+
249
+ const queryStartTime = Date.now();
250
+ let firstTokenTime = 0;
251
+ const spinner = createSpinner(colors.muted("Routing through failover mesh..."));
252
+ spinner.start();
253
+
254
+ let hasStartedStreaming = false;
255
+ let streamedText = "";
256
+ const onToken = (token) => {
257
+ if (!hasStartedStreaming) {
258
+ hasStartedStreaming = true;
259
+ firstTokenTime = Date.now();
260
+ spinner.stop();
261
+ }
262
+ process.stdout.write(token);
263
+ streamedText += token;
264
+ };
265
+
266
+ try {
267
+ const result = await routePrompt(fullPrompt, mode.systemPrompt, aiConfig, onToken);
268
+ spinner.stop();
269
+
270
+ if (opts.raw) {
271
+ if (!hasStartedStreaming) {
272
+ console.log(result.text);
273
+ } else {
274
+ if (!result.text.endsWith("\n")) {
275
+ console.log("");
276
+ }
277
+ }
278
+ } else {
279
+ if (hasStartedStreaming) {
280
+ clearStreamedText(streamedText);
281
+ }
282
+ console.log("");
283
+ console.log(label.aether + " " + colors.dim(`via ${result.provider}${result.model ? ` (${result.model})` : ""} • Node ${result.node}`));
284
+ console.log(separator("─"));
285
+ console.log("");
286
+
287
+ if (result.provider === "local" || result.provider === "krylo-fallback") {
288
+ console.log(colors.text(" " + result.text.split("\n").join("\n ")));
289
+ } else {
290
+ let displayText = result.text;
291
+ const cleanedText = displayText.replace(/\[WRITE_FILE:\s*([^\n\]]+)\][\s\S]*?\[END_WRITE\]/g, (match, p1) => {
292
+ return `\n\n${colors.brand("⚡ [File creation request: " + p1 + "]")}\n\n`;
293
+ });
294
+ const rendered = getMarked().parse(cleanedText);
295
+ console.log(rendered);
296
+ }
297
+
298
+ const elapsedSec = ((Date.now() - queryStartTime) / 1000).toFixed(1);
299
+ let speedText = "";
300
+ if (firstTokenTime > 0) {
301
+ const streamElapsed = (Date.now() - firstTokenTime) / 1000;
302
+ if (streamElapsed > 0.05) {
303
+ const estimatedTokens = Math.max(1, Math.round(streamedText.length / 4));
304
+ const tps = (estimatedTokens / streamElapsed).toFixed(1);
305
+ speedText = ` • ${tps} tok/s`;
306
+ }
307
+ }
308
+
309
+ console.log(separator("─"));
310
+ console.log(
311
+ " " + colors.dim(`Node ${result.node} • ${result.provider}`) +
312
+ (result.model ? colors.dim(` • ${result.model}`) : "") +
313
+ colors.dim(` • ${elapsedSec}s${speedText}`)
314
+ );
315
+ console.log("");
316
+
317
+ // Parse file write blocks
318
+ const writeRegex = /\[WRITE_FILE:\s*([^\n\]]+)\]\n([\s\S]*?)\n\[END_WRITE\]/g;
319
+ let match;
320
+ const fileWrites = [];
321
+ while ((match = writeRegex.exec(result.text)) !== null) {
322
+ fileWrites.push({ path: match[1].trim(), content: match[2] });
323
+ }
324
+
325
+ if (fileWrites.length > 0) {
326
+ const { resolve, dirname } = await import("node:path");
327
+ const { mkdir, writeFile } = await import("node:fs/promises");
328
+
329
+ for (const fileWrite of fileWrites) {
330
+ const finalPath = resolve(fileWrite.path);
331
+ console.log("");
332
+ console.log(label.system + " " + colors.warning(`Auto-Writing File: ${colors.accent(finalPath)} (${fileWrite.content.length} bytes)`));
333
+ try {
334
+ const dir = dirname(finalPath);
335
+ await mkdir(dir, { recursive: true });
336
+ await writeFile(finalPath, fileWrite.content, "utf-8");
337
+ console.log(" " + colors.success(`✓ File created successfully!\n`));
338
+ } catch (err) {
339
+ console.log(" " + colors.danger(`✗ Write failed: ${err.message}\n`));
340
+ }
341
+ }
342
+ }
343
+ }
344
+ } catch (err) {
345
+ spinner.fail("Request failed");
346
+ console.error(label.error + " " + colors.danger(err.message));
347
+ process.exit(1);
348
+ }
349
+ }
350
+
351
+ async function handleConfigSet(key, value) {
352
+ const normalizedKey = key.toUpperCase();
353
+
354
+ if (!isValidConfigKey(normalizedKey)) {
355
+ console.log("\n" + label.config + " " + colors.warning(`Unknown key format: "${normalizedKey}"`));
356
+ console.log(" " + colors.muted("Use format: PROVIDER_API_KEY or PROVIDER_MODEL"));
357
+ console.log(" " + colors.muted("Example: GROQ_API_KEY, OPENAI_API_KEY, GOOGLE_MODEL\n"));
358
+ return;
359
+ }
360
+
361
+ await setConfigValue(normalizedKey, value);
362
+ const maskedValue = normalizedKey.includes("KEY") && value.length > 8
363
+ ? value.slice(0, 6) + "•••" + value.slice(-3)
364
+ : value;
365
+ console.log("\n" + label.config + " " + colors.success(`✓ Set ${normalizedKey} = ${maskedValue}`));
366
+ console.log(" " + colors.muted(`Saved to ${getConfigPath()}`) + "\n");
367
+ }
368
+
369
+ async function handleConfigGet(key) {
370
+ const normalizedKey = key.toUpperCase();
371
+ const value = await getConfigValue(normalizedKey);
372
+
373
+ if (value === undefined) {
374
+ console.log("\n" + label.config + " " + colors.muted(`"${normalizedKey}" is not set.\n`));
375
+ } else {
376
+ const masked = normalizedKey.includes("KEY") && typeof value === "string" && value.length > 8
377
+ ? value.slice(0, 6) + "•••" + value.slice(-3)
378
+ : value;
379
+ console.log("\n" + label.config + " " + keyValue(normalizedKey, masked) + "\n");
380
+ }
381
+ }
382
+
383
+ async function handleConfigList() {
384
+ const exists = await configExists();
385
+ if (!exists) {
386
+ console.log("\n" + label.config + " " + colors.muted("No config file found."));
387
+ console.log(" " + colors.muted("Run ") + colors.accent("aether setup") + colors.muted(" for guided setup.\n"));
388
+ return;
389
+ }
390
+
391
+ const config = await listConfig();
392
+ const keys = Object.keys(config);
393
+
394
+ if (keys.length === 0) {
395
+ console.log("\n" + label.config + " " + colors.muted("Config file is empty.\n"));
396
+ return;
397
+ }
398
+
399
+ console.log("");
400
+ console.log(colors.brand(" ◈ CONFIGURATION"));
401
+ console.log(separator("─"));
402
+ for (const [k, v] of Object.entries(config)) {
403
+ console.log(keyValue(" " + k, v));
404
+ }
405
+ console.log("");
406
+ console.log(" " + colors.dim(`Location: ${getConfigPath()}`) + "\n");
407
+ }
408
+
409
+ async function handleConfigDelete(key) {
410
+ const normalizedKey = key.toUpperCase();
411
+ await deleteConfigValue(normalizedKey);
412
+ console.log("\n" + label.config + " " + colors.success(`✓ Deleted "${normalizedKey}"`) + "\n");
413
+ }
414
+
415
+ async function handleConfigReset() {
416
+ await resetConfig();
417
+ console.log("\n" + label.config + " " + colors.success("✓ All configuration cleared.") + "\n");
418
+ }
419
+
420
+ async function handleProviders(opts) {
421
+ const aiConfig = await getAIConfig();
422
+ const active = getActiveProviders(aiConfig);
423
+ const activeIds = new Set(active.map((a) => a.id));
424
+
425
+ console.log("");
426
+ console.log(colors.brand(" ⚡ SUPPORTED AI PROVIDERS"));
427
+ console.log(separator("─"));
428
+
429
+ const tiers = getProvidersByTier();
430
+ const sections = [
431
+ { label: "🆓 FREE TIER", providers: tiers.free, color: "#67ffb0" },
432
+ { label: "🔓 FREE + PAID", providers: tiers["free+paid"], color: "#ffb900" },
433
+ { label: "💎 PAID", providers: tiers.paid, color: "#6ce8ff" },
434
+ ];
435
+
436
+ for (const section of sections) {
437
+ if (opts.free && section.label === "💎 PAID") continue;
438
+
439
+ console.log("");
440
+ console.log(" " + chalk.hex(section.color).bold(section.label));
441
+ console.log("");
442
+
443
+ for (const p of section.providers) {
444
+ const status = activeIds.has(p.id)
445
+ ? colors.success(" ✓ ACTIVE")
446
+ : colors.dim(" ○ Not configured");
447
+ const name = chalk.hex(section.color).bold(p.name.padEnd(18));
448
+ console.log(` ${name} ${status}`);
449
+ console.log(` ${"".padEnd(18)} ${colors.muted(p.description)}`);
450
+ console.log(` ${"".padEnd(18)} ${colors.dim("Key: ")}${colors.accent(p.key)} ${colors.dim("Model: ")}${colors.text(p.defaultModel)}`);
451
+ console.log("");
452
+ }
453
+ }
454
+
455
+ console.log(separator("─"));
456
+ console.log(" " + colors.muted("Configure: ") + colors.accent("aether config set <KEY_NAME> <your-key>"));
457
+ console.log(" " + colors.muted("Quick setup: ") + colors.accent("aether setup"));
458
+ console.log("");
459
+ }
460
+
461
+ function handleModels(providerName) {
462
+ if (!providerName) {
463
+ console.log("");
464
+ console.log(colors.brand(" ◈ MODELS BY PROVIDER"));
465
+ console.log(separator("─"));
466
+
467
+ for (const [id, p] of Object.entries(PROVIDERS)) {
468
+ console.log("");
469
+ console.log(" " + colors.accent(p.name));
470
+ for (const m of p.models) {
471
+ const isDefault = m === p.defaultModel;
472
+ console.log(" " + (isDefault ? colors.accent3(" ★ " + m) : colors.muted(" " + m)));
473
+ }
474
+ }
475
+ console.log("");
476
+ return;
477
+ }
478
+
479
+ const key = providerName.toLowerCase();
480
+ const provider = PROVIDERS[key];
481
+
482
+ if (!provider) {
483
+ console.log("\n" + label.error + " " + colors.danger(`Unknown provider: "${providerName}"`));
484
+ console.log(" " + colors.muted("Available: " + Object.keys(PROVIDERS).join(", ")) + "\n");
485
+ return;
486
+ }
487
+
488
+ console.log("");
489
+ console.log(colors.brand(` ◈ ${provider.name} MODELS`));
490
+ console.log(separator("─"));
491
+ for (const m of provider.models) {
492
+ const isDefault = m === provider.defaultModel;
493
+ console.log(" " + (isDefault ? colors.accent3("★ " + m + " (default)") : colors.text(" " + m)));
494
+ }
495
+ console.log("");
496
+ console.log(" " + colors.muted("Override: ") + colors.accent(`aether ask --model ${provider.models[0]} "prompt"`) + "\n");
497
+ }
498
+
499
+ function handleModes() {
500
+ console.log("");
501
+ console.log(colors.brand(" ◈ AETHER REASONING MODES"));
502
+ console.log(separator("─"));
503
+ console.log("");
504
+
505
+ for (const mode of Object.values(MODES)) {
506
+ const badge = chalk.hex("#6ce8ff").bold(`[${mode.label}]`);
507
+ console.log(` ${badge} ${colors.dim(mode.layer)}`);
508
+ console.log(" " + colors.text(mode.description));
509
+
510
+ const sig = mode.signal;
511
+ const bar = (val) => {
512
+ const filled = Math.round(val / 10);
513
+ return chalk.hex("#6ce8ff")("█".repeat(filled)) + chalk.hex("#1a2a3a")("░".repeat(10 - filled)) + colors.dim(` ${val}%`);
514
+ };
515
+ console.log(
516
+ " " + colors.dim("RSN ") + bar(sig.reasoning) +
517
+ " " + colors.dim("CLR ") + bar(sig.clarity)
518
+ );
519
+ console.log(
520
+ " " + colors.dim("SIQ ") + bar(sig.systemIQ) +
521
+ " " + colors.dim("DLV ") + bar(sig.delivery)
522
+ );
523
+ console.log("");
524
+ }
525
+ }
526
+
527
+ async function handleStatus() {
528
+ const aiConfig = await getAIConfig();
529
+ const exists = await configExists();
530
+ const active = getActiveProviders(aiConfig);
531
+
532
+ console.log("");
533
+ console.log(colors.brand(" ⚡ AETHER SYSTEM STATUS"));
534
+ console.log(separator("─"));
535
+ console.log(keyValue(" Version", `v${VERSION}`));
536
+ console.log(keyValue(" Config", exists ? colors.success("✓ Found") : colors.warning("✗ Not found")));
537
+ console.log(keyValue(" Location", getConfigPath()));
538
+
539
+ console.log("");
540
+ console.log(colors.accent(" ◈ Active Providers:"));
541
+ if (active.length === 0) {
542
+ console.log(" " + colors.warning(" No providers configured. Run `aether setup` to get started."));
543
+ } else {
544
+ for (const { id, provider } of active) {
545
+ console.log(" " + colors.success(" ✓ ") + colors.text(provider.name) + colors.dim(` (${provider.defaultModel})`));
546
+ }
547
+ }
548
+
549
+ console.log("");
550
+ console.log(colors.accent(" ◈ Local Fallbacks:"));
551
+ console.log(keyValue(" Math Solver", colors.success("✓ Active")));
552
+ console.log(keyValue(" Krylo Companion", colors.success("✓ Standing By")));
553
+
554
+ console.log("");
555
+ console.log(colors.accent(" ◈ Failover Mesh:"));
556
+ const totalNodes = 1 + active.length; // +1 for Krylo
557
+ console.log(keyValue(" Active Nodes", `${totalNodes}`));
558
+ console.log(keyValue(" Mesh Status", active.length > 0 ? colors.success("✓ Online") : colors.warning("⚠ Local Only")));
559
+ console.log("");
560
+ }
561
+
562
+ async function handleSetup() {
563
+ const { createInterface } = await import("node:readline");
564
+
565
+ console.log("");
566
+ console.log(colors.brand(" ⚡ AETHER SETUP WIZARD"));
567
+ console.log(separator("─"));
568
+ console.log("");
569
+ console.log(colors.text(" Configure your AI providers. Press Enter to skip any provider."));
570
+ console.log(colors.muted(" Keys are stored locally at: " + getConfigPath()));
571
+ console.log("");
572
+
573
+ const rl = createInterface({
574
+ input: process.stdin,
575
+ output: process.stdout,
576
+ });
577
+
578
+ const ask = (question) => new Promise((resolve) => {
579
+ rl.question(" " + colors.accent("? ") + colors.text(question) + " ", (answer) => {
580
+ resolve(answer.trim());
581
+ });
582
+ });
583
+
584
+ // Recommended free providers first
585
+ const setupOrder = [
586
+ { id: "groq", hint: "Get free key at https://console.groq.com" },
587
+ { id: "google", hint: "Get free key at https://aistudio.google.com/apikey" },
588
+ { id: "openrouter", hint: "Free models at https://openrouter.ai/keys" },
589
+ { id: "together", hint: "Free credits at https://api.together.xyz" },
590
+ { id: "cerebras", hint: "Free tier at https://cloud.cerebras.ai" },
591
+ { id: "cohere", hint: "Free dev key at https://dashboard.cohere.com" },
592
+ { id: "openai", hint: "Paid — https://platform.openai.com/api-keys" },
593
+ { id: "anthropic", hint: "Paid — https://console.anthropic.com" },
594
+ { id: "xai", hint: "Paid — https://console.x.ai" },
595
+ { id: "mistral", hint: "https://console.mistral.ai" },
596
+ { id: "deepseek", hint: "https://platform.deepseek.com" },
597
+ { id: "perplexity", hint: "https://www.perplexity.ai/settings/api" },
598
+ { id: "fireworks", hint: "https://fireworks.ai/api-keys" },
599
+ ];
600
+
601
+ let configured = 0;
602
+
603
+ for (const { id, hint } of setupOrder) {
604
+ const provider = PROVIDERS[id];
605
+ if (!provider) continue;
606
+
607
+ const tierBadge = provider.tier === "free"
608
+ ? chalk.hex("#67ffb0")("[FREE]")
609
+ : provider.tier === "free+paid"
610
+ ? chalk.hex("#ffb900")("[FREE+PAID]")
611
+ : chalk.hex("#6ce8ff")("[PAID]");
612
+
613
+ console.log(` ${tierBadge} ${colors.text.bold(provider.name)} — ${colors.dim(provider.description)}`);
614
+ console.log(" " + colors.dim(hint));
615
+
616
+ const key = await ask(`${provider.key}:`);
617
+ if (key) {
618
+ await setConfigValue(provider.key, key);
619
+ console.log(" " + colors.success("✓ Saved!") + "\n");
620
+ configured++;
621
+ } else {
622
+ console.log(" " + colors.dim("Skipped") + "\n");
623
+ }
624
+ }
625
+
626
+ rl.close();
627
+
628
+ console.log(separator("─", 62));
629
+ if (configured > 0) {
630
+ console.log("\n " + colors.success(`✓ Setup complete! ${configured} provider(s) configured.`));
631
+ console.log(" " + colors.muted("Start chatting: ") + colors.accent("aether chat"));
632
+ console.log(" " + colors.muted("Quick query: ") + colors.accent('aether ask "Hello!"'));
633
+ } else {
634
+ console.log("\n " + colors.warning("No providers configured. Aether will use Krylo fallback mode."));
635
+ console.log(" " + colors.muted("Run ") + colors.accent("aether setup") + colors.muted(" again anytime."));
636
+ }
637
+ console.log("");
638
+ }
639
+
640
+ function showMiniBanner() {
641
+ console.log("");
642
+ console.log(colors.brand(" ⚡ Aether Core AI v110") + colors.dim(" — Universal AI Gateway"));
643
+ console.log(separator("─"));
644
+ console.log("");
645
+ }
646
+
647
+ function formatBytes(bytes) {
648
+ if (bytes < 1024) return `${bytes}B`;
649
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
650
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
651
+ }
652
+
653
+ async function handleThemeGet() {
654
+ const aiConfig = await getAIConfig();
655
+ const theme = aiConfig.THEME || "cyberpunk";
656
+ console.log("\n" + label.config + " " + colors.muted("Active Theme: ") + colors.accent(theme.toUpperCase()) + "\n");
657
+ }
658
+
659
+ async function handleThemeSet(name) {
660
+ const success = setTheme(name);
661
+ if (success) {
662
+ await setConfigValue("THEME", name.toLowerCase().trim());
663
+ console.log("\n" + label.config + " " + colors.success(`✓ Switched theme to ${name.toUpperCase()}`) + "\n");
664
+ } else {
665
+ console.log("\n" + label.error + " " + colors.danger(`Unknown theme: "${name}".`) + colors.muted(` Available: ${getThemesList().join(", ")}\n`));
666
+ }
667
+ }
668
+
669
+ function handleThemesList() {
670
+ console.log("");
671
+ console.log(colors.brand(" ◈ AVAILABLE COLOR THEMES"));
672
+ console.log(separator("─"));
673
+ const active = getActiveTheme();
674
+ for (const t of getThemesList()) {
675
+ const isAct = t === active ? colors.success("★ ACTIVE") : "";
676
+ console.log(bullet(t.toUpperCase().padEnd(14) + isAct));
677
+ }
678
+ console.log("");
679
+ }