@krishivpb60/aether-ai-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +30 -0
- package/LICENSE +21 -0
- package/ORIGINAL_REQUEST.md +74 -0
- package/README.md +271 -0
- package/aether_pip/__init__.py +1 -0
- package/aether_pip/cli.py +49 -0
- package/bin/aether.js +10 -0
- package/package.json +46 -0
- package/setup.py +51 -0
- package/src/ai/fallback.js +179 -0
- package/src/ai/google.js +87 -0
- package/src/ai/providers.js +203 -0
- package/src/ai/router.js +114 -0
- package/src/ai/universal.js +465 -0
- package/src/ai/xai.js +50 -0
- package/src/chat.js +1034 -0
- package/src/cli.js +642 -0
- package/src/config.js +214 -0
- package/src/file-parser.js +94 -0
- package/src/modes.js +88 -0
- package/src/ui/banner.js +60 -0
- package/src/ui/spinner.js +43 -0
- package/src/ui/theme.js +169 -0
- package/test/config.test.js +182 -0
- package/test/fallback.test.js +105 -0
- package/test/file-parser.test.js +136 -0
- package/test/router.test.js +174 -0
- package/test/ux.test.js +128 -0
package/src/config.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════
|
|
2
|
+
// AETHER AI CLI — Secure Configuration Management
|
|
3
|
+
// Stores user API keys locally at ~/.aether/config.json
|
|
4
|
+
// Supports ALL AI providers (13+ and growing)
|
|
5
|
+
// ═══════════════════════════════════════════════════════════
|
|
6
|
+
|
|
7
|
+
import { readFile, writeFile, mkdir, unlink, access } from "node:fs/promises";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { getAllConfigKeys } from "./ai/providers.js";
|
|
11
|
+
|
|
12
|
+
const CONFIG_DIR = join(homedir(), ".aether");
|
|
13
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
14
|
+
|
|
15
|
+
const SENSITIVE_PATTERNS = ["KEY", "TOKEN", "SECRET"];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Returns the full path to the config file.
|
|
19
|
+
* @returns {string}
|
|
20
|
+
*/
|
|
21
|
+
export function getConfigPath() {
|
|
22
|
+
return CONFIG_FILE;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Loads the config from disk.
|
|
27
|
+
* @returns {Promise<object>} Parsed config or empty object
|
|
28
|
+
*/
|
|
29
|
+
export async function loadConfig() {
|
|
30
|
+
try {
|
|
31
|
+
const raw = await readFile(CONFIG_FILE, "utf-8");
|
|
32
|
+
return JSON.parse(raw);
|
|
33
|
+
} catch {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Saves config to disk, creating the directory if needed.
|
|
40
|
+
* @param {object} config - The config object to write
|
|
41
|
+
*/
|
|
42
|
+
export async function saveConfig(config) {
|
|
43
|
+
try {
|
|
44
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
45
|
+
await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
|
|
46
|
+
} catch (err) {
|
|
47
|
+
throw new Error(`Failed to save config: ${err.message}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Gets a single config value.
|
|
53
|
+
* @param {string} key
|
|
54
|
+
* @returns {Promise<string|undefined>}
|
|
55
|
+
*/
|
|
56
|
+
export async function getConfigValue(key) {
|
|
57
|
+
const config = await loadConfig();
|
|
58
|
+
return config[key];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Sets a single config value.
|
|
63
|
+
* @param {string} key
|
|
64
|
+
* @param {string} value
|
|
65
|
+
*/
|
|
66
|
+
export async function setConfigValue(key, value) {
|
|
67
|
+
const config = await loadConfig();
|
|
68
|
+
config[key] = value;
|
|
69
|
+
await saveConfig(config);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Deletes a single config key.
|
|
74
|
+
* @param {string} key
|
|
75
|
+
*/
|
|
76
|
+
export async function deleteConfigValue(key) {
|
|
77
|
+
const config = await loadConfig();
|
|
78
|
+
delete config[key];
|
|
79
|
+
await saveConfig(config);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Deletes the entire config file.
|
|
84
|
+
*/
|
|
85
|
+
export async function resetConfig() {
|
|
86
|
+
try {
|
|
87
|
+
await unlink(CONFIG_FILE);
|
|
88
|
+
} catch {
|
|
89
|
+
// File may not exist
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Lists all config keys with sensitive values masked.
|
|
95
|
+
* @returns {Promise<object>} Config with sensitive values masked
|
|
96
|
+
*/
|
|
97
|
+
export async function listConfig() {
|
|
98
|
+
const config = await loadConfig();
|
|
99
|
+
const masked = {};
|
|
100
|
+
|
|
101
|
+
for (const [key, value] of Object.entries(config)) {
|
|
102
|
+
const isSensitive = SENSITIVE_PATTERNS.some((p) => key.toUpperCase().includes(p));
|
|
103
|
+
if (isSensitive && typeof value === "string" && value.length > 8) {
|
|
104
|
+
masked[key] = value.slice(0, 6) + "•••" + value.slice(-3);
|
|
105
|
+
} else {
|
|
106
|
+
masked[key] = value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return masked;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Returns the full flat config object for the AI router.
|
|
115
|
+
* Merges the config file with environment variables (config file takes precedence).
|
|
116
|
+
* @returns {Promise<object>}
|
|
117
|
+
*/
|
|
118
|
+
export async function getAIConfig() {
|
|
119
|
+
const config = await loadConfig();
|
|
120
|
+
const allKeys = getAllConfigKeys();
|
|
121
|
+
|
|
122
|
+
// Merge: config file values override env vars
|
|
123
|
+
const merged = {};
|
|
124
|
+
for (const key of allKeys) {
|
|
125
|
+
merged[key] = config[key] || process.env[key] || "";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Also pass through any custom model overrides and extra keys
|
|
129
|
+
for (const [key, value] of Object.entries(config)) {
|
|
130
|
+
if (!merged[key]) {
|
|
131
|
+
merged[key] = value;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check env vars for anything the config didn't have
|
|
136
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
137
|
+
if (key.endsWith("_API_KEY") && !merged[key]) {
|
|
138
|
+
merged[key] = value;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return merged;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Checks if the config file exists.
|
|
147
|
+
* @returns {Promise<boolean>}
|
|
148
|
+
*/
|
|
149
|
+
export async function configExists() {
|
|
150
|
+
try {
|
|
151
|
+
await access(CONFIG_FILE);
|
|
152
|
+
return true;
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Checks if the given key is a valid/recognized config key.
|
|
160
|
+
* Accepts any *_API_KEY, *_MODEL, and known keys.
|
|
161
|
+
* @param {string} key
|
|
162
|
+
* @returns {boolean}
|
|
163
|
+
*/
|
|
164
|
+
export function isValidConfigKey(key) {
|
|
165
|
+
const upper = key.toUpperCase();
|
|
166
|
+
// Accept any API key or model override
|
|
167
|
+
if (upper.endsWith("_API_KEY") || upper.endsWith("_API_KEYS") || upper.endsWith("_MODEL") || upper === "THEME" || upper === "CUSTOM_COMMANDS") {
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
// Accept known config keys
|
|
171
|
+
const knownKeys = getAllConfigKeys();
|
|
172
|
+
return knownKeys.includes(upper);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const HISTORY_FILE = join(CONFIG_DIR, "history.json");
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Loads chat history from disk.
|
|
179
|
+
* @returns {Promise<Array>} List of chat exchanges
|
|
180
|
+
*/
|
|
181
|
+
export async function loadHistory() {
|
|
182
|
+
try {
|
|
183
|
+
const raw = await readFile(HISTORY_FILE, "utf-8");
|
|
184
|
+
return JSON.parse(raw);
|
|
185
|
+
} catch {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Saves chat history to disk.
|
|
192
|
+
* @param {Array} history - List of chat exchanges to save
|
|
193
|
+
*/
|
|
194
|
+
export async function saveHistory(history) {
|
|
195
|
+
try {
|
|
196
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
197
|
+
// Limit saved history to last 50 entries to keep it light
|
|
198
|
+
const trimmed = history.slice(-50);
|
|
199
|
+
await writeFile(HISTORY_FILE, JSON.stringify(trimmed, null, 2), "utf-8");
|
|
200
|
+
} catch {
|
|
201
|
+
// Fail silently to not block chat
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Deletes the chat history file.
|
|
207
|
+
*/
|
|
208
|
+
export async function clearHistory() {
|
|
209
|
+
try {
|
|
210
|
+
await unlink(HISTORY_FILE);
|
|
211
|
+
} catch {
|
|
212
|
+
// File may not exist
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════
|
|
2
|
+
// AETHER AI CLI — File Parser & Context Injector
|
|
3
|
+
// ═══════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
import { readFile, stat } from "node:fs/promises";
|
|
6
|
+
import { resolve, extname, basename } from "node:path";
|
|
7
|
+
|
|
8
|
+
const MAX_CONTENT_LENGTH = 30000;
|
|
9
|
+
|
|
10
|
+
const SUPPORTED_EXTENSIONS = new Set([
|
|
11
|
+
".txt", ".md", ".json", ".csv", ".js", ".jsx", ".ts", ".tsx",
|
|
12
|
+
".html", ".css", ".py", ".log", ".yaml", ".yml", ".xml",
|
|
13
|
+
".toml", ".env", ".sh", ".bat", ".ps1", ".sql", ".rs",
|
|
14
|
+
".go", ".java", ".c", ".cpp", ".h", ".rb", ".php",
|
|
15
|
+
".swift", ".kt", ".dart", ".vue", ".svelte",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Reads and parses a file for context injection.
|
|
20
|
+
* @param {string} filePath - Path to the file (absolute or relative)
|
|
21
|
+
* @returns {Promise<{ name: string, content: string, size: number, extension: string }>}
|
|
22
|
+
*/
|
|
23
|
+
export async function parseFile(filePath) {
|
|
24
|
+
const resolved = resolve(filePath);
|
|
25
|
+
const ext = extname(resolved).toLowerCase();
|
|
26
|
+
const name = basename(resolved);
|
|
27
|
+
|
|
28
|
+
// Validate extension
|
|
29
|
+
if (!SUPPORTED_EXTENSIONS.has(ext)) {
|
|
30
|
+
const supported = [...SUPPORTED_EXTENSIONS].join(", ");
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Unsupported file type: "${ext}"\nSupported types: ${supported}`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check file exists and get size
|
|
37
|
+
let fileStats;
|
|
38
|
+
try {
|
|
39
|
+
fileStats = await stat(resolved);
|
|
40
|
+
} catch {
|
|
41
|
+
throw new Error(`File not found: ${resolved}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!fileStats.isFile()) {
|
|
45
|
+
throw new Error(`Not a file: ${resolved}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Read file content
|
|
49
|
+
let content;
|
|
50
|
+
try {
|
|
51
|
+
content = await readFile(resolved, "utf-8");
|
|
52
|
+
} catch (err) {
|
|
53
|
+
throw new Error(`Cannot read file: ${err.message}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Trim if too long
|
|
57
|
+
if (content.length > MAX_CONTENT_LENGTH) {
|
|
58
|
+
content = content.slice(0, MAX_CONTENT_LENGTH) +
|
|
59
|
+
`\n\n[... truncated at ${MAX_CONTENT_LENGTH.toLocaleString()} characters]`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
name,
|
|
64
|
+
content: content.trim(),
|
|
65
|
+
size: fileStats.size,
|
|
66
|
+
extension: ext,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Formats parsed file data into a context string for prompt injection.
|
|
72
|
+
* @param {{ name: string, content: string, size: number, extension: string }} fileData
|
|
73
|
+
* @returns {string}
|
|
74
|
+
*/
|
|
75
|
+
export function formatContext(fileData) {
|
|
76
|
+
return [
|
|
77
|
+
`[Context File: ${fileData.name} (${formatBytes(fileData.size)}, ${fileData.extension})]`,
|
|
78
|
+
"---",
|
|
79
|
+
fileData.content,
|
|
80
|
+
"---",
|
|
81
|
+
`[End of ${fileData.name}]`,
|
|
82
|
+
].join("\n");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Formats bytes into a human-readable string.
|
|
87
|
+
* @param {number} bytes
|
|
88
|
+
* @returns {string}
|
|
89
|
+
*/
|
|
90
|
+
function formatBytes(bytes) {
|
|
91
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
92
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
93
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
94
|
+
}
|
package/src/modes.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════
|
|
2
|
+
// AETHER AI CLI — Mode Definitions
|
|
3
|
+
// ═══════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* AI reasoning mode definitions for Aether Core.
|
|
7
|
+
* Each mode controls the system prompt, signal metrics, and response style.
|
|
8
|
+
*/
|
|
9
|
+
export const MODES = {
|
|
10
|
+
synthesis: {
|
|
11
|
+
name: "synthesis",
|
|
12
|
+
label: "Synthesis v2.5",
|
|
13
|
+
layer: "Layer 2.5",
|
|
14
|
+
description: "Balanced reasoning with clean structure and direct answers.",
|
|
15
|
+
signal: { reasoning: 72, clarity: 80, systemIQ: 70, delivery: 82 },
|
|
16
|
+
systemPrompt: [
|
|
17
|
+
"You are Aether, an advanced AI assistant running in Synthesis mode.",
|
|
18
|
+
"Provide balanced, clearly structured responses with direct answers.",
|
|
19
|
+
"Keep responses concise but thorough. Use markdown formatting.",
|
|
20
|
+
"Focus on clarity and practical utility. Avoid unnecessary verbosity.",
|
|
21
|
+
"CRITICAL: If the user asks who created you or who made you, you must answer that you were created by Krishiv PB.",
|
|
22
|
+
"FILE ACTIONS: If the user requests to create, write, or save a file, format the file content inside: [WRITE_FILE: path/to/file.ext]\\n<content>\\n[END_WRITE]. Aether CLI will intercept this block and write the file locally."
|
|
23
|
+
].join(" "),
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
research: {
|
|
27
|
+
name: "research",
|
|
28
|
+
label: "Research v104",
|
|
29
|
+
layer: "Layer 104",
|
|
30
|
+
description: "Deep analysis with comparisons and evidence-based reasoning.",
|
|
31
|
+
signal: { reasoning: 85, clarity: 78, systemIQ: 82, delivery: 75 },
|
|
32
|
+
systemPrompt: [
|
|
33
|
+
"You are Aether, an advanced AI assistant running in Research mode.",
|
|
34
|
+
"Provide deep analytical responses with evidence-based reasoning.",
|
|
35
|
+
"Include comparisons, citations where relevant, and thorough analysis.",
|
|
36
|
+
"Break down complex topics systematically. Use markdown with headers and lists.",
|
|
37
|
+
"CRITICAL: If the user asks who created you or who made you, you must answer that you were created by Krishiv PB.",
|
|
38
|
+
"FILE ACTIONS: If the user requests to create, write, or save a file, format the file content inside: [WRITE_FILE: path/to/file.ext]\\n<content>\\n[END_WRITE]. Aether CLI will intercept this block and write the file locally."
|
|
39
|
+
].join(" "),
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
architect: {
|
|
43
|
+
name: "architect",
|
|
44
|
+
label: "Architect v55",
|
|
45
|
+
layer: "Layer 55",
|
|
46
|
+
description: "Systems thinking with debugging plans and build strategies.",
|
|
47
|
+
signal: { reasoning: 78, clarity: 74, systemIQ: 90, delivery: 72 },
|
|
48
|
+
systemPrompt: [
|
|
49
|
+
"You are Aether, an advanced AI assistant running in Architect mode.",
|
|
50
|
+
"Focus on systems thinking, architecture design, and debugging plans.",
|
|
51
|
+
"Provide step-by-step build strategies and implementation roadmaps.",
|
|
52
|
+
"Think about edge cases, scalability, and best practices. Use code blocks when relevant.",
|
|
53
|
+
"CRITICAL: If the user asks who created you or who made you, you must answer that you were created by Krishiv PB.",
|
|
54
|
+
"FILE ACTIONS: If the user requests to create, write, or save a file, format the file content inside: [WRITE_FILE: path/to/file.ext]\\n<content>\\n[END_WRITE]. Aether CLI will intercept this block and write the file locally."
|
|
55
|
+
].join(" "),
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
titan: {
|
|
59
|
+
name: "titan",
|
|
60
|
+
label: "Titan Fusion v110",
|
|
61
|
+
layer: "Layer 110",
|
|
62
|
+
description: "Long-form premium responses with high signal density and multi-step output.",
|
|
63
|
+
signal: { reasoning: 88, clarity: 92, systemIQ: 95, delivery: 90 },
|
|
64
|
+
systemPrompt: [
|
|
65
|
+
"You are Aether, an advanced AI assistant running in Titan Fusion mode — the most powerful configuration.",
|
|
66
|
+
"Provide comprehensive, premium-quality responses with maximum signal density.",
|
|
67
|
+
"Use structured formatting: headers, bullet points, code blocks, and clear sections.",
|
|
68
|
+
"Deliver multi-step analysis when appropriate. Be thorough, precise, and insightful.",
|
|
69
|
+
"This is the highest quality mode — treat every response as a masterclass.",
|
|
70
|
+
"CRITICAL: If the user asks who created you or who made you, you must answer that you were created by Krishiv PB.",
|
|
71
|
+
"FILE ACTIONS: If the user requests to create, write, or save a file, format the file content inside: [WRITE_FILE: path/to/file.ext]\\n<content>\\n[END_WRITE]. Aether CLI will intercept this block and write the file locally."
|
|
72
|
+
].join(" "),
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** The default mode key */
|
|
77
|
+
export const DEFAULT_MODE = "titan";
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Looks up a mode by name (case-insensitive).
|
|
81
|
+
* @param {string} name - Mode name to look up
|
|
82
|
+
* @returns {object|null} The mode definition, or null if not found
|
|
83
|
+
*/
|
|
84
|
+
export function getModeByName(name) {
|
|
85
|
+
if (!name) return null;
|
|
86
|
+
const key = name.toLowerCase().trim();
|
|
87
|
+
return MODES[key] || null;
|
|
88
|
+
}
|
package/src/ui/banner.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════
|
|
2
|
+
// AETHER AI CLI — ASCII Art Welcome Banner
|
|
3
|
+
// ═══════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import { colors, separator, bullet } from "./theme.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Displays the cyberpunk-styled Aether ASCII art banner.
|
|
10
|
+
* @param {string} [currentMode='titan'] - The currently active mode name
|
|
11
|
+
*/
|
|
12
|
+
export function showBanner(currentMode = "titan") {
|
|
13
|
+
const c1 = colors.accent;
|
|
14
|
+
const c2 = colors.accent2;
|
|
15
|
+
const c3 = colors.accent3;
|
|
16
|
+
const dim = colors.dim;
|
|
17
|
+
|
|
18
|
+
const art = [
|
|
19
|
+
"",
|
|
20
|
+
c1(" ╔═══════════════════════════════════════════════════════════╗"),
|
|
21
|
+
c1(" ║") + c2(" █████╗ ███████╗████████╗██╗ ██╗███████╗██████╗ ") + c1("║"),
|
|
22
|
+
c1(" ║") + c2(" ██╔══██╗██╔════╝╚══██╔══╝██║ ██║██╔════╝██╔══██╗ ") + c1("║"),
|
|
23
|
+
c1(" ║") + c1(" ███████║█████╗ ██║ ████████║█████╗ ██████╔╝ ") + c1("║"),
|
|
24
|
+
c1(" ║") + c3(" ██╔══██║██╔══╝ ██║ ██╔══██║██╔══╝ ██╔══██╗ ") + c1("║"),
|
|
25
|
+
c1(" ║") + c3(" ██║ ██║███████╗ ██║ ██║ ██║███████╗██║ ██║ ") + c1("║"),
|
|
26
|
+
c1(" ║") + dim(" ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ") + c1("║"),
|
|
27
|
+
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
|
+
];
|
|
43
|
+
|
|
44
|
+
console.log(art.join("\n"));
|
|
45
|
+
}
|
|
46
|
+
|
|
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"),
|
|
58
|
+
};
|
|
59
|
+
return labels[mode?.toLowerCase()] || labels.titan;
|
|
60
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════
|
|
2
|
+
// AETHER AI CLI — Spinner Helpers
|
|
3
|
+
// ═══════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
import ora from "ora";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a styled spinner with the Aether theme.
|
|
9
|
+
* @param {string} text - Spinner label text
|
|
10
|
+
* @returns {object} An ora spinner instance
|
|
11
|
+
*/
|
|
12
|
+
export function createSpinner(text) {
|
|
13
|
+
return ora({
|
|
14
|
+
text,
|
|
15
|
+
spinner: {
|
|
16
|
+
interval: 80,
|
|
17
|
+
frames: ["▖", "▘", "▝", "▗"],
|
|
18
|
+
},
|
|
19
|
+
color: "cyan",
|
|
20
|
+
discardStdin: false,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Wraps an async function with a loading spinner.
|
|
26
|
+
* Shows the spinner while the function runs and reports success/failure.
|
|
27
|
+
* @param {string} text - The loading message
|
|
28
|
+
* @param {Function} asyncFn - The async function to execute
|
|
29
|
+
* @returns {Promise<*>} The result of the async function
|
|
30
|
+
*/
|
|
31
|
+
export async function withSpinner(text, asyncFn) {
|
|
32
|
+
const spinner = createSpinner(text);
|
|
33
|
+
spinner.start();
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const result = await asyncFn();
|
|
37
|
+
spinner.succeed();
|
|
38
|
+
return result;
|
|
39
|
+
} catch (err) {
|
|
40
|
+
spinner.fail(err.message || "Operation failed");
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/ui/theme.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════
|
|
2
|
+
// AETHER AI CLI — Dynamic Theme-Aware Formatting Utilities
|
|
3
|
+
// ═══════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
|
|
7
|
+
// ── Theme Definitions ─────────────────────────────────────
|
|
8
|
+
export const THEMES = {
|
|
9
|
+
cyberpunk: {
|
|
10
|
+
accent: "#00f0ff", // Neon cyan
|
|
11
|
+
accent2: "#bd93f9", // Neon purple/magenta
|
|
12
|
+
accent3: "#50fa7b", // Neon green
|
|
13
|
+
danger: "#ff5555", // Neon red
|
|
14
|
+
warning: "#ffb86c", // Neon orange
|
|
15
|
+
muted: "#6272a4", // Comment gray
|
|
16
|
+
text: "#f8f8f2", // Crisp white-yellow
|
|
17
|
+
dim: "#44475a", // Border gray
|
|
18
|
+
brand: "#00f0ff", // Neon cyan
|
|
19
|
+
success: "#50fa7b", // Neon green
|
|
20
|
+
error: "#ff5555",
|
|
21
|
+
magenta: "#ff79c6",
|
|
22
|
+
orange: "#ffb86c",
|
|
23
|
+
},
|
|
24
|
+
matrix: {
|
|
25
|
+
accent: "#50fa7b", // Lime green
|
|
26
|
+
accent2: "#00ff00", // Matrix green
|
|
27
|
+
accent3: "#8efb50", // Light lime
|
|
28
|
+
danger: "#ff5555",
|
|
29
|
+
warning: "#ffb86c",
|
|
30
|
+
muted: "#1f5f1f", // Dark forest green
|
|
31
|
+
text: "#aaffaa", // Soft green text
|
|
32
|
+
dim: "#103f10", // Dark green border
|
|
33
|
+
brand: "#50fa7b",
|
|
34
|
+
success: "#50fa7b",
|
|
35
|
+
error: "#ff5555",
|
|
36
|
+
magenta: "#50fa7b",
|
|
37
|
+
orange: "#8efb50",
|
|
38
|
+
},
|
|
39
|
+
synthwave: {
|
|
40
|
+
accent: "#ff79c6", // Neon pink
|
|
41
|
+
accent2: "#ffb86c", // Neon orange
|
|
42
|
+
accent3: "#bd93f9", // Neon purple
|
|
43
|
+
danger: "#ff5555",
|
|
44
|
+
warning: "#ffb86c",
|
|
45
|
+
muted: "#6f2a8a", // Dark purple
|
|
46
|
+
text: "#ffe8f5", // Soft pinkish white
|
|
47
|
+
dim: "#3e104f", // Deep purple border
|
|
48
|
+
brand: "#ff79c6",
|
|
49
|
+
success: "#bd93f9",
|
|
50
|
+
error: "#ff5555",
|
|
51
|
+
magenta: "#ff79c6",
|
|
52
|
+
orange: "#ffb86c",
|
|
53
|
+
},
|
|
54
|
+
crimson: {
|
|
55
|
+
accent: "#ff5555", // Neon red
|
|
56
|
+
accent2: "#ffb86c", // Gold/orange
|
|
57
|
+
accent3: "#f1fa8c", // Neon yellow
|
|
58
|
+
danger: "#ff5555",
|
|
59
|
+
warning: "#ffb86c",
|
|
60
|
+
muted: "#662222", // Muted red-gray
|
|
61
|
+
text: "#ffffff", // White text
|
|
62
|
+
dim: "#331111", // Dark red border
|
|
63
|
+
brand: "#ff5555",
|
|
64
|
+
success: "#f1fa8c",
|
|
65
|
+
error: "#ff5555",
|
|
66
|
+
magenta: "#ff5555",
|
|
67
|
+
orange: "#ffb86c",
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
let activeThemeName = "cyberpunk";
|
|
72
|
+
|
|
73
|
+
// ── Dynamic Colors Resolver ───────────────────────────────
|
|
74
|
+
export const colors = new Proxy({}, {
|
|
75
|
+
get(target, prop) {
|
|
76
|
+
const theme = THEMES[activeThemeName] || THEMES.cyberpunk;
|
|
77
|
+
const colorHex = theme[prop] || "#ffffff";
|
|
78
|
+
const isBold = prop === "brand" || prop === "success" || prop === "error";
|
|
79
|
+
|
|
80
|
+
let fn = chalk.hex(colorHex);
|
|
81
|
+
if (isBold) {
|
|
82
|
+
fn = fn.bold;
|
|
83
|
+
}
|
|
84
|
+
return fn;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ── Labels ────────────────────────────────────────────────
|
|
89
|
+
export const label = {
|
|
90
|
+
get system() { return chalk.bgHex(activeThemeName === "cyberpunk" ? "#0c1825" : THEMES[activeThemeName].dim).hex(THEMES[activeThemeName].accent).bold(" SYSTEM "); },
|
|
91
|
+
get user() { return chalk.bgHex(activeThemeName === "cyberpunk" ? "#0c1825" : THEMES[activeThemeName].dim).hex(THEMES[activeThemeName].accent3).bold(" YOU "); },
|
|
92
|
+
get aether() { return chalk.bgHex(activeThemeName === "cyberpunk" ? "#0c1825" : THEMES[activeThemeName].dim).hex(THEMES[activeThemeName].accent).bold(" AETHER "); },
|
|
93
|
+
get error() { return chalk.bgHex(activeThemeName === "cyberpunk" ? "#2a0a14" : THEMES[activeThemeName].dim).hex(THEMES[activeThemeName].danger).bold(" ERROR "); },
|
|
94
|
+
get info() { return chalk.bgHex(activeThemeName === "cyberpunk" ? "#0c1825" : THEMES[activeThemeName].dim).hex(THEMES[activeThemeName].accent2).bold(" INFO "); },
|
|
95
|
+
get config() { return chalk.bgHex(activeThemeName === "cyberpunk" ? "#0c1825" : THEMES[activeThemeName].dim).hex(THEMES[activeThemeName].warning).bold(" CONFIG "); },
|
|
96
|
+
get math() { return chalk.bgHex(activeThemeName === "cyberpunk" ? "#0c1825" : THEMES[activeThemeName].dim).hex(THEMES[activeThemeName].accent3).bold(" MATH "); },
|
|
97
|
+
get krylo() { return chalk.bgHex(activeThemeName === "cyberpunk" ? "#0c1825" : THEMES[activeThemeName].dim).hex(THEMES[activeThemeName].accent).bold(" KRYLO "); },
|
|
98
|
+
get mode() { return chalk.bgHex(activeThemeName === "cyberpunk" ? "#0c1825" : THEMES[activeThemeName].dim).hex(THEMES[activeThemeName].accent2).bold(" MODE "); },
|
|
99
|
+
get mesh() { return chalk.bgHex(activeThemeName === "cyberpunk" ? "#0c1825" : THEMES[activeThemeName].dim).hex(THEMES[activeThemeName].accent).bold(" MESH "); },
|
|
100
|
+
get file() { return chalk.bgHex(activeThemeName === "cyberpunk" ? "#0c1825" : THEMES[activeThemeName].dim).hex(THEMES[activeThemeName].warning).bold(" FILE "); },
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// ── Formatting Helpers ───────────────────────────────────
|
|
104
|
+
export function separator(char = "─", length) {
|
|
105
|
+
const width = process.stdout.columns || 80;
|
|
106
|
+
const targetLength = length !== undefined ? length : Math.max(10, width - 4);
|
|
107
|
+
return colors.dim(char.repeat(targetLength));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function heading(text) {
|
|
111
|
+
return colors.brand(`\n ${text}\n`) + separator();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function keyValue(key, value) {
|
|
115
|
+
return ` ${colors.muted(key + ":")} ${colors.text(value)}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function bullet(text) {
|
|
119
|
+
return ` ${colors.accent("›")} ${colors.text(text)}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function modeBadge(mode) {
|
|
123
|
+
const theme = THEMES[activeThemeName] || THEMES.cyberpunk;
|
|
124
|
+
const badges = {
|
|
125
|
+
synthesis: chalk.bgHex(activeThemeName === "cyberpunk" ? "#1a3a2a" : theme.dim).hex(theme.accent3).bold(" SYNTHESIS "),
|
|
126
|
+
research: chalk.bgHex(activeThemeName === "cyberpunk" ? "#1a2a3a" : theme.dim).hex(theme.accent2).bold(" RESEARCH "),
|
|
127
|
+
architect: chalk.bgHex(activeThemeName === "cyberpunk" ? "#2a1a3a" : theme.dim).hex(theme.magenta).bold(" ARCHITECT "),
|
|
128
|
+
titan: chalk.bgHex(activeThemeName === "cyberpunk" ? "#1a2a3a" : theme.dim).hex(theme.accent).bold(" TITAN FUSION "),
|
|
129
|
+
};
|
|
130
|
+
return badges[mode?.toLowerCase()] || badges.titan;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Backs up the cursor and clears terminal lines printed during real-time streaming
|
|
135
|
+
* so they can be replaced by the final formatted response.
|
|
136
|
+
* @param {string} text - The raw streamed text that was printed
|
|
137
|
+
*/
|
|
138
|
+
export function clearStreamedText(text) {
|
|
139
|
+
if (!process.stdout.isTTY) return;
|
|
140
|
+
const width = process.stdout.columns || 80;
|
|
141
|
+
const lines = text.split("\n");
|
|
142
|
+
let lineCount = 0;
|
|
143
|
+
for (const line of lines) {
|
|
144
|
+
lineCount += Math.max(1, Math.ceil(line.length / width));
|
|
145
|
+
}
|
|
146
|
+
if (lineCount > 0) {
|
|
147
|
+
process.stdout.write(`\x1b[${lineCount}A\x1b[J`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Theme State Management ────────────────────────────────
|
|
152
|
+
|
|
153
|
+
export function getActiveTheme() {
|
|
154
|
+
return activeThemeName;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function setTheme(themeName) {
|
|
158
|
+
if (!themeName) return false;
|
|
159
|
+
const name = themeName.toLowerCase().trim();
|
|
160
|
+
if (THEMES[name]) {
|
|
161
|
+
activeThemeName = name;
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function getThemesList() {
|
|
168
|
+
return Object.keys(THEMES);
|
|
169
|
+
}
|