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.
- aether_ai_agent_cli-1.1.4.dist-info/METADATA +309 -0
- aether_ai_agent_cli-1.1.4.dist-info/RECORD +25 -0
- aether_ai_agent_cli-1.1.4.dist-info/WHEEL +5 -0
- aether_ai_agent_cli-1.1.4.dist-info/entry_points.txt +2 -0
- aether_ai_agent_cli-1.1.4.dist-info/licenses/LICENSE +21 -0
- aether_ai_agent_cli-1.1.4.dist-info/top_level.txt +1 -0
- aether_pip/__init__.py +1 -0
- aether_pip/cli.py +49 -0
- aether_pip/node_project/bin/aether.js +10 -0
- aether_pip/node_project/package-lock.json +794 -0
- aether_pip/node_project/package.json +46 -0
- aether_pip/node_project/src/ai/fallback.js +179 -0
- aether_pip/node_project/src/ai/google.js +87 -0
- aether_pip/node_project/src/ai/providers.js +203 -0
- aether_pip/node_project/src/ai/router.js +114 -0
- aether_pip/node_project/src/ai/universal.js +507 -0
- aether_pip/node_project/src/ai/xai.js +50 -0
- aether_pip/node_project/src/chat.js +1018 -0
- aether_pip/node_project/src/cli.js +679 -0
- aether_pip/node_project/src/config.js +214 -0
- aether_pip/node_project/src/file-parser.js +94 -0
- aether_pip/node_project/src/modes.js +121 -0
- aether_pip/node_project/src/ui/banner.js +60 -0
- aether_pip/node_project/src/ui/spinner.js +43 -0
- aether_pip/node_project/src/ui/theme.js +169 -0
|
@@ -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
|
+
}
|