@krishivpb60/aether-ai-cli 1.3.4 → 1.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/HIGHLIGHTS.md +13 -0
- package/package.json +1 -1
- package/src/ai/telemetry.js +56 -3
- package/src/chat.js +2184 -1692
- package/src/cli.js +31 -0
- package/src/dashboard.js +112 -0
- package/src/telemetry-server.js +855 -0
- package/src/ui/dashboard.html +834 -0
- package/test/autopilot-debug.test.js +91 -0
- package/test/git-tui.test.js +94 -0
- package/test/telemetry.test.js +104 -0
package/src/chat.js
CHANGED
|
@@ -1,1692 +1,2184 @@
|
|
|
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
|
-
StreamFilter,
|
|
24
|
-
stripCodeFences,
|
|
25
|
-
getActiveTheme,
|
|
26
|
-
setTheme,
|
|
27
|
-
getThemesList,
|
|
28
|
-
interactiveMenu
|
|
29
|
-
} from "./ui/theme.js";
|
|
30
|
-
import { createSpinner } from "./ui/spinner.js";
|
|
31
|
-
import { showBanner } from "./ui/banner.js";
|
|
32
|
-
import { routePrompt } from "./ai/router.js";
|
|
33
|
-
import { getActiveProviders } from "./ai/providers.js";
|
|
34
|
-
import {
|
|
35
|
-
getAIConfig,
|
|
36
|
-
loadHistory,
|
|
37
|
-
saveHistory,
|
|
38
|
-
clearHistory,
|
|
39
|
-
setConfigValue,
|
|
40
|
-
listSessions,
|
|
41
|
-
switchSession,
|
|
42
|
-
startNewSession
|
|
43
|
-
} from "./config.js";
|
|
44
|
-
import { MODES, DEFAULT_MODE, getModeByName } from "./modes.js";
|
|
45
|
-
import { parseFile, formatContext } from "./file-parser.js";
|
|
46
|
-
import { runMainframeHack } from "./ai/fallback.js";
|
|
47
|
-
import { AGENT_INSTRUCTIONS } from "./agent.js";
|
|
48
|
-
import { checkForUpdates } from "./updater.js";
|
|
49
|
-
import { getSessionTokenStats, getBreakdownByModel, resetSessionTokenStats } from "./ai/tokens.js";
|
|
50
|
-
import { getGitDiff } from "./git.js";
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// Configure marked dynamically for terminal output
|
|
55
|
-
const getMarked = () => new Marked(markedTerminal({
|
|
56
|
-
reflowText: true,
|
|
57
|
-
width: process.stdout.columns ? Math.max(20, process.stdout.columns - 4) : 80,
|
|
58
|
-
showSectionPrefix: false,
|
|
59
|
-
code: (c) => colors.orange(c),
|
|
60
|
-
codespan: (c) => colors.accent3(c),
|
|
61
|
-
heading: (h) => colors.accent.bold(h),
|
|
62
|
-
strong: (s) => colors.magenta.bold(s),
|
|
63
|
-
em: chalk.italic,
|
|
64
|
-
hr: (h) => colors.dim(h),
|
|
65
|
-
}));
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Starts the interactive Aether chat session.
|
|
69
|
-
* @param {{ mode?: string, preferredProvider?: string }} [options={}]
|
|
70
|
-
*/
|
|
71
|
-
export async function startChat(options = {}) {
|
|
72
|
-
// Load AI config
|
|
73
|
-
const aiConfig = await getAIConfig();
|
|
74
|
-
|
|
75
|
-
// Run update check
|
|
76
|
-
await checkForUpdates();
|
|
77
|
-
|
|
78
|
-
// Reset token stats for the new session
|
|
79
|
-
resetSessionTokenStats();
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
// Set theme from configuration
|
|
83
|
-
const theme = aiConfig.THEME || "cyberpunk";
|
|
84
|
-
setTheme(theme);
|
|
85
|
-
|
|
86
|
-
let currentMode = getModeByName(options.mode) || getModeByName(aiConfig.DEFAULT_MODE) || MODES[DEFAULT_MODE];
|
|
87
|
-
let attachedFiles = [];
|
|
88
|
-
|
|
89
|
-
// Persistent history loader
|
|
90
|
-
const history = await loadHistory();
|
|
91
|
-
|
|
92
|
-
// Mini-game state
|
|
93
|
-
const game = {
|
|
94
|
-
active: false,
|
|
95
|
-
code: "",
|
|
96
|
-
attempts: 0,
|
|
97
|
-
maxAttempts: 6,
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
// Show banner
|
|
101
|
-
showBanner(currentMode.name);
|
|
102
|
-
|
|
103
|
-
// Active providers diagnostic check
|
|
104
|
-
const active = getActiveProviders(aiConfig);
|
|
105
|
-
if (active.length === 0) {
|
|
106
|
-
console.log(
|
|
107
|
-
"\n" + label.system + " " +
|
|
108
|
-
colors.warning("No API keys configured. Using local fallback solvers.") + "\n" +
|
|
109
|
-
" " + colors.muted("Run ") + colors.accent("aether setup") +
|
|
110
|
-
colors.muted(" to configure providers (free options available!).\n")
|
|
111
|
-
);
|
|
112
|
-
} else {
|
|
113
|
-
const providerNames = active.map((a) => a.provider.name);
|
|
114
|
-
const unique = [...new Set(providerNames)];
|
|
115
|
-
console.log(
|
|
116
|
-
label.mesh + " " +
|
|
117
|
-
colors.accent("Failover mesh online: ") +
|
|
118
|
-
colors.text(unique.join(" → ")) +
|
|
119
|
-
colors.muted(" → Krylo fallback")
|
|
120
|
-
);
|
|
121
|
-
console.log(
|
|
122
|
-
" " + colors.dim(`${active.length} node(s) active across ${unique.length} provider(s)`) + "\n"
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Display loaded history message if any
|
|
127
|
-
if (history.length > 0) {
|
|
128
|
-
console.log(
|
|
129
|
-
" " + label.info + " " +
|
|
130
|
-
colors.muted(`Restored ${Math.floor(history.length / 2)} message exchanges from persistent logs.`) + "\n"
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Completer: handles commands & dynamic local file path autocomplete
|
|
135
|
-
const completer = (line) => {
|
|
136
|
-
const builtIn = [
|
|
137
|
-
"/help", "/mode", "/modes", "/attach", "/files", "/clear",
|
|
138
|
-
"/providers", "/export", "/status", "/copy", "/exit", "/quit",
|
|
139
|
-
"/theme", "/themes", "/history-clear", "/game", "/abort", "/cmd", "/write",
|
|
140
|
-
"/commit", "/run", "/history", "/autopilot", "/tokens", "/update",
|
|
141
|
-
"/review", "/diagnose", "/explain", "/refactor", "/bug", "/doc", "/translate",
|
|
142
|
-
"/search"
|
|
143
|
-
];
|
|
144
|
-
const customCmds = aiConfig.CUSTOM_COMMANDS || {};
|
|
145
|
-
const commands = [...builtIn, ...Object.keys(customCmds)];
|
|
146
|
-
|
|
147
|
-
// File path autocompletion on /attach
|
|
148
|
-
if (line.startsWith("/attach ")) {
|
|
149
|
-
const query = line.slice(8);
|
|
150
|
-
const lastSlash = Math.max(query.lastIndexOf("/"), query.lastIndexOf("\\"));
|
|
151
|
-
let searchDir = ".";
|
|
152
|
-
let searchPrefix = query;
|
|
153
|
-
|
|
154
|
-
if (lastSlash !== -1) {
|
|
155
|
-
searchDir = query.slice(0, lastSlash);
|
|
156
|
-
if (searchDir === "") {
|
|
157
|
-
searchDir = sep;
|
|
158
|
-
}
|
|
159
|
-
searchPrefix = query.slice(lastSlash + 1);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
try {
|
|
163
|
-
const resolved = resolve(searchDir);
|
|
164
|
-
if (existsSync(resolved) && statSync(resolved).isDirectory()) {
|
|
165
|
-
const files = readdirSync(resolved);
|
|
166
|
-
const hits = files
|
|
167
|
-
.filter((f) => f.toLowerCase().startsWith(searchPrefix.toLowerCase()) && !f.startsWith("."))
|
|
168
|
-
.map((f) => {
|
|
169
|
-
const fullPath = searchDir === "." || searchDir === sep ? f : join(searchDir, f);
|
|
170
|
-
const fullResolved = resolve(fullPath);
|
|
171
|
-
const isDir = statSync(fullResolved).isDirectory();
|
|
172
|
-
return `/attach ${fullPath}${isDir ? "/" : ""}`;
|
|
173
|
-
});
|
|
174
|
-
return [hits.length ? hits : [], line];
|
|
175
|
-
}
|
|
176
|
-
} catch (e) {
|
|
177
|
-
// Fallback silently on fs errors
|
|
178
|
-
}
|
|
179
|
-
return [[], line];
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Sub-arguments autocomplete on /mode
|
|
183
|
-
if (line.startsWith("/mode ")) {
|
|
184
|
-
const query = line.slice(6).toLowerCase();
|
|
185
|
-
const modesList = Object.keys(MODES);
|
|
186
|
-
const hits = modesList
|
|
187
|
-
.filter((m) => m.startsWith(query))
|
|
188
|
-
.map((m) => `/mode ${m}`);
|
|
189
|
-
return [hits.length ? hits : [], line];
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Sub-arguments autocomplete on /theme
|
|
193
|
-
if (line.startsWith("/theme ")) {
|
|
194
|
-
const query = line.slice(7).toLowerCase();
|
|
195
|
-
const themesList = getThemesList();
|
|
196
|
-
const hits = themesList
|
|
197
|
-
.filter((t) => t.startsWith(query))
|
|
198
|
-
.map((t) => `/theme ${t}`);
|
|
199
|
-
return [hits.length ? hits : [], line];
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Sub-arguments autocomplete on /cmd
|
|
203
|
-
if (line.startsWith("/cmd ")) {
|
|
204
|
-
const query = line.slice(5).toLowerCase();
|
|
205
|
-
const subcmds = ["list", "add", "remove"];
|
|
206
|
-
const hits = subcmds
|
|
207
|
-
.filter((s) => s.startsWith(query))
|
|
208
|
-
.map((s) => `/cmd ${s}`);
|
|
209
|
-
return [hits.length ? hits : [], line];
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const hits = commands.filter((c) => c.startsWith(line));
|
|
213
|
-
return [hits.length ? hits : [], line];
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
// Create readline interface
|
|
217
|
-
const rl = createInterface({
|
|
218
|
-
input: process.stdin,
|
|
219
|
-
output: process.stdout,
|
|
220
|
-
prompt: colors.accent(" ❯ "),
|
|
221
|
-
terminal: true,
|
|
222
|
-
completer
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
// Load persistent history entries directly into the shell up/down array
|
|
226
|
-
if (history.length > 0) {
|
|
227
|
-
const userQueries = history
|
|
228
|
-
.filter((h) => h.role === "user")
|
|
229
|
-
.map((h) => h.content);
|
|
230
|
-
// Readline history is structured newest first (index 0)
|
|
231
|
-
rl.history = [...new Set(userQueries)].reverse();
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// ── AI Execution Helper ──────────────────────────────────
|
|
235
|
-
async function executeAIQuery(promptText, originalInput = promptText) {
|
|
236
|
-
// ── Build Prompt with Context ─────────────────────────
|
|
237
|
-
let fullPrompt = promptText;
|
|
238
|
-
if (attachedFiles.length > 0) {
|
|
239
|
-
const contexts = attachedFiles.map((f) => formatContext(f)).join("\n\n");
|
|
240
|
-
fullPrompt = `${contexts}\n\n${promptText}`;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Append AGENT_INSTRUCTIONS to mode systemPrompt
|
|
244
|
-
const systemPrompt = currentMode.systemPrompt + "\n" + AGENT_INSTRUCTIONS;
|
|
245
|
-
|
|
246
|
-
let loopCount = 0;
|
|
247
|
-
const MAX_LOOPS = 5;
|
|
248
|
-
let currentQueryPrompt = fullPrompt;
|
|
249
|
-
let aiResponseText = "";
|
|
250
|
-
let lastResult = null;
|
|
251
|
-
|
|
252
|
-
try {
|
|
253
|
-
while (loopCount < MAX_LOOPS) {
|
|
254
|
-
const queryStartTime = Date.now();
|
|
255
|
-
let firstTokenTime = 0;
|
|
256
|
-
|
|
257
|
-
if (loopCount > 0) {
|
|
258
|
-
console.log(colors.accent(`\n🤖 [Aether Autopilot Mode - Iteration ${loopCount + 1}/${MAX_LOOPS}]`));
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const spinner = createSpinner(
|
|
262
|
-
colors.muted(loopCount === 0 ? `Routing through mesh ${currentMode.label}...` : `Agent executing tasks...`)
|
|
263
|
-
);
|
|
264
|
-
spinner.start();
|
|
265
|
-
|
|
266
|
-
let hasStartedStreaming = false;
|
|
267
|
-
let streamedText = "";
|
|
268
|
-
const filter = new StreamFilter(process.stdout.write.bind(process.stdout));
|
|
269
|
-
const onToken = (token) => {
|
|
270
|
-
if (!hasStartedStreaming) {
|
|
271
|
-
hasStartedStreaming = true;
|
|
272
|
-
firstTokenTime = Date.now();
|
|
273
|
-
spinner.stop();
|
|
274
|
-
}
|
|
275
|
-
filter.write(token);
|
|
276
|
-
streamedText += token;
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
const result = await routePrompt(currentQueryPrompt, systemPrompt, aiConfig, onToken, history);
|
|
280
|
-
spinner.stop();
|
|
281
|
-
filter.flush();
|
|
282
|
-
|
|
283
|
-
aiResponseText = result.text;
|
|
284
|
-
lastResult = result;
|
|
285
|
-
|
|
286
|
-
if (hasStartedStreaming) {
|
|
287
|
-
clearStreamedText(filter.filteredText);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Display response
|
|
291
|
-
console.log("");
|
|
292
|
-
console.log(label.aether + " " + providerBadge(result));
|
|
293
|
-
console.log(separator("─"));
|
|
294
|
-
console.log("");
|
|
295
|
-
|
|
296
|
-
if (result.provider === "local" || result.provider === "krylo-fallback") {
|
|
297
|
-
console.log(colors.text(" " + result.text.split("\n").join("\n ")));
|
|
298
|
-
} else {
|
|
299
|
-
let displayText = result.text;
|
|
300
|
-
const rendered = getMarked().parse(displayText);
|
|
301
|
-
console.log(rendered);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const elapsedSec = ((Date.now() - queryStartTime) / 1000).toFixed(1);
|
|
305
|
-
let speedText = "";
|
|
306
|
-
if (firstTokenTime > 0) {
|
|
307
|
-
const streamElapsed = (Date.now() - firstTokenTime) / 1000;
|
|
308
|
-
if (streamElapsed > 0.05) {
|
|
309
|
-
const estimatedTokens = Math.max(1, Math.round(streamedText.length / 4));
|
|
310
|
-
const tps = (estimatedTokens / streamElapsed).toFixed(1);
|
|
311
|
-
speedText = ` • ${tps} tok/s`;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
const showTokens = aiConfig.SHOW_TOKENS !== "false";
|
|
316
|
-
let tokensText = "";
|
|
317
|
-
if (showTokens && result.usage) {
|
|
318
|
-
const { promptTokens, completionTokens } = result.usage;
|
|
319
|
-
tokensText = ` • ${promptTokens.toLocaleString()} in / ${completionTokens.toLocaleString()} out tokens`;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
console.log(separator("─"));
|
|
323
|
-
console.log(
|
|
324
|
-
" " + colors.dim(`Node ${result.node} • ${result.provider}`) +
|
|
325
|
-
(result.model ? colors.dim(` • ${result.model}`) : "") +
|
|
326
|
-
colors.dim(` • ${elapsedSec}s${speedText}`) +
|
|
327
|
-
colors.dim(tokensText) +
|
|
328
|
-
colors.dim(` • ${Math.floor(history.length / 2)} exchanges`)
|
|
329
|
-
);
|
|
330
|
-
console.log("");
|
|
331
|
-
|
|
332
|
-
// Process any agent tools output by the AI
|
|
333
|
-
const { processAgentBlocks } = await import("./agent.js");
|
|
334
|
-
const toolResults = await processAgentBlocks(aiResponseText, aiConfig, rl);
|
|
335
|
-
|
|
336
|
-
if (toolResults.length === 0) {
|
|
337
|
-
// No tools executed, end loop
|
|
338
|
-
break;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Store this turn in history so AI knows what happened
|
|
342
|
-
history.push({ role: "user", content: currentQueryPrompt, timestamp: new Date() });
|
|
343
|
-
history.push({
|
|
344
|
-
role: "assistant",
|
|
345
|
-
content: aiResponseText,
|
|
346
|
-
provider: result.provider,
|
|
347
|
-
model: result.model,
|
|
348
|
-
node: result.node,
|
|
349
|
-
timestamp: new Date(),
|
|
350
|
-
});
|
|
351
|
-
await saveHistory(history, currentMode.name);
|
|
352
|
-
|
|
353
|
-
// Format tool outputs as next prompt
|
|
354
|
-
let formattedResults = "### Agent Tool Outputs:\n";
|
|
355
|
-
for (const tr of toolResults) {
|
|
356
|
-
if (tr.success) {
|
|
357
|
-
if (tr.tool === "RUN_COMMAND") {
|
|
358
|
-
formattedResults += `\n- RUN_COMMAND "${tr.arg}" succeeded. Output:\n\`\`\`\n${tr.stdout || ""}${tr.stderr || ""}\n\`\`\`;`;
|
|
359
|
-
} else if (tr.tool === "READ_FILE") {
|
|
360
|
-
formattedResults += `\n- READ_FILE "${tr.arg}" succeeded. File Content:\n\`\`\`\n${tr.content}\n\`\`\`;`;
|
|
361
|
-
} else if (tr.tool === "WRITE_FILE") {
|
|
362
|
-
formattedResults += `\n- WRITE_FILE "${tr.arg}" succeeded.`;
|
|
363
|
-
} else if (tr.tool === "SEARCH_WEB") {
|
|
364
|
-
const resultsList = tr.results.map((r, i) => `${i+1}. [${r.title}](${r.url})\n ${r.snippet}`).join("\n");
|
|
365
|
-
formattedResults += `\n- SEARCH_WEB "${tr.arg}" succeeded. Results:\n${resultsList}`;
|
|
366
|
-
}
|
|
367
|
-
} else {
|
|
368
|
-
formattedResults += `\n- ${tr.tool} "${tr.arg}" failed: ${tr.error}`;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
formattedResults += "\n\nPlease continue and finalize your task or perform next steps.";
|
|
372
|
-
|
|
373
|
-
currentQueryPrompt = formattedResults;
|
|
374
|
-
loopCount++;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// Store final state in history
|
|
378
|
-
if (loopCount > 0) {
|
|
379
|
-
// Just save to disk to persist
|
|
380
|
-
await saveHistory(history, currentMode.name);
|
|
381
|
-
} else {
|
|
382
|
-
// Standard single-turn save
|
|
383
|
-
history.push({ role: "user", content: originalInput, timestamp: new Date() });
|
|
384
|
-
history.push({
|
|
385
|
-
role: "assistant",
|
|
386
|
-
content: aiResponseText,
|
|
387
|
-
provider: lastResult.provider,
|
|
388
|
-
model: lastResult.model,
|
|
389
|
-
node: lastResult.node,
|
|
390
|
-
timestamp: new Date(),
|
|
391
|
-
});
|
|
392
|
-
await saveHistory(history, currentMode.name);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
} catch (err) {
|
|
396
|
-
console.log("\n" + label.error + " " + colors.danger(err.message) + "\n");
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// Sync shell's recall history list
|
|
400
|
-
const userQueries = history
|
|
401
|
-
.filter((h) => h.role === "user")
|
|
402
|
-
.map((h) => h.content);
|
|
403
|
-
rl.history = [...new Set(userQueries)].reverse();
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
rl.prompt();
|
|
407
|
-
|
|
408
|
-
rl.on("line", async (line) => {
|
|
409
|
-
const input = line.trim();
|
|
410
|
-
if (!input) {
|
|
411
|
-
rl.prompt();
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// ── Handle Game Input ──────────────────────────────────
|
|
416
|
-
if (game.active && !input.startsWith("/")) {
|
|
417
|
-
handleGuess(input, game);
|
|
418
|
-
rl.prompt();
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// ── Handle Slash Commands ──────────────────────────────
|
|
423
|
-
if (input.startsWith("/")) {
|
|
424
|
-
const [cmd, ...args] = input.split(/\s+/);
|
|
425
|
-
const builtInList = [
|
|
426
|
-
"/", "/help", "/mode", "/modes", "/attach", "/files", "/clear",
|
|
427
|
-
"/providers", "/export", "/status", "/copy", "/exit", "/quit",
|
|
428
|
-
"/theme", "/themes", "/history-clear", "/game", "/abort", "/cmd",
|
|
429
|
-
"/guess", "/write", "/commit", "/run", "/history", "/autopilot", "/tokens",
|
|
430
|
-
"/update", "/review", "/diagnose", "/explain", "/refactor", "/bug", "/doc",
|
|
431
|
-
"/translate", "/search"
|
|
432
|
-
];
|
|
433
|
-
|
|
434
|
-
const customCmds = aiConfig.CUSTOM_COMMANDS || {};
|
|
435
|
-
|
|
436
|
-
if (!builtInList.includes(cmd.toLowerCase()) && customCmds[cmd]) {
|
|
437
|
-
const template = customCmds[cmd];
|
|
438
|
-
const userArg = args.join(" ");
|
|
439
|
-
const rewrittenPrompt = template + (userArg ? " " + userArg : "");
|
|
440
|
-
|
|
441
|
-
console.log("\n" + label.system + " " + colors.accent(`Executing custom command: `) + colors.text(cmd));
|
|
442
|
-
console.log(" " + colors.muted("Prompt: ") + colors.text(rewrittenPrompt) + "\n");
|
|
443
|
-
|
|
444
|
-
await executeAIQuery(rewrittenPrompt, input);
|
|
445
|
-
rl.prompt();
|
|
446
|
-
return;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
const handled = await handleCommand(input, {
|
|
450
|
-
currentMode,
|
|
451
|
-
attachedFiles,
|
|
452
|
-
history,
|
|
453
|
-
aiConfig,
|
|
454
|
-
game,
|
|
455
|
-
setMode: (mode) => { currentMode = mode; },
|
|
456
|
-
addFile: (file) => { attachedFiles.push(file); },
|
|
457
|
-
clearFiles: () => { attachedFiles = []; },
|
|
458
|
-
rl,
|
|
459
|
-
});
|
|
460
|
-
if (handled !== "exit") {
|
|
461
|
-
rl.prompt();
|
|
462
|
-
}
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
await executeAIQuery(input);
|
|
467
|
-
rl.prompt();
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
rl.on("close", () => {
|
|
471
|
-
console.log("\n" + label.system + " " + colors.muted("Session terminated. Stay cyberpunk. ⚡\n"));
|
|
472
|
-
process.exit(0);
|
|
473
|
-
});
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
/**
|
|
477
|
-
* Handles slash commands in the chat.
|
|
478
|
-
*/
|
|
479
|
-
async function handleCommand(input, ctx) {
|
|
480
|
-
const [cmd, ...args] = input.split(/\s+/);
|
|
481
|
-
|
|
482
|
-
switch (cmd.toLowerCase()) {
|
|
483
|
-
case "/":
|
|
484
|
-
case "/help":
|
|
485
|
-
showHelp(ctx.aiConfig);
|
|
486
|
-
break;
|
|
487
|
-
|
|
488
|
-
case "/mode":
|
|
489
|
-
handleModeSwitch(args, ctx);
|
|
490
|
-
break;
|
|
491
|
-
|
|
492
|
-
case "/modes":
|
|
493
|
-
showModes();
|
|
494
|
-
break;
|
|
495
|
-
|
|
496
|
-
case "/attach":
|
|
497
|
-
await handleAttach(args, ctx);
|
|
498
|
-
break;
|
|
499
|
-
|
|
500
|
-
case "/files":
|
|
501
|
-
showAttachedFiles(ctx.attachedFiles);
|
|
502
|
-
break;
|
|
503
|
-
|
|
504
|
-
case "/clear":
|
|
505
|
-
// Actual screen clear & scrollback reset
|
|
506
|
-
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
507
|
-
showBanner(ctx.currentMode.name);
|
|
508
|
-
break;
|
|
509
|
-
|
|
510
|
-
case "/export":
|
|
511
|
-
await handleExport(ctx.history);
|
|
512
|
-
break;
|
|
513
|
-
|
|
514
|
-
case "/status":
|
|
515
|
-
showStatus(ctx);
|
|
516
|
-
break;
|
|
517
|
-
|
|
518
|
-
case "/providers":
|
|
519
|
-
showActiveProviders(ctx.aiConfig);
|
|
520
|
-
break;
|
|
521
|
-
|
|
522
|
-
case "/update":
|
|
523
|
-
console.log("\n" + label.system + " " + colors.muted("Checking registry for updates..."));
|
|
524
|
-
await checkForUpdates(true);
|
|
525
|
-
console.log("");
|
|
526
|
-
break;
|
|
527
|
-
|
|
528
|
-
case "/review":
|
|
529
|
-
await handleReviewCommand(ctx);
|
|
530
|
-
break;
|
|
531
|
-
|
|
532
|
-
case "/diagnose":
|
|
533
|
-
await handleDiagnoseCommand(args, ctx);
|
|
534
|
-
break;
|
|
535
|
-
|
|
536
|
-
case "/explain":
|
|
537
|
-
case "/refactor":
|
|
538
|
-
case "/bug":
|
|
539
|
-
case "/doc":
|
|
540
|
-
case "/translate":
|
|
541
|
-
await handleFileAICommand(cmd, args, ctx);
|
|
542
|
-
break;
|
|
543
|
-
|
|
544
|
-
case "/search":
|
|
545
|
-
await handleSearchCommand(args, ctx);
|
|
546
|
-
break;
|
|
547
|
-
|
|
548
|
-
case "/theme":
|
|
549
|
-
await handleThemeSwitch(args);
|
|
550
|
-
break;
|
|
551
|
-
|
|
552
|
-
case "/themes":
|
|
553
|
-
showThemesList();
|
|
554
|
-
break;
|
|
555
|
-
|
|
556
|
-
case "/history-clear":
|
|
557
|
-
await handleHistoryClear(ctx.history, ctx.rl);
|
|
558
|
-
break;
|
|
559
|
-
|
|
560
|
-
case "/game":
|
|
561
|
-
handleGameStart(ctx.game);
|
|
562
|
-
break;
|
|
563
|
-
|
|
564
|
-
case "/abort":
|
|
565
|
-
handleGameAbort(ctx.game);
|
|
566
|
-
break;
|
|
567
|
-
|
|
568
|
-
case "/guess":
|
|
569
|
-
if (ctx.game.active) {
|
|
570
|
-
handleGuess(args[0] || "", ctx.game);
|
|
571
|
-
} else {
|
|
572
|
-
console.log("\n" + label.system + " " + colors.warning("Game is not active. Type /game to start.\n"));
|
|
573
|
-
}
|
|
574
|
-
break;
|
|
575
|
-
|
|
576
|
-
case "/copy":
|
|
577
|
-
await handleCopy(ctx.history);
|
|
578
|
-
break;
|
|
579
|
-
|
|
580
|
-
case "/cmd":
|
|
581
|
-
await handleCustomCommands(args, ctx);
|
|
582
|
-
break;
|
|
583
|
-
|
|
584
|
-
case "/write":
|
|
585
|
-
await handleWriteFile(args, ctx);
|
|
586
|
-
break;
|
|
587
|
-
|
|
588
|
-
case "/commit":
|
|
589
|
-
await handleCommitInsideChat(ctx);
|
|
590
|
-
break;
|
|
591
|
-
|
|
592
|
-
case "/run":
|
|
593
|
-
await handleRunCommand(args, ctx);
|
|
594
|
-
break;
|
|
595
|
-
|
|
596
|
-
case "/history":
|
|
597
|
-
await handleHistorySwitch(ctx);
|
|
598
|
-
break;
|
|
599
|
-
|
|
600
|
-
case "/autopilot":
|
|
601
|
-
await handleAutopilotSwitch(args, ctx);
|
|
602
|
-
break;
|
|
603
|
-
|
|
604
|
-
case "/
|
|
605
|
-
await
|
|
606
|
-
break;
|
|
607
|
-
|
|
608
|
-
case "/
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
console.log(
|
|
630
|
-
console.log(
|
|
631
|
-
console.log(
|
|
632
|
-
console.log(
|
|
633
|
-
console.log(keyValue("/
|
|
634
|
-
console.log(keyValue("/
|
|
635
|
-
console.log(keyValue("/
|
|
636
|
-
console.log(keyValue("/
|
|
637
|
-
console.log(keyValue("/
|
|
638
|
-
console.log(keyValue("/
|
|
639
|
-
console.log(keyValue("/
|
|
640
|
-
console.log(keyValue("/
|
|
641
|
-
console.log(keyValue("/
|
|
642
|
-
console.log(keyValue("/
|
|
643
|
-
console.log(keyValue("/
|
|
644
|
-
console.log(keyValue("/
|
|
645
|
-
console.log(keyValue("/
|
|
646
|
-
console.log(keyValue("/
|
|
647
|
-
console.log(keyValue("/
|
|
648
|
-
console.log(keyValue("/
|
|
649
|
-
console.log(keyValue("/
|
|
650
|
-
console.log(keyValue("/
|
|
651
|
-
console.log(keyValue("/
|
|
652
|
-
console.log(keyValue("/
|
|
653
|
-
console.log(keyValue("/
|
|
654
|
-
console.log(keyValue("/
|
|
655
|
-
console.log(keyValue("/
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
const
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
console.log(
|
|
701
|
-
console.log("");
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
const
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
const
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
)
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
console.log("\n" + label.
|
|
765
|
-
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
console.log("");
|
|
829
|
-
console.log(
|
|
830
|
-
console.log(
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
console.log(
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
console.log("
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
console.log("\n" + label.system + " " + colors.
|
|
859
|
-
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
console.log(
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
console.log("
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
const
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
if (
|
|
1064
|
-
console.log(
|
|
1065
|
-
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
"
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
const
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
const
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
*
|
|
1248
|
-
*/
|
|
1249
|
-
|
|
1250
|
-
const
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
const
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
const
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
const
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
console.log(
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
console.log(
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
const
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
);
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
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
|
+
StreamFilter,
|
|
24
|
+
stripCodeFences,
|
|
25
|
+
getActiveTheme,
|
|
26
|
+
setTheme,
|
|
27
|
+
getThemesList,
|
|
28
|
+
interactiveMenu
|
|
29
|
+
} from "./ui/theme.js";
|
|
30
|
+
import { createSpinner } from "./ui/spinner.js";
|
|
31
|
+
import { showBanner } from "./ui/banner.js";
|
|
32
|
+
import { routePrompt } from "./ai/router.js";
|
|
33
|
+
import { getActiveProviders } from "./ai/providers.js";
|
|
34
|
+
import {
|
|
35
|
+
getAIConfig,
|
|
36
|
+
loadHistory,
|
|
37
|
+
saveHistory,
|
|
38
|
+
clearHistory,
|
|
39
|
+
setConfigValue,
|
|
40
|
+
listSessions,
|
|
41
|
+
switchSession,
|
|
42
|
+
startNewSession
|
|
43
|
+
} from "./config.js";
|
|
44
|
+
import { MODES, DEFAULT_MODE, getModeByName } from "./modes.js";
|
|
45
|
+
import { parseFile, formatContext } from "./file-parser.js";
|
|
46
|
+
import { runMainframeHack } from "./ai/fallback.js";
|
|
47
|
+
import { AGENT_INSTRUCTIONS } from "./agent.js";
|
|
48
|
+
import { checkForUpdates } from "./updater.js";
|
|
49
|
+
import { getSessionTokenStats, getBreakdownByModel, resetSessionTokenStats } from "./ai/tokens.js";
|
|
50
|
+
import { getGitDiff } from "./git.js";
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
// Configure marked dynamically for terminal output
|
|
55
|
+
const getMarked = () => new Marked(markedTerminal({
|
|
56
|
+
reflowText: true,
|
|
57
|
+
width: process.stdout.columns ? Math.max(20, process.stdout.columns - 4) : 80,
|
|
58
|
+
showSectionPrefix: false,
|
|
59
|
+
code: (c) => colors.orange(c),
|
|
60
|
+
codespan: (c) => colors.accent3(c),
|
|
61
|
+
heading: (h) => colors.accent.bold(h),
|
|
62
|
+
strong: (s) => colors.magenta.bold(s),
|
|
63
|
+
em: chalk.italic,
|
|
64
|
+
hr: (h) => colors.dim(h),
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Starts the interactive Aether chat session.
|
|
69
|
+
* @param {{ mode?: string, preferredProvider?: string }} [options={}]
|
|
70
|
+
*/
|
|
71
|
+
export async function startChat(options = {}) {
|
|
72
|
+
// Load AI config
|
|
73
|
+
const aiConfig = await getAIConfig();
|
|
74
|
+
|
|
75
|
+
// Run update check
|
|
76
|
+
await checkForUpdates();
|
|
77
|
+
|
|
78
|
+
// Reset token stats for the new session
|
|
79
|
+
resetSessionTokenStats();
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
// Set theme from configuration
|
|
83
|
+
const theme = aiConfig.THEME || "cyberpunk";
|
|
84
|
+
setTheme(theme);
|
|
85
|
+
|
|
86
|
+
let currentMode = getModeByName(options.mode) || getModeByName(aiConfig.DEFAULT_MODE) || MODES[DEFAULT_MODE];
|
|
87
|
+
let attachedFiles = [];
|
|
88
|
+
|
|
89
|
+
// Persistent history loader
|
|
90
|
+
const history = await loadHistory();
|
|
91
|
+
|
|
92
|
+
// Mini-game state
|
|
93
|
+
const game = {
|
|
94
|
+
active: false,
|
|
95
|
+
code: "",
|
|
96
|
+
attempts: 0,
|
|
97
|
+
maxAttempts: 6,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Show banner
|
|
101
|
+
showBanner(currentMode.name);
|
|
102
|
+
|
|
103
|
+
// Active providers diagnostic check
|
|
104
|
+
const active = getActiveProviders(aiConfig);
|
|
105
|
+
if (active.length === 0) {
|
|
106
|
+
console.log(
|
|
107
|
+
"\n" + label.system + " " +
|
|
108
|
+
colors.warning("No API keys configured. Using local fallback solvers.") + "\n" +
|
|
109
|
+
" " + colors.muted("Run ") + colors.accent("aether setup") +
|
|
110
|
+
colors.muted(" to configure providers (free options available!).\n")
|
|
111
|
+
);
|
|
112
|
+
} else {
|
|
113
|
+
const providerNames = active.map((a) => a.provider.name);
|
|
114
|
+
const unique = [...new Set(providerNames)];
|
|
115
|
+
console.log(
|
|
116
|
+
label.mesh + " " +
|
|
117
|
+
colors.accent("Failover mesh online: ") +
|
|
118
|
+
colors.text(unique.join(" → ")) +
|
|
119
|
+
colors.muted(" → Krylo fallback")
|
|
120
|
+
);
|
|
121
|
+
console.log(
|
|
122
|
+
" " + colors.dim(`${active.length} node(s) active across ${unique.length} provider(s)`) + "\n"
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Display loaded history message if any
|
|
127
|
+
if (history.length > 0) {
|
|
128
|
+
console.log(
|
|
129
|
+
" " + label.info + " " +
|
|
130
|
+
colors.muted(`Restored ${Math.floor(history.length / 2)} message exchanges from persistent logs.`) + "\n"
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Completer: handles commands & dynamic local file path autocomplete
|
|
135
|
+
const completer = (line) => {
|
|
136
|
+
const builtIn = [
|
|
137
|
+
"/help", "/mode", "/modes", "/attach", "/files", "/clear",
|
|
138
|
+
"/providers", "/export", "/status", "/copy", "/exit", "/quit",
|
|
139
|
+
"/theme", "/themes", "/history-clear", "/game", "/abort", "/cmd", "/write",
|
|
140
|
+
"/commit", "/run", "/history", "/autopilot", "/tokens", "/update",
|
|
141
|
+
"/review", "/diagnose", "/explain", "/refactor", "/bug", "/doc", "/translate",
|
|
142
|
+
"/search", "/git", "/dashboard"
|
|
143
|
+
];
|
|
144
|
+
const customCmds = aiConfig.CUSTOM_COMMANDS || {};
|
|
145
|
+
const commands = [...builtIn, ...Object.keys(customCmds)];
|
|
146
|
+
|
|
147
|
+
// File path autocompletion on /attach
|
|
148
|
+
if (line.startsWith("/attach ")) {
|
|
149
|
+
const query = line.slice(8);
|
|
150
|
+
const lastSlash = Math.max(query.lastIndexOf("/"), query.lastIndexOf("\\"));
|
|
151
|
+
let searchDir = ".";
|
|
152
|
+
let searchPrefix = query;
|
|
153
|
+
|
|
154
|
+
if (lastSlash !== -1) {
|
|
155
|
+
searchDir = query.slice(0, lastSlash);
|
|
156
|
+
if (searchDir === "") {
|
|
157
|
+
searchDir = sep;
|
|
158
|
+
}
|
|
159
|
+
searchPrefix = query.slice(lastSlash + 1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const resolved = resolve(searchDir);
|
|
164
|
+
if (existsSync(resolved) && statSync(resolved).isDirectory()) {
|
|
165
|
+
const files = readdirSync(resolved);
|
|
166
|
+
const hits = files
|
|
167
|
+
.filter((f) => f.toLowerCase().startsWith(searchPrefix.toLowerCase()) && !f.startsWith("."))
|
|
168
|
+
.map((f) => {
|
|
169
|
+
const fullPath = searchDir === "." || searchDir === sep ? f : join(searchDir, f);
|
|
170
|
+
const fullResolved = resolve(fullPath);
|
|
171
|
+
const isDir = statSync(fullResolved).isDirectory();
|
|
172
|
+
return `/attach ${fullPath}${isDir ? "/" : ""}`;
|
|
173
|
+
});
|
|
174
|
+
return [hits.length ? hits : [], line];
|
|
175
|
+
}
|
|
176
|
+
} catch (e) {
|
|
177
|
+
// Fallback silently on fs errors
|
|
178
|
+
}
|
|
179
|
+
return [[], line];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Sub-arguments autocomplete on /mode
|
|
183
|
+
if (line.startsWith("/mode ")) {
|
|
184
|
+
const query = line.slice(6).toLowerCase();
|
|
185
|
+
const modesList = Object.keys(MODES);
|
|
186
|
+
const hits = modesList
|
|
187
|
+
.filter((m) => m.startsWith(query))
|
|
188
|
+
.map((m) => `/mode ${m}`);
|
|
189
|
+
return [hits.length ? hits : [], line];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Sub-arguments autocomplete on /theme
|
|
193
|
+
if (line.startsWith("/theme ")) {
|
|
194
|
+
const query = line.slice(7).toLowerCase();
|
|
195
|
+
const themesList = getThemesList();
|
|
196
|
+
const hits = themesList
|
|
197
|
+
.filter((t) => t.startsWith(query))
|
|
198
|
+
.map((t) => `/theme ${t}`);
|
|
199
|
+
return [hits.length ? hits : [], line];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Sub-arguments autocomplete on /cmd
|
|
203
|
+
if (line.startsWith("/cmd ")) {
|
|
204
|
+
const query = line.slice(5).toLowerCase();
|
|
205
|
+
const subcmds = ["list", "add", "remove"];
|
|
206
|
+
const hits = subcmds
|
|
207
|
+
.filter((s) => s.startsWith(query))
|
|
208
|
+
.map((s) => `/cmd ${s}`);
|
|
209
|
+
return [hits.length ? hits : [], line];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const hits = commands.filter((c) => c.startsWith(line));
|
|
213
|
+
return [hits.length ? hits : [], line];
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// Create readline interface
|
|
217
|
+
const rl = createInterface({
|
|
218
|
+
input: process.stdin,
|
|
219
|
+
output: process.stdout,
|
|
220
|
+
prompt: colors.accent(" ❯ "),
|
|
221
|
+
terminal: true,
|
|
222
|
+
completer
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Load persistent history entries directly into the shell up/down array
|
|
226
|
+
if (history.length > 0) {
|
|
227
|
+
const userQueries = history
|
|
228
|
+
.filter((h) => h.role === "user")
|
|
229
|
+
.map((h) => h.content);
|
|
230
|
+
// Readline history is structured newest first (index 0)
|
|
231
|
+
rl.history = [...new Set(userQueries)].reverse();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── AI Execution Helper ──────────────────────────────────
|
|
235
|
+
async function executeAIQuery(promptText, originalInput = promptText) {
|
|
236
|
+
// ── Build Prompt with Context ─────────────────────────
|
|
237
|
+
let fullPrompt = promptText;
|
|
238
|
+
if (attachedFiles.length > 0) {
|
|
239
|
+
const contexts = attachedFiles.map((f) => formatContext(f)).join("\n\n");
|
|
240
|
+
fullPrompt = `${contexts}\n\n${promptText}`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Append AGENT_INSTRUCTIONS to mode systemPrompt
|
|
244
|
+
const systemPrompt = currentMode.systemPrompt + "\n" + AGENT_INSTRUCTIONS;
|
|
245
|
+
|
|
246
|
+
let loopCount = 0;
|
|
247
|
+
const MAX_LOOPS = 5;
|
|
248
|
+
let currentQueryPrompt = fullPrompt;
|
|
249
|
+
let aiResponseText = "";
|
|
250
|
+
let lastResult = null;
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
while (loopCount < MAX_LOOPS) {
|
|
254
|
+
const queryStartTime = Date.now();
|
|
255
|
+
let firstTokenTime = 0;
|
|
256
|
+
|
|
257
|
+
if (loopCount > 0) {
|
|
258
|
+
console.log(colors.accent(`\n🤖 [Aether Autopilot Mode - Iteration ${loopCount + 1}/${MAX_LOOPS}]`));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const spinner = createSpinner(
|
|
262
|
+
colors.muted(loopCount === 0 ? `Routing through mesh ${currentMode.label}...` : `Agent executing tasks...`)
|
|
263
|
+
);
|
|
264
|
+
spinner.start();
|
|
265
|
+
|
|
266
|
+
let hasStartedStreaming = false;
|
|
267
|
+
let streamedText = "";
|
|
268
|
+
const filter = new StreamFilter(process.stdout.write.bind(process.stdout));
|
|
269
|
+
const onToken = (token) => {
|
|
270
|
+
if (!hasStartedStreaming) {
|
|
271
|
+
hasStartedStreaming = true;
|
|
272
|
+
firstTokenTime = Date.now();
|
|
273
|
+
spinner.stop();
|
|
274
|
+
}
|
|
275
|
+
filter.write(token);
|
|
276
|
+
streamedText += token;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const result = await routePrompt(currentQueryPrompt, systemPrompt, aiConfig, onToken, history);
|
|
280
|
+
spinner.stop();
|
|
281
|
+
filter.flush();
|
|
282
|
+
|
|
283
|
+
aiResponseText = result.text;
|
|
284
|
+
lastResult = result;
|
|
285
|
+
|
|
286
|
+
if (hasStartedStreaming) {
|
|
287
|
+
clearStreamedText(filter.filteredText);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Display response
|
|
291
|
+
console.log("");
|
|
292
|
+
console.log(label.aether + " " + providerBadge(result));
|
|
293
|
+
console.log(separator("─"));
|
|
294
|
+
console.log("");
|
|
295
|
+
|
|
296
|
+
if (result.provider === "local" || result.provider === "krylo-fallback") {
|
|
297
|
+
console.log(colors.text(" " + result.text.split("\n").join("\n ")));
|
|
298
|
+
} else {
|
|
299
|
+
let displayText = result.text;
|
|
300
|
+
const rendered = getMarked().parse(displayText);
|
|
301
|
+
console.log(rendered);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const elapsedSec = ((Date.now() - queryStartTime) / 1000).toFixed(1);
|
|
305
|
+
let speedText = "";
|
|
306
|
+
if (firstTokenTime > 0) {
|
|
307
|
+
const streamElapsed = (Date.now() - firstTokenTime) / 1000;
|
|
308
|
+
if (streamElapsed > 0.05) {
|
|
309
|
+
const estimatedTokens = Math.max(1, Math.round(streamedText.length / 4));
|
|
310
|
+
const tps = (estimatedTokens / streamElapsed).toFixed(1);
|
|
311
|
+
speedText = ` • ${tps} tok/s`;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const showTokens = aiConfig.SHOW_TOKENS !== "false";
|
|
316
|
+
let tokensText = "";
|
|
317
|
+
if (showTokens && result.usage) {
|
|
318
|
+
const { promptTokens, completionTokens } = result.usage;
|
|
319
|
+
tokensText = ` • ${promptTokens.toLocaleString()} in / ${completionTokens.toLocaleString()} out tokens`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
console.log(separator("─"));
|
|
323
|
+
console.log(
|
|
324
|
+
" " + colors.dim(`Node ${result.node} • ${result.provider}`) +
|
|
325
|
+
(result.model ? colors.dim(` • ${result.model}`) : "") +
|
|
326
|
+
colors.dim(` • ${elapsedSec}s${speedText}`) +
|
|
327
|
+
colors.dim(tokensText) +
|
|
328
|
+
colors.dim(` • ${Math.floor(history.length / 2)} exchanges`)
|
|
329
|
+
);
|
|
330
|
+
console.log("");
|
|
331
|
+
|
|
332
|
+
// Process any agent tools output by the AI
|
|
333
|
+
const { processAgentBlocks } = await import("./agent.js");
|
|
334
|
+
const toolResults = await processAgentBlocks(aiResponseText, aiConfig, rl);
|
|
335
|
+
|
|
336
|
+
if (toolResults.length === 0) {
|
|
337
|
+
// No tools executed, end loop
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Store this turn in history so AI knows what happened
|
|
342
|
+
history.push({ role: "user", content: currentQueryPrompt, timestamp: new Date() });
|
|
343
|
+
history.push({
|
|
344
|
+
role: "assistant",
|
|
345
|
+
content: aiResponseText,
|
|
346
|
+
provider: result.provider,
|
|
347
|
+
model: result.model,
|
|
348
|
+
node: result.node,
|
|
349
|
+
timestamp: new Date(),
|
|
350
|
+
});
|
|
351
|
+
await saveHistory(history, currentMode.name);
|
|
352
|
+
|
|
353
|
+
// Format tool outputs as next prompt
|
|
354
|
+
let formattedResults = "### Agent Tool Outputs:\n";
|
|
355
|
+
for (const tr of toolResults) {
|
|
356
|
+
if (tr.success) {
|
|
357
|
+
if (tr.tool === "RUN_COMMAND") {
|
|
358
|
+
formattedResults += `\n- RUN_COMMAND "${tr.arg}" succeeded. Output:\n\`\`\`\n${tr.stdout || ""}${tr.stderr || ""}\n\`\`\`;`;
|
|
359
|
+
} else if (tr.tool === "READ_FILE") {
|
|
360
|
+
formattedResults += `\n- READ_FILE "${tr.arg}" succeeded. File Content:\n\`\`\`\n${tr.content}\n\`\`\`;`;
|
|
361
|
+
} else if (tr.tool === "WRITE_FILE") {
|
|
362
|
+
formattedResults += `\n- WRITE_FILE "${tr.arg}" succeeded.`;
|
|
363
|
+
} else if (tr.tool === "SEARCH_WEB") {
|
|
364
|
+
const resultsList = tr.results.map((r, i) => `${i+1}. [${r.title}](${r.url})\n ${r.snippet}`).join("\n");
|
|
365
|
+
formattedResults += `\n- SEARCH_WEB "${tr.arg}" succeeded. Results:\n${resultsList}`;
|
|
366
|
+
}
|
|
367
|
+
} else {
|
|
368
|
+
formattedResults += `\n- ${tr.tool} "${tr.arg}" failed: ${tr.error}`;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
formattedResults += "\n\nPlease continue and finalize your task or perform next steps.";
|
|
372
|
+
|
|
373
|
+
currentQueryPrompt = formattedResults;
|
|
374
|
+
loopCount++;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Store final state in history
|
|
378
|
+
if (loopCount > 0) {
|
|
379
|
+
// Just save to disk to persist
|
|
380
|
+
await saveHistory(history, currentMode.name);
|
|
381
|
+
} else {
|
|
382
|
+
// Standard single-turn save
|
|
383
|
+
history.push({ role: "user", content: originalInput, timestamp: new Date() });
|
|
384
|
+
history.push({
|
|
385
|
+
role: "assistant",
|
|
386
|
+
content: aiResponseText,
|
|
387
|
+
provider: lastResult.provider,
|
|
388
|
+
model: lastResult.model,
|
|
389
|
+
node: lastResult.node,
|
|
390
|
+
timestamp: new Date(),
|
|
391
|
+
});
|
|
392
|
+
await saveHistory(history, currentMode.name);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
} catch (err) {
|
|
396
|
+
console.log("\n" + label.error + " " + colors.danger(err.message) + "\n");
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Sync shell's recall history list
|
|
400
|
+
const userQueries = history
|
|
401
|
+
.filter((h) => h.role === "user")
|
|
402
|
+
.map((h) => h.content);
|
|
403
|
+
rl.history = [...new Set(userQueries)].reverse();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
rl.prompt();
|
|
407
|
+
|
|
408
|
+
rl.on("line", async (line) => {
|
|
409
|
+
const input = line.trim();
|
|
410
|
+
if (!input) {
|
|
411
|
+
rl.prompt();
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ── Handle Game Input ──────────────────────────────────
|
|
416
|
+
if (game.active && !input.startsWith("/")) {
|
|
417
|
+
handleGuess(input, game);
|
|
418
|
+
rl.prompt();
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ── Handle Slash Commands ──────────────────────────────
|
|
423
|
+
if (input.startsWith("/")) {
|
|
424
|
+
const [cmd, ...args] = input.split(/\s+/);
|
|
425
|
+
const builtInList = [
|
|
426
|
+
"/", "/help", "/mode", "/modes", "/attach", "/files", "/clear",
|
|
427
|
+
"/providers", "/export", "/status", "/copy", "/exit", "/quit",
|
|
428
|
+
"/theme", "/themes", "/history-clear", "/game", "/abort", "/cmd",
|
|
429
|
+
"/guess", "/write", "/commit", "/run", "/history", "/autopilot", "/tokens",
|
|
430
|
+
"/update", "/review", "/diagnose", "/explain", "/refactor", "/bug", "/doc",
|
|
431
|
+
"/translate", "/search", "/git", "/dashboard"
|
|
432
|
+
];
|
|
433
|
+
|
|
434
|
+
const customCmds = aiConfig.CUSTOM_COMMANDS || {};
|
|
435
|
+
|
|
436
|
+
if (!builtInList.includes(cmd.toLowerCase()) && customCmds[cmd]) {
|
|
437
|
+
const template = customCmds[cmd];
|
|
438
|
+
const userArg = args.join(" ");
|
|
439
|
+
const rewrittenPrompt = template + (userArg ? " " + userArg : "");
|
|
440
|
+
|
|
441
|
+
console.log("\n" + label.system + " " + colors.accent(`Executing custom command: `) + colors.text(cmd));
|
|
442
|
+
console.log(" " + colors.muted("Prompt: ") + colors.text(rewrittenPrompt) + "\n");
|
|
443
|
+
|
|
444
|
+
await executeAIQuery(rewrittenPrompt, input);
|
|
445
|
+
rl.prompt();
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const handled = await handleCommand(input, {
|
|
450
|
+
currentMode,
|
|
451
|
+
attachedFiles,
|
|
452
|
+
history,
|
|
453
|
+
aiConfig,
|
|
454
|
+
game,
|
|
455
|
+
setMode: (mode) => { currentMode = mode; },
|
|
456
|
+
addFile: (file) => { attachedFiles.push(file); },
|
|
457
|
+
clearFiles: () => { attachedFiles = []; },
|
|
458
|
+
rl,
|
|
459
|
+
});
|
|
460
|
+
if (handled !== "exit") {
|
|
461
|
+
rl.prompt();
|
|
462
|
+
}
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
await executeAIQuery(input);
|
|
467
|
+
rl.prompt();
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
rl.on("close", () => {
|
|
471
|
+
console.log("\n" + label.system + " " + colors.muted("Session terminated. Stay cyberpunk. ⚡\n"));
|
|
472
|
+
process.exit(0);
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Handles slash commands in the chat.
|
|
478
|
+
*/
|
|
479
|
+
async function handleCommand(input, ctx) {
|
|
480
|
+
const [cmd, ...args] = input.split(/\s+/);
|
|
481
|
+
|
|
482
|
+
switch (cmd.toLowerCase()) {
|
|
483
|
+
case "/":
|
|
484
|
+
case "/help":
|
|
485
|
+
showHelp(ctx.aiConfig);
|
|
486
|
+
break;
|
|
487
|
+
|
|
488
|
+
case "/mode":
|
|
489
|
+
handleModeSwitch(args, ctx);
|
|
490
|
+
break;
|
|
491
|
+
|
|
492
|
+
case "/modes":
|
|
493
|
+
showModes();
|
|
494
|
+
break;
|
|
495
|
+
|
|
496
|
+
case "/attach":
|
|
497
|
+
await handleAttach(args, ctx);
|
|
498
|
+
break;
|
|
499
|
+
|
|
500
|
+
case "/files":
|
|
501
|
+
showAttachedFiles(ctx.attachedFiles);
|
|
502
|
+
break;
|
|
503
|
+
|
|
504
|
+
case "/clear":
|
|
505
|
+
// Actual screen clear & scrollback reset
|
|
506
|
+
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
507
|
+
showBanner(ctx.currentMode.name);
|
|
508
|
+
break;
|
|
509
|
+
|
|
510
|
+
case "/export":
|
|
511
|
+
await handleExport(ctx.history);
|
|
512
|
+
break;
|
|
513
|
+
|
|
514
|
+
case "/status":
|
|
515
|
+
showStatus(ctx);
|
|
516
|
+
break;
|
|
517
|
+
|
|
518
|
+
case "/providers":
|
|
519
|
+
showActiveProviders(ctx.aiConfig);
|
|
520
|
+
break;
|
|
521
|
+
|
|
522
|
+
case "/update":
|
|
523
|
+
console.log("\n" + label.system + " " + colors.muted("Checking registry for updates..."));
|
|
524
|
+
await checkForUpdates(true);
|
|
525
|
+
console.log("");
|
|
526
|
+
break;
|
|
527
|
+
|
|
528
|
+
case "/review":
|
|
529
|
+
await handleReviewCommand(ctx);
|
|
530
|
+
break;
|
|
531
|
+
|
|
532
|
+
case "/diagnose":
|
|
533
|
+
await handleDiagnoseCommand(args, ctx);
|
|
534
|
+
break;
|
|
535
|
+
|
|
536
|
+
case "/explain":
|
|
537
|
+
case "/refactor":
|
|
538
|
+
case "/bug":
|
|
539
|
+
case "/doc":
|
|
540
|
+
case "/translate":
|
|
541
|
+
await handleFileAICommand(cmd, args, ctx);
|
|
542
|
+
break;
|
|
543
|
+
|
|
544
|
+
case "/search":
|
|
545
|
+
await handleSearchCommand(args, ctx);
|
|
546
|
+
break;
|
|
547
|
+
|
|
548
|
+
case "/theme":
|
|
549
|
+
await handleThemeSwitch(args);
|
|
550
|
+
break;
|
|
551
|
+
|
|
552
|
+
case "/themes":
|
|
553
|
+
showThemesList();
|
|
554
|
+
break;
|
|
555
|
+
|
|
556
|
+
case "/history-clear":
|
|
557
|
+
await handleHistoryClear(ctx.history, ctx.rl);
|
|
558
|
+
break;
|
|
559
|
+
|
|
560
|
+
case "/game":
|
|
561
|
+
handleGameStart(ctx.game);
|
|
562
|
+
break;
|
|
563
|
+
|
|
564
|
+
case "/abort":
|
|
565
|
+
handleGameAbort(ctx.game);
|
|
566
|
+
break;
|
|
567
|
+
|
|
568
|
+
case "/guess":
|
|
569
|
+
if (ctx.game.active) {
|
|
570
|
+
handleGuess(args[0] || "", ctx.game);
|
|
571
|
+
} else {
|
|
572
|
+
console.log("\n" + label.system + " " + colors.warning("Game is not active. Type /game to start.\n"));
|
|
573
|
+
}
|
|
574
|
+
break;
|
|
575
|
+
|
|
576
|
+
case "/copy":
|
|
577
|
+
await handleCopy(ctx.history);
|
|
578
|
+
break;
|
|
579
|
+
|
|
580
|
+
case "/cmd":
|
|
581
|
+
await handleCustomCommands(args, ctx);
|
|
582
|
+
break;
|
|
583
|
+
|
|
584
|
+
case "/write":
|
|
585
|
+
await handleWriteFile(args, ctx);
|
|
586
|
+
break;
|
|
587
|
+
|
|
588
|
+
case "/commit":
|
|
589
|
+
await handleCommitInsideChat(ctx);
|
|
590
|
+
break;
|
|
591
|
+
|
|
592
|
+
case "/run":
|
|
593
|
+
await handleRunCommand(args, ctx);
|
|
594
|
+
break;
|
|
595
|
+
|
|
596
|
+
case "/history":
|
|
597
|
+
await handleHistorySwitch(ctx);
|
|
598
|
+
break;
|
|
599
|
+
|
|
600
|
+
case "/autopilot":
|
|
601
|
+
await handleAutopilotSwitch(args, ctx);
|
|
602
|
+
break;
|
|
603
|
+
|
|
604
|
+
case "/git":
|
|
605
|
+
await handleGitTUI(ctx);
|
|
606
|
+
break;
|
|
607
|
+
|
|
608
|
+
case "/dashboard":
|
|
609
|
+
await handleDashboardCommand(ctx);
|
|
610
|
+
break;
|
|
611
|
+
|
|
612
|
+
case "/tokens":
|
|
613
|
+
await handleTokensDisplay(ctx);
|
|
614
|
+
break;
|
|
615
|
+
|
|
616
|
+
case "/exit":
|
|
617
|
+
case "/quit":
|
|
618
|
+
ctx.rl.close();
|
|
619
|
+
return "exit";
|
|
620
|
+
|
|
621
|
+
default:
|
|
622
|
+
console.log("\n" + label.system + " " + colors.warning(`Unknown command: ${cmd}. Type /help for available commands.\n`));
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// ── Command Handlers ────────────────────────────────────────
|
|
627
|
+
|
|
628
|
+
function showHelp(aiConfig) {
|
|
629
|
+
console.log("");
|
|
630
|
+
console.log(colors.brand(" ⚡ AETHER CLI COMMANDS"));
|
|
631
|
+
console.log(separator("─"));
|
|
632
|
+
console.log("");
|
|
633
|
+
console.log(keyValue("/", "Show this help menu"));
|
|
634
|
+
console.log(keyValue("/help", "Show this help menu"));
|
|
635
|
+
console.log(keyValue("/mode <name>", "Switch mode (" + Object.keys(MODES).join(", ") + ")"));
|
|
636
|
+
console.log(keyValue("/modes", "List all modes with signal metrics"));
|
|
637
|
+
console.log(keyValue("/theme <name>", "Switch visual theme (cyberpunk, matrix, synthwave, crimson)"));
|
|
638
|
+
console.log(keyValue("/themes", "List available visual themes"));
|
|
639
|
+
console.log(keyValue("/attach <path>", "Attach a file for context (supports Tab path autocomplete!)"));
|
|
640
|
+
console.log(keyValue("/files", "List attached files"));
|
|
641
|
+
console.log(keyValue("/clear", "Clear terminal screen and reprint banner"));
|
|
642
|
+
console.log(keyValue("/providers", "Show active AI providers"));
|
|
643
|
+
console.log(keyValue("/export", "Export conversation to file"));
|
|
644
|
+
console.log(keyValue("/history", "List, switch, and resume past interactive chat sessions"));
|
|
645
|
+
console.log(keyValue("/history-clear", "Clear saved persistent chat history"));
|
|
646
|
+
console.log(keyValue("/autopilot <mode|debug [cmd]>", "View/switch autopilot level (off, safe, workspace, machine) or run autonomous debug loop"));
|
|
647
|
+
console.log(keyValue("/git", "Launch interactive Git branch tree, history, and file staging TUI"));
|
|
648
|
+
console.log(keyValue("/dashboard", "Spawn web-based local cyberpunk telemetry dashboard companion"));
|
|
649
|
+
console.log(keyValue("/tokens", "View detailed session token usage and exchanges telemetry"));
|
|
650
|
+
console.log(keyValue("/update", "Force check for updates and update Aether CLI manually"));
|
|
651
|
+
console.log(keyValue("/game", "Start the local mainframe hacking mini-game"));
|
|
652
|
+
console.log(keyValue("/copy", "Copy the last assistant response to clipboard"));
|
|
653
|
+
console.log(keyValue("/cmd <list|add|remove>", "Manage custom command shortcuts"));
|
|
654
|
+
console.log(keyValue("/write <filename>", "Extract last code block and save to file"));
|
|
655
|
+
console.log(keyValue("/commit", "Generate conventional commit message and commit changes"));
|
|
656
|
+
console.log(keyValue("/run <command>", "Execute a shell command interactively"));
|
|
657
|
+
console.log(keyValue("/review", "Run git diff and stream an AI code review"));
|
|
658
|
+
console.log(keyValue("/diagnose [cmd]", "Run build/tests and AI-debug any errors"));
|
|
659
|
+
console.log(keyValue("/explain <file>", "AI-explain the design and logic of a file"));
|
|
660
|
+
console.log(keyValue("/refactor <file>", "AI-refactor the code of a target file"));
|
|
661
|
+
console.log(keyValue("/bug <file>", "AI-audit a file to find potential logic bugs"));
|
|
662
|
+
console.log(keyValue("/doc <file>", "AI-generate documentation/docstrings for a file"));
|
|
663
|
+
console.log(keyValue("/translate <file> <lang>", "AI-translate code of a file to another language"));
|
|
664
|
+
console.log(keyValue("/search <query>", "Find matches in code files (use --ai for semantic search)"));
|
|
665
|
+
console.log(keyValue("/exit", "End session"));
|
|
666
|
+
|
|
667
|
+
if (aiConfig && aiConfig.CUSTOM_COMMANDS) {
|
|
668
|
+
const custom = aiConfig.CUSTOM_COMMANDS;
|
|
669
|
+
const entries = Object.entries(custom);
|
|
670
|
+
if (entries.length > 0) {
|
|
671
|
+
console.log("");
|
|
672
|
+
console.log(colors.brand(" ⚡ CUSTOM SHORTCUTS"));
|
|
673
|
+
console.log(separator("─"));
|
|
674
|
+
for (const [cmd, template] of entries) {
|
|
675
|
+
console.log(keyValue(cmd, `Shortcut for: "${template}"`));
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
console.log("");
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function handleModeSwitch(args, ctx) {
|
|
683
|
+
const modeName = args[0];
|
|
684
|
+
if (!modeName) {
|
|
685
|
+
console.log("\n" + label.mode + " " + colors.warning("Usage: /mode <" + Object.keys(MODES).join("|") + ">\n"));
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const newMode = getModeByName(modeName);
|
|
690
|
+
if (!newMode) {
|
|
691
|
+
console.log("\n" + label.mode + " " + colors.danger(`Unknown mode: "${modeName}".`) + " " + colors.muted("Available: " + Object.keys(MODES).join(", ") + "\n"));
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
ctx.setMode(newMode);
|
|
696
|
+
console.log("\n" + label.mode + " " + colors.accent("Switched to ") + modeBadge(newMode.name));
|
|
697
|
+
console.log(" " + colors.muted(newMode.description) + "\n");
|
|
698
|
+
|
|
699
|
+
const sig = newMode.signal;
|
|
700
|
+
console.log(" " + signalBar("Reasoning", sig.reasoning));
|
|
701
|
+
console.log(" " + signalBar("Clarity", sig.clarity));
|
|
702
|
+
console.log(" " + signalBar("System IQ", sig.systemIQ));
|
|
703
|
+
console.log(" " + signalBar("Delivery", sig.delivery));
|
|
704
|
+
console.log("");
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function showModes() {
|
|
708
|
+
console.log("");
|
|
709
|
+
console.log(colors.brand(" ◈ AVAILABLE REASONING MODES"));
|
|
710
|
+
console.log(separator("─"));
|
|
711
|
+
console.log("");
|
|
712
|
+
|
|
713
|
+
for (const mode of Object.values(MODES)) {
|
|
714
|
+
console.log(" " + modeBadge(mode.name) + " " + colors.muted(`(${mode.layer})`));
|
|
715
|
+
console.log(" " + colors.text(mode.description));
|
|
716
|
+
const sig = mode.signal;
|
|
717
|
+
console.log(" " + signalBar("RSN", sig.reasoning) + " " + signalBar("CLR", sig.clarity) + " " + signalBar("SIQ", sig.systemIQ) + " " + signalBar("DLV", sig.delivery));
|
|
718
|
+
console.log("");
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
async function handleAttach(args, ctx) {
|
|
723
|
+
const filePath = args.join(" ").trim();
|
|
724
|
+
if (!filePath) {
|
|
725
|
+
const { scanWorkspaceFiles } = await import("./file-parser.js");
|
|
726
|
+
const { interactiveCheckbox } = await import("./ui/theme.js");
|
|
727
|
+
|
|
728
|
+
const workspaceFiles = scanWorkspaceFiles(process.cwd());
|
|
729
|
+
if (workspaceFiles.length === 0) {
|
|
730
|
+
console.log("\n" + label.file + " " + colors.muted("No supported files found in this workspace.\n"));
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
ctx.rl.pause();
|
|
735
|
+
const selected = await interactiveCheckbox(
|
|
736
|
+
"Attach files (Arrow Keys to navigate, Space to toggle, Enter to confirm, Esc/q to cancel):\n",
|
|
737
|
+
workspaceFiles,
|
|
738
|
+
ctx.attachedFiles.map(f => f.relativePath || f.name)
|
|
739
|
+
);
|
|
740
|
+
ctx.rl.resume();
|
|
741
|
+
|
|
742
|
+
if (selected === null) {
|
|
743
|
+
console.log("\n" + label.file + " " + colors.muted("Selection canceled.\n"));
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
ctx.clearFiles();
|
|
748
|
+
if (selected.length === 0) {
|
|
749
|
+
console.log("\n" + label.file + " " + colors.success("Cleared all attachments.\n"));
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
let successCount = 0;
|
|
754
|
+
for (const file of selected) {
|
|
755
|
+
try {
|
|
756
|
+
const fileData = await parseFile(file);
|
|
757
|
+
fileData.relativePath = file;
|
|
758
|
+
ctx.addFile(fileData);
|
|
759
|
+
successCount++;
|
|
760
|
+
} catch (err) {
|
|
761
|
+
console.log(label.error + " " + colors.danger(`Failed to attach ${file}: ${err.message}`));
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
console.log("\n" + label.file + " " + colors.success(`Successfully attached ${successCount} file(s).\n`));
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
try {
|
|
769
|
+
const fileData = await parseFile(filePath);
|
|
770
|
+
ctx.addFile(fileData);
|
|
771
|
+
console.log("\n" + label.file + " " + colors.success(`Attached: ${fileData.name}`));
|
|
772
|
+
console.log(" " + colors.muted(`${formatBytes(fileData.size)} • ${fileData.extension} • ${ctx.attachedFiles.length} file(s) loaded\n`));
|
|
773
|
+
} catch (err) {
|
|
774
|
+
console.log("\n" + label.error + " " + colors.danger(err.message) + "\n");
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function showAttachedFiles(files) {
|
|
779
|
+
if (files.length === 0) {
|
|
780
|
+
console.log("\n" + label.file + " " + colors.muted("No files attached. Use /attach <path> to add context.\n"));
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
console.log("");
|
|
785
|
+
console.log(label.file + " " + colors.accent(`${files.length} file(s) attached:`));
|
|
786
|
+
for (const f of files) {
|
|
787
|
+
console.log(bullet(`${f.name} (${formatBytes(f.size)}, ${f.extension})`));
|
|
788
|
+
}
|
|
789
|
+
console.log("");
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
async function handleExport(history) {
|
|
793
|
+
if (history.length === 0) {
|
|
794
|
+
console.log("\n" + label.system + " " + colors.muted("No conversation to export.\n"));
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
799
|
+
const filename = `aether-chat-${timestamp}.md`;
|
|
800
|
+
const filepath = resolve(filename);
|
|
801
|
+
|
|
802
|
+
let content = `# Aether AI Chat Export\n*Exported at ${new Date().toLocaleString()}*\n\n---\n\n`;
|
|
803
|
+
|
|
804
|
+
for (const entry of history) {
|
|
805
|
+
if (entry.role === "user") {
|
|
806
|
+
content += `## 👤 You\n${entry.content}\n\n`;
|
|
807
|
+
} else {
|
|
808
|
+
content += `## 🤖 Aether (${entry.provider || "unknown"})\n${entry.content}\n\n---\n\n`;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
try {
|
|
813
|
+
await writeFile(filepath, content, "utf-8");
|
|
814
|
+
console.log("\n" + label.system + " " + colors.success(`Exported to: ${filepath}\n`));
|
|
815
|
+
} catch (err) {
|
|
816
|
+
console.log("\n" + label.error + " " + colors.danger(`Export failed: ${err.message}\n`));
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function showStatus(ctx) {
|
|
821
|
+
const active = getActiveProviders(ctx.aiConfig);
|
|
822
|
+
|
|
823
|
+
console.log("");
|
|
824
|
+
console.log(colors.brand(" ◈ SESSION STATUS"));
|
|
825
|
+
console.log(separator("─"));
|
|
826
|
+
console.log(keyValue(" Theme", getActiveTheme().toUpperCase()));
|
|
827
|
+
console.log(keyValue(" Mode", ctx.currentMode.label));
|
|
828
|
+
console.log(keyValue(" Layer", ctx.currentMode.layer));
|
|
829
|
+
console.log(keyValue(" Exchanges", String(Math.floor(ctx.history.length / 2))));
|
|
830
|
+
console.log(keyValue(" Files", String(ctx.attachedFiles.length)));
|
|
831
|
+
console.log(keyValue(" Providers", String(active.length)));
|
|
832
|
+
console.log("");
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function showActiveProviders(aiConfig) {
|
|
836
|
+
const active = getActiveProviders(aiConfig);
|
|
837
|
+
|
|
838
|
+
console.log("");
|
|
839
|
+
console.log(colors.brand(" ◈ ACTIVE PROVIDERS"));
|
|
840
|
+
console.log(separator("─"));
|
|
841
|
+
|
|
842
|
+
if (active.length === 0) {
|
|
843
|
+
console.log(" " + colors.warning("No providers. Run `aether setup` to configure.") + "\n");
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
for (const { provider } of active) {
|
|
848
|
+
console.log(" " + colors.success("✓ ") + colors.text(provider.name) + colors.dim(` • ${provider.defaultModel}`));
|
|
849
|
+
}
|
|
850
|
+
console.log(" " + colors.success("✓ ") + colors.text("Krylo Companion") + colors.dim(" • Local fallback"));
|
|
851
|
+
console.log(" " + colors.success("✓ ") + colors.text("Math Solver") + colors.dim(" • Local"));
|
|
852
|
+
console.log("");
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
async function handleThemeSwitch(args) {
|
|
856
|
+
const themeName = args[0];
|
|
857
|
+
if (!themeName) {
|
|
858
|
+
console.log("\n" + label.system + " " + colors.warning("Usage: /theme <theme-name>. Type /themes to list themes.\n"));
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const success = setTheme(themeName);
|
|
863
|
+
if (success) {
|
|
864
|
+
await setConfigValue("THEME", themeName.toLowerCase().trim());
|
|
865
|
+
console.log("\n" + label.system + " " + colors.success(`✓ Theme switched to ${themeName.toUpperCase()}`));
|
|
866
|
+
console.log(" " + colors.muted("Visual grid modulates synchronized.\n"));
|
|
867
|
+
} else {
|
|
868
|
+
console.log("\n" + label.system + " " + colors.danger(`Unknown theme: "${themeName}".`) + " " + colors.muted(`Available: ${getThemesList().join(", ")}\n`));
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function showThemesList() {
|
|
873
|
+
console.log("");
|
|
874
|
+
console.log(colors.brand(" ◈ AVAILABLE COLOR THEMES"));
|
|
875
|
+
console.log(separator("─"));
|
|
876
|
+
const active = getActiveTheme();
|
|
877
|
+
for (const t of getThemesList()) {
|
|
878
|
+
const activeText = t === active ? colors.success("★ ACTIVE") : "";
|
|
879
|
+
console.log(bullet(t.toUpperCase().padEnd(14) + activeText));
|
|
880
|
+
}
|
|
881
|
+
console.log("");
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
async function handleHistoryClear(history, rl) {
|
|
885
|
+
await clearHistory();
|
|
886
|
+
history.length = 0;
|
|
887
|
+
if (rl) rl.history = [];
|
|
888
|
+
console.log("\n" + label.system + " " + colors.success("✓ Persistent chat history and prompt history cleared.\n"));
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
async function handleAutopilotSwitch(args, ctx) {
|
|
892
|
+
const setting = args[0]?.toLowerCase().trim();
|
|
893
|
+
if (setting === "debug") {
|
|
894
|
+
await handleAutopilotDebug(args.slice(1).join(" "), ctx);
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
if (!setting) {
|
|
898
|
+
const current = (ctx.aiConfig.AUTOPILOT || "off").toUpperCase();
|
|
899
|
+
console.log("\n" + label.system + " " + colors.brand("🤖 AUTOPILOT AGENT CONFIGURATION"));
|
|
900
|
+
console.log(separator("─"));
|
|
901
|
+
console.log(keyValue(" Current Setting", current));
|
|
902
|
+
console.log("");
|
|
903
|
+
console.log(" " + colors.muted("Available Modes:"));
|
|
904
|
+
console.log(" • " + colors.accent("off") + colors.text(" - Always ask user for confirmation before executing any actions."));
|
|
905
|
+
console.log(" • " + colors.accent("safe") + colors.text(" - Run read-only/safe terminal commands and searches automatically."));
|
|
906
|
+
console.log(" • " + colors.accent("workspace") + colors.text(" - Run any actions automatically if they stay inside the workspace."));
|
|
907
|
+
console.log(" • " + colors.accent("machine") + colors.text(" - Complete autopilot. Run any action automatically (Full access)."));
|
|
908
|
+
console.log("");
|
|
909
|
+
console.log(" " + colors.muted("To change setting: ") + colors.accent("/autopilot <mode>") + "\n");
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const valid = ["off", "safe", "workspace", "machine"];
|
|
914
|
+
if (!valid.includes(setting)) {
|
|
915
|
+
console.log("\n" + label.system + " " + colors.danger(`ERROR: Unknown autopilot mode "${setting}".`) + " " + colors.muted("Choose from: off, safe, workspace, machine.\n"));
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
await setConfigValue("AUTOPILOT", setting);
|
|
920
|
+
ctx.aiConfig.AUTOPILOT = setting;
|
|
921
|
+
console.log("\n" + label.system + " " + colors.success(`✓ Autopilot setting updated to ${setting.toUpperCase()} successfully.\n`));
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
async function handleHistorySwitch(ctx) {
|
|
925
|
+
const sessions = listSessions();
|
|
926
|
+
if (sessions.length === 0) {
|
|
927
|
+
console.log("\n" + label.system + " " + colors.muted("No past chat sessions found.\n"));
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const items = sessions.map((s) => {
|
|
932
|
+
const dateStr = new Date(s.timestamp).toLocaleString();
|
|
933
|
+
const count = s.messages.length;
|
|
934
|
+
const exchanges = Math.floor(count / 2);
|
|
935
|
+
// Find first user query preview
|
|
936
|
+
const firstQuery = s.messages.find((m) => m.role === "user")?.content || "Empty conversation";
|
|
937
|
+
const preview = firstQuery.length > 50 ? firstQuery.slice(0, 47) + "..." : firstQuery;
|
|
938
|
+
const modeBadgeText = `[${s.mode}]`;
|
|
939
|
+
return `${colors.dim(dateStr)} ${colors.brand(modeBadgeText.padEnd(12))} ${colors.muted(exchanges + " exch")} • ${colors.text(preview)}`;
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
// Add an option to start a new session
|
|
943
|
+
items.push(colors.accent("➕ Start a new chat session"));
|
|
944
|
+
|
|
945
|
+
ctx.rl.pause();
|
|
946
|
+
const selectedIndex = await interactiveMenu(
|
|
947
|
+
"Select a past chat session to resume (Arrow Keys to navigate, Enter to select, Esc/q to cancel):\n",
|
|
948
|
+
items
|
|
949
|
+
);
|
|
950
|
+
ctx.rl.resume();
|
|
951
|
+
|
|
952
|
+
if (selectedIndex === null) {
|
|
953
|
+
console.log("\n" + label.system + " " + colors.muted("Selection canceled.\n"));
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Clear screen and load the selected session
|
|
958
|
+
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
959
|
+
|
|
960
|
+
if (selectedIndex === sessions.length) {
|
|
961
|
+
// Start new session
|
|
962
|
+
const newSessionFile = startNewSession();
|
|
963
|
+
ctx.history.length = 0;
|
|
964
|
+
showBanner(ctx.currentMode.name);
|
|
965
|
+
console.log("\n" + label.system + " " + colors.success("Started a new chat session.\n"));
|
|
966
|
+
} else {
|
|
967
|
+
const selectedSession = sessions[selectedIndex];
|
|
968
|
+
switchSession(selectedSession.file);
|
|
969
|
+
|
|
970
|
+
// Load history
|
|
971
|
+
const loadedHistory = await loadHistory();
|
|
972
|
+
ctx.history.length = 0;
|
|
973
|
+
for (const msg of loadedHistory) {
|
|
974
|
+
ctx.history.push(msg);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
showBanner(ctx.currentMode.name);
|
|
978
|
+
console.log("\n" + label.system + " " + colors.success(`✓ Switched to chat session from ${new Date(selectedSession.timestamp).toLocaleString()}`));
|
|
979
|
+
console.log(" " + colors.muted(`Restored ${Math.floor(ctx.history.length / 2)} message exchanges.\n`));
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Sync shell's recall history list
|
|
983
|
+
const userQueries = ctx.history
|
|
984
|
+
.filter((h) => h.role === "user")
|
|
985
|
+
.map((h) => h.content);
|
|
986
|
+
ctx.rl.history = [...new Set(userQueries)].reverse();
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function handleGameStart(game) {
|
|
990
|
+
if (game.active) {
|
|
991
|
+
console.log("\n" + label.system + " " + colors.warning("Mainframe breach is already in progress. Type /abort to cancel.\n"));
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Set up game
|
|
996
|
+
game.active = true;
|
|
997
|
+
game.attempts = 0;
|
|
998
|
+
|
|
999
|
+
// Generate random 4-digit code
|
|
1000
|
+
const code = Array.from({ length: 4 }, () => Math.floor(Math.random() * 10)).join("");
|
|
1001
|
+
game.code = code;
|
|
1002
|
+
|
|
1003
|
+
const rules = runMainframeHack();
|
|
1004
|
+
console.log("\n" + rules.text + "\n");
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function handleGameAbort(game) {
|
|
1008
|
+
if (!game.active) {
|
|
1009
|
+
console.log("\n" + label.system + " " + colors.warning("No security breach in progress.\n"));
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
game.active = false;
|
|
1013
|
+
console.log("\n" + label.system + " " + colors.warning("Breach protocol aborted. Connection terminated.\n"));
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function handleGuess(input, game) {
|
|
1017
|
+
const guess = input.trim();
|
|
1018
|
+
if (!/^\d{4}$/.test(guess)) {
|
|
1019
|
+
console.log("\n" + label.error + " " + colors.danger("BREACH ERROR: Code must be exactly 4 digits (0-9).") + "\n");
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
game.attempts++;
|
|
1024
|
+
|
|
1025
|
+
const codeArr = game.code.split("");
|
|
1026
|
+
const guessArr = guess.split("");
|
|
1027
|
+
|
|
1028
|
+
let hits = 0;
|
|
1029
|
+
let closes = 0;
|
|
1030
|
+
|
|
1031
|
+
const codeUsed = [false, false, false, false];
|
|
1032
|
+
const guessUsed = [false, false, false, false];
|
|
1033
|
+
|
|
1034
|
+
// First pass: Hits
|
|
1035
|
+
for (let i = 0; i < 4; i++) {
|
|
1036
|
+
if (guessArr[i] === codeArr[i]) {
|
|
1037
|
+
hits++;
|
|
1038
|
+
codeUsed[i] = true;
|
|
1039
|
+
guessUsed[i] = true;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// Second pass: Closes
|
|
1044
|
+
for (let i = 0; i < 4; i++) {
|
|
1045
|
+
if (guessUsed[i]) continue;
|
|
1046
|
+
for (let j = 0; j < 4; j++) {
|
|
1047
|
+
if (codeUsed[j]) continue;
|
|
1048
|
+
if (guessArr[i] === codeArr[j]) {
|
|
1049
|
+
closes++;
|
|
1050
|
+
codeUsed[j] = true;
|
|
1051
|
+
break;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
console.log("");
|
|
1057
|
+
console.log(colors.magenta(` [BREACH ATTEMPT #${game.attempts} / ${game.maxAttempts}]`));
|
|
1058
|
+
console.log(colors.text(` BREACH INPUT: ${guess.split("").join(" ")}`));
|
|
1059
|
+
console.log(colors.success(` HITS (Pos): ${"█ ".repeat(hits)}${"░ ".repeat(4 - hits)} (${hits})`));
|
|
1060
|
+
console.log(colors.warning(` CLOSE (Val): ${"█ ".repeat(closes)}${"░ ".repeat(4 - closes)} (${closes})`));
|
|
1061
|
+
console.log("");
|
|
1062
|
+
|
|
1063
|
+
if (hits === 4) {
|
|
1064
|
+
console.log(label.system + " " + colors.success("MAINFRAME BYPASSED! Access granted. Decryption complete. 🔓\n"));
|
|
1065
|
+
game.active = false;
|
|
1066
|
+
} else if (game.attempts >= game.maxAttempts) {
|
|
1067
|
+
console.log(label.error + " " + colors.danger("SECURITY SHUTDOWN! Mainframe locked out. Intrusion logged. 🔒"));
|
|
1068
|
+
console.log(" Intrusion PIN was: " + colors.accent(game.code) + "\n");
|
|
1069
|
+
game.active = false;
|
|
1070
|
+
} else {
|
|
1071
|
+
console.log(colors.muted(" Recalibrating security bypass codes...") + "\n");
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
async function handleCopy(history) {
|
|
1076
|
+
const lastResponse = [...history].reverse().find((h) => h.role === "assistant");
|
|
1077
|
+
if (!lastResponse) {
|
|
1078
|
+
console.log("\n" + label.system + " " + colors.muted("No response to copy yet.\n"));
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
try {
|
|
1083
|
+
await copyToClipboard(lastResponse.content);
|
|
1084
|
+
console.log("\n" + label.system + " " + colors.success("✓ Last response copied to OS Clipboard successfully!\n"));
|
|
1085
|
+
} catch (err) {
|
|
1086
|
+
console.log("\n" + label.system + " " + colors.muted("Unable to copy automatically. Displaying content below:"));
|
|
1087
|
+
console.log(colors.text(lastResponse.content.slice(0, 800)));
|
|
1088
|
+
if (lastResponse.content.length > 800) {
|
|
1089
|
+
console.log(colors.dim(" [... truncated, use /export to save full conversation]"));
|
|
1090
|
+
}
|
|
1091
|
+
console.log("");
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function copyToClipboard(text) {
|
|
1096
|
+
return new Promise((resolve, reject) => {
|
|
1097
|
+
let command;
|
|
1098
|
+
if (process.platform === "win32") {
|
|
1099
|
+
command = "clip";
|
|
1100
|
+
} else if (process.platform === "darwin") {
|
|
1101
|
+
command = "pbcopy";
|
|
1102
|
+
} else {
|
|
1103
|
+
command = "xclip -selection clipboard || xsel -ib";
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
try {
|
|
1107
|
+
const child = exec(command, (err) => {
|
|
1108
|
+
if (err) reject(err);
|
|
1109
|
+
else resolve();
|
|
1110
|
+
});
|
|
1111
|
+
child.stdin.write(text);
|
|
1112
|
+
child.stdin.end();
|
|
1113
|
+
} catch (e) {
|
|
1114
|
+
reject(e);
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// ── Box / Badges / Theme helpers ─────────────────────────────
|
|
1120
|
+
|
|
1121
|
+
function providerBadge(result) {
|
|
1122
|
+
const badges = {
|
|
1123
|
+
"groq": chalk.bgHex("#1a2a1a").hex("#67ffb0")(" Groq "),
|
|
1124
|
+
"together ai": chalk.bgHex("#1a2a1a").hex("#67ffb0")(" Together "),
|
|
1125
|
+
"cerebras": chalk.bgHex("#1a2a1a").hex("#67ffb0")(" Cerebras "),
|
|
1126
|
+
"openai": chalk.bgHex("#1a2a1a").hex("#67ffb0")(" OpenAI "),
|
|
1127
|
+
"google": chalk.bgHex("#1a1a2a").hex("#2d7dff")(" Gemini "),
|
|
1128
|
+
"anthropic": chalk.bgHex("#2a1a2a").hex("#b06cff")(" Claude "),
|
|
1129
|
+
"xai": chalk.bgHex("#1a2a1a").hex("#67ffb0")(" Grok "),
|
|
1130
|
+
"mistral ai": chalk.bgHex("#1a1a2a").hex("#ffb900")(" Mistral "),
|
|
1131
|
+
"openrouter": chalk.bgHex("#1a1a2a").hex("#6ce8ff")(" OpenRouter "),
|
|
1132
|
+
"cohere": chalk.bgHex("#1a2a2a").hex("#6ce8ff")(" Cohere "),
|
|
1133
|
+
"deepseek": chalk.bgHex("#1a1a2a").hex("#2d7dff")(" DeepSeek "),
|
|
1134
|
+
"perplexity": chalk.bgHex("#1a2a2a").hex("#6ce8ff")(" Perplexity "),
|
|
1135
|
+
"fireworks ai": chalk.bgHex("#2a1a1a").hex("#ff6b8d")(" Fireworks "),
|
|
1136
|
+
"local": chalk.bgHex("#1a2a1a").hex("#67ffb0")(" Math Solver "),
|
|
1137
|
+
"krylo-fallback": chalk.bgHex("#0c1825").hex("#6ce8ff")(" Krylo "),
|
|
1138
|
+
};
|
|
1139
|
+
|
|
1140
|
+
const badge = badges[result.provider] || colors.muted(` ${result.provider} `);
|
|
1141
|
+
return badge + colors.dim(` Node ${result.node}`);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function signalBar(name, value) {
|
|
1145
|
+
const filled = Math.round(value / 10);
|
|
1146
|
+
const empty = 10 - filled;
|
|
1147
|
+
const bar = colors.accent("█".repeat(filled)) + colors.dim("░".repeat(empty));
|
|
1148
|
+
return `${colors.muted(name.padEnd(10))} ${bar} ${colors.muted(value + "%")}`;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function formatBytes(bytes) {
|
|
1152
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
1153
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
1154
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/**
|
|
1158
|
+
* Handles the management of custom slash command shortcuts.
|
|
1159
|
+
*/
|
|
1160
|
+
async function handleCustomCommands(args, ctx) {
|
|
1161
|
+
const sub = args[0]?.toLowerCase();
|
|
1162
|
+
|
|
1163
|
+
if (sub === "list") {
|
|
1164
|
+
const custom = ctx.aiConfig.CUSTOM_COMMANDS || {};
|
|
1165
|
+
const entries = Object.entries(custom);
|
|
1166
|
+
|
|
1167
|
+
console.log("");
|
|
1168
|
+
console.log(colors.brand(" ⚡ CUSTOM SHORTCUT COMMANDS"));
|
|
1169
|
+
console.log(separator("─"));
|
|
1170
|
+
|
|
1171
|
+
if (entries.length === 0) {
|
|
1172
|
+
console.log(" " + colors.muted("No custom commands registered."));
|
|
1173
|
+
console.log(" " + colors.muted("Create one: ") + colors.accent("/cmd add /explain \"Explain this code:\"") + "\n");
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
for (const [cmd, template] of entries) {
|
|
1178
|
+
console.log(` ${colors.accent(cmd.padEnd(16))} ${colors.text(template)}`);
|
|
1179
|
+
}
|
|
1180
|
+
console.log("");
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
if (sub === "add") {
|
|
1185
|
+
const name = args[1];
|
|
1186
|
+
const template = args.slice(2).join(" ");
|
|
1187
|
+
|
|
1188
|
+
if (!name || !template) {
|
|
1189
|
+
console.log("\n" + label.system + " " + colors.warning("Usage: /cmd add <name> <template>"));
|
|
1190
|
+
console.log(" " + colors.muted("Example: /cmd add /explain \"Explain this code in detail:\"") + "\n");
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
if (!name.startsWith("/")) {
|
|
1195
|
+
console.log("\n" + label.system + " " + colors.danger("ERROR: Command name must start with a slash '/' (e.g. /explain)") + "\n");
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
const builtIn = [
|
|
1200
|
+
"/help", "/mode", "/modes", "/attach", "/files", "/clear",
|
|
1201
|
+
"/providers", "/export", "/status", "/copy", "/exit", "/quit",
|
|
1202
|
+
"/theme", "/themes", "/history-clear", "/game", "/abort", "/cmd", "/guess", "/tokens"
|
|
1203
|
+
];
|
|
1204
|
+
|
|
1205
|
+
if (builtIn.includes(name.toLowerCase())) {
|
|
1206
|
+
console.log("\n" + label.system + " " + colors.danger(`ERROR: Cannot override system command "${name}"`) + "\n");
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
const custom = ctx.aiConfig.CUSTOM_COMMANDS || {};
|
|
1211
|
+
custom[name] = template;
|
|
1212
|
+
|
|
1213
|
+
await setConfigValue("CUSTOM_COMMANDS", custom);
|
|
1214
|
+
ctx.aiConfig.CUSTOM_COMMANDS = custom; // sync context
|
|
1215
|
+
|
|
1216
|
+
console.log("\n" + label.system + " " + colors.success(`✓ Command registered successfully!`));
|
|
1217
|
+
console.log(` ${colors.accent(name)} ➔ "${template}"\n`);
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
if (sub === "remove") {
|
|
1222
|
+
const name = args[1];
|
|
1223
|
+
if (!name) {
|
|
1224
|
+
console.log("\n" + label.system + " " + colors.warning("Usage: /cmd remove <name>") + "\n");
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
const custom = ctx.aiConfig.CUSTOM_COMMANDS || {};
|
|
1229
|
+
if (!custom[name]) {
|
|
1230
|
+
console.log("\n" + label.system + " " + colors.warning(`No custom command named "${name}" exists.`) + "\n");
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
delete custom[name];
|
|
1235
|
+
await setConfigValue("CUSTOM_COMMANDS", custom);
|
|
1236
|
+
ctx.aiConfig.CUSTOM_COMMANDS = custom; // sync context
|
|
1237
|
+
|
|
1238
|
+
console.log("\n" + label.system + " " + colors.success(`✓ Removed custom command: "${name}"\n`));
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
console.log("\n" + label.system + " " + colors.warning("Usage: /cmd <list|add|remove> [args]"));
|
|
1243
|
+
console.log(" " + colors.muted("Type /help for help or /cmd list to see existing shortcuts.\n"));
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
/**
|
|
1247
|
+
* Extracts all code blocks from a markdown string.
|
|
1248
|
+
*/
|
|
1249
|
+
function extractCodeBlocks(markdown) {
|
|
1250
|
+
const regex = /```[\w-]*\n([\s\S]*?)\n```/g;
|
|
1251
|
+
const blocks = [];
|
|
1252
|
+
let match;
|
|
1253
|
+
while ((match = regex.exec(markdown)) !== null) {
|
|
1254
|
+
blocks.push(match[1]);
|
|
1255
|
+
}
|
|
1256
|
+
return blocks;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/**
|
|
1260
|
+
* Manual file writing command. Extracts the last code block of the previous
|
|
1261
|
+
* assistant response and writes it to a file.
|
|
1262
|
+
*/
|
|
1263
|
+
async function handleWriteFile(args, ctx) {
|
|
1264
|
+
const filename = args.join(" ");
|
|
1265
|
+
if (!filename) {
|
|
1266
|
+
console.log("\n" + label.system + " " + colors.warning("Usage: /write <filename>") + "\n");
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
const lastResponse = [...ctx.history].reverse().find((h) => h.role === "assistant");
|
|
1271
|
+
if (!lastResponse) {
|
|
1272
|
+
console.log("\n" + label.system + " " + colors.muted("No assistant response available to write.\n"));
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
const codeBlocks = extractCodeBlocks(lastResponse.content);
|
|
1277
|
+
if (codeBlocks.length === 0) {
|
|
1278
|
+
console.log("\n" + label.system + " " + colors.warning("No code blocks found in the last response.\n"));
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
const blockContent = codeBlocks[codeBlocks.length - 1];
|
|
1283
|
+
const filepath = resolve(filename);
|
|
1284
|
+
|
|
1285
|
+
try {
|
|
1286
|
+
const { dirname } = await import("node:path");
|
|
1287
|
+
const { mkdir } = await import("node:fs/promises");
|
|
1288
|
+
const dir = dirname(filepath);
|
|
1289
|
+
await mkdir(dir, { recursive: true });
|
|
1290
|
+
await writeFile(filepath, blockContent, "utf-8");
|
|
1291
|
+
console.log("\n" + label.system + " " + colors.success(`✓ Code block successfully written to: ${filepath}\n`));
|
|
1292
|
+
} catch (err) {
|
|
1293
|
+
console.log("\n" + label.error + " " + colors.danger(`Write failed: ${err.message}\n`));
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/**
|
|
1298
|
+
* Interactive git commit command inside chat loop.
|
|
1299
|
+
*/
|
|
1300
|
+
async function handleCommitInsideChat(ctx) {
|
|
1301
|
+
const { getGitDiff, runGitCommit } = await import("./git.js");
|
|
1302
|
+
const { exec } = await import("node:child_process");
|
|
1303
|
+
const { promisify } = await import("node:util");
|
|
1304
|
+
const execAsync = promisify(exec);
|
|
1305
|
+
|
|
1306
|
+
try {
|
|
1307
|
+
const { diff, isStaged } = await getGitDiff();
|
|
1308
|
+
if (!diff) {
|
|
1309
|
+
console.log("\n" + label.system + " " + colors.warning("No staged or unstaged changes detected. Stage your files using 'git add' first.\n"));
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
if (!isStaged) {
|
|
1314
|
+
ctx.rl.pause();
|
|
1315
|
+
const stageAnswer = await new Promise((resolve) => {
|
|
1316
|
+
ctx.rl.question(colors.warning("\nNo staged changes found. Do you want to stage all changes automatically? [y/N]: "), resolve);
|
|
1317
|
+
});
|
|
1318
|
+
ctx.rl.resume();
|
|
1319
|
+
|
|
1320
|
+
if (stageAnswer.toLowerCase().trim() === "y" || stageAnswer.toLowerCase().trim() === "yes") {
|
|
1321
|
+
await execAsync("git add .");
|
|
1322
|
+
console.log(label.system + " " + colors.success("Staged all changes successfully."));
|
|
1323
|
+
} else {
|
|
1324
|
+
console.log("\n" + label.system + " " + colors.muted("Aborted. Please stage files using 'git add' first.\n"));
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
console.log("\n" + label.system + " " + colors.brand("Reading git diff and generating conventional commit message..."));
|
|
1330
|
+
console.log("");
|
|
1331
|
+
|
|
1332
|
+
const systemPrompt = "You are an expert developer assistant. Generate a concise, clear, and professional conventional commit message (e.g., 'feat: add login page', 'fix: resolve buffer overflow') based on the provided git diff. Output ONLY the commit message itself on a single line, with absolutely no backticks, markdown, explanations, prefix, or introductory text.";
|
|
1333
|
+
const userPrompt = `Here is the git diff:\n\n${diff}`;
|
|
1334
|
+
|
|
1335
|
+
let firstToken = true;
|
|
1336
|
+
let commitMessage = "";
|
|
1337
|
+
const onToken = (token) => {
|
|
1338
|
+
if (firstToken) {
|
|
1339
|
+
firstToken = false;
|
|
1340
|
+
process.stdout.write(label.aether + " Suggested Commit Message: " + colors.success(token));
|
|
1341
|
+
} else {
|
|
1342
|
+
process.stdout.write(colors.success(token));
|
|
1343
|
+
}
|
|
1344
|
+
commitMessage += token;
|
|
1345
|
+
};
|
|
1346
|
+
|
|
1347
|
+
const result = await routePrompt(userPrompt, systemPrompt, ctx.aiConfig, onToken);
|
|
1348
|
+
console.log("\n");
|
|
1349
|
+
|
|
1350
|
+
const cleanMessage = result.text.trim().replace(/^`+|`+$/g, ""); // strip quotes/backticks
|
|
1351
|
+
|
|
1352
|
+
ctx.rl.pause();
|
|
1353
|
+
const answer = await new Promise((resolve) => {
|
|
1354
|
+
ctx.rl.question(colors.muted("Commit with this message? [Y/n]: "), resolve);
|
|
1355
|
+
});
|
|
1356
|
+
ctx.rl.resume();
|
|
1357
|
+
|
|
1358
|
+
if (answer.toLowerCase().trim() === "n" || answer.toLowerCase().trim() === "no") {
|
|
1359
|
+
console.log("\n" + label.system + " " + colors.muted("Commit aborted.\n"));
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
console.log("\n" + label.system + " " + colors.brand("Executing git commit..."));
|
|
1364
|
+
const output = await runGitCommit(cleanMessage);
|
|
1365
|
+
console.log("\n" + colors.success(output) + "\n");
|
|
1366
|
+
|
|
1367
|
+
} catch (err) {
|
|
1368
|
+
console.log("\n" + label.error + " " + colors.danger(err.message) + "\n");
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
/**
|
|
1373
|
+
* Sandboxed interactive shell command execution.
|
|
1374
|
+
*/
|
|
1375
|
+
async function handleRunCommand(args, ctx) {
|
|
1376
|
+
const command = args.join(" ").trim();
|
|
1377
|
+
if (!command) {
|
|
1378
|
+
console.log("\n" + label.system + " " + colors.warning("Usage: /run <command>\n"));
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
const { spawn } = await import("node:child_process");
|
|
1383
|
+
|
|
1384
|
+
console.log("\n" + label.system + " " + colors.brand(`Running command: ${command}`));
|
|
1385
|
+
console.log(separator("─") + "\n");
|
|
1386
|
+
|
|
1387
|
+
ctx.rl.pause();
|
|
1388
|
+
|
|
1389
|
+
return new Promise((resolve) => {
|
|
1390
|
+
const isWindows = process.platform === "win32";
|
|
1391
|
+
const shell = isWindows ? "cmd.exe" : "/bin/sh";
|
|
1392
|
+
const shellArgs = isWindows ? ["/c", command] : ["-c", command];
|
|
1393
|
+
|
|
1394
|
+
const child = spawn(shell, shellArgs, { stdio: "inherit" });
|
|
1395
|
+
|
|
1396
|
+
child.on("close", (code) => {
|
|
1397
|
+
ctx.rl.resume();
|
|
1398
|
+
console.log("\n" + separator("─"));
|
|
1399
|
+
if (code === 0) {
|
|
1400
|
+
console.log(label.system + " " + colors.success(`✓ Command exited successfully (code 0).\n`));
|
|
1401
|
+
} else {
|
|
1402
|
+
console.log(label.system + " " + colors.danger(`✗ Command failed with exit status ${code}.\n`));
|
|
1403
|
+
}
|
|
1404
|
+
resolve();
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
child.on("error", (err) => {
|
|
1408
|
+
ctx.rl.resume();
|
|
1409
|
+
console.log("\n" + label.error + " " + colors.danger(`Failed to start command: ${err.message}\n`));
|
|
1410
|
+
resolve();
|
|
1411
|
+
});
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/**
|
|
1416
|
+
* Interactive display of session token usage statistics.
|
|
1417
|
+
*/
|
|
1418
|
+
async function handleTokensDisplay(ctx) {
|
|
1419
|
+
const stats = getSessionTokenStats();
|
|
1420
|
+
const breakdown = getBreakdownByModel();
|
|
1421
|
+
|
|
1422
|
+
console.log("\n" + separator("━"));
|
|
1423
|
+
console.log(colors.accent.bold(" ★ AETHER SESSION TOKEN TELEMETRY ★"));
|
|
1424
|
+
console.log(separator("─"));
|
|
1425
|
+
|
|
1426
|
+
const models = Object.keys(breakdown);
|
|
1427
|
+
if (models.length === 0) {
|
|
1428
|
+
console.log(colors.muted(" No queries executed in this session yet."));
|
|
1429
|
+
} else {
|
|
1430
|
+
// Print header
|
|
1431
|
+
console.log(
|
|
1432
|
+
colors.brand(" " + "Model".padEnd(35) + "Prompt".padStart(10) + "Completion".padStart(12) + "Total".padStart(10))
|
|
1433
|
+
);
|
|
1434
|
+
console.log(colors.dim(" " + "─".repeat(67)));
|
|
1435
|
+
for (const [model, data] of Object.entries(breakdown)) {
|
|
1436
|
+
const truncatedModel = model.length > 33 ? model.slice(0, 30) + "..." : model;
|
|
1437
|
+
console.log(
|
|
1438
|
+
" " + colors.text(truncatedModel.padEnd(35)) +
|
|
1439
|
+
colors.brand(data.prompt.toLocaleString().padStart(10)) +
|
|
1440
|
+
colors.brand(data.completion.toLocaleString().padStart(12)) +
|
|
1441
|
+
colors.accent.bold(data.total.toLocaleString().padStart(10))
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
console.log(separator("─"));
|
|
1447
|
+
console.log(" " + colors.accent("Total Exchanges:") + colors.text(` ${stats.exchanges}`));
|
|
1448
|
+
console.log(" " + colors.accent("Total Tokens:") + colors.text(` Prompt: ${stats.prompt.toLocaleString()} | Completion: ${stats.completion.toLocaleString()} | Sum: `) + colors.brand.bold(stats.total.toLocaleString()));
|
|
1449
|
+
console.log(separator("━") + "\n");
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
/**
|
|
1453
|
+
* Streams an AI query prompt and prints telemetry details at the end.
|
|
1454
|
+
*/
|
|
1455
|
+
async function executeAISpecialCommand(prompt, specialLabel, ctx) {
|
|
1456
|
+
const systemPrompt = ctx.currentMode.systemPrompt + "\n" + AGENT_INSTRUCTIONS;
|
|
1457
|
+
let hasStarted = false;
|
|
1458
|
+
let responseText = "";
|
|
1459
|
+
const queryStartTime = Date.now();
|
|
1460
|
+
let firstTokenTime = 0;
|
|
1461
|
+
|
|
1462
|
+
const onToken = (token) => {
|
|
1463
|
+
if (!hasStarted) {
|
|
1464
|
+
hasStarted = true;
|
|
1465
|
+
firstTokenTime = Date.now();
|
|
1466
|
+
process.stdout.write("\n" + label.aether + " " + colors.accent(specialLabel) + "\n" + separator("─") + "\n\n");
|
|
1467
|
+
}
|
|
1468
|
+
process.stdout.write(colors.success(token));
|
|
1469
|
+
responseText += token;
|
|
1470
|
+
};
|
|
1471
|
+
|
|
1472
|
+
const result = await routePrompt(prompt, systemPrompt, ctx.aiConfig, onToken);
|
|
1473
|
+
console.log("\n");
|
|
1474
|
+
|
|
1475
|
+
const elapsedSec = ((Date.now() - queryStartTime) / 1000).toFixed(1);
|
|
1476
|
+
let speedText = "";
|
|
1477
|
+
if (firstTokenTime > 0) {
|
|
1478
|
+
const streamElapsed = (Date.now() - firstTokenTime) / 1000;
|
|
1479
|
+
if (streamElapsed > 0.05) {
|
|
1480
|
+
const estimatedTokens = Math.max(1, Math.round(responseText.length / 4));
|
|
1481
|
+
const tps = (estimatedTokens / streamElapsed).toFixed(1);
|
|
1482
|
+
speedText = ` • ${tps} tok/s`;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
const showTokens = ctx.aiConfig.SHOW_TOKENS !== "false";
|
|
1487
|
+
let tokensText = "";
|
|
1488
|
+
if (showTokens && result.usage) {
|
|
1489
|
+
const { promptTokens, completionTokens } = result.usage;
|
|
1490
|
+
tokensText = ` • ${promptTokens.toLocaleString()} in / ${completionTokens.toLocaleString()} out tokens`;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
console.log(separator("─"));
|
|
1494
|
+
console.log(
|
|
1495
|
+
" " + colors.dim(`Node ${result.node} • ${result.provider}`) +
|
|
1496
|
+
(result.model ? colors.dim(` • ${result.model}`) : "") +
|
|
1497
|
+
colors.dim(` • ${elapsedSec}s${speedText}`) +
|
|
1498
|
+
colors.dim(tokensText)
|
|
1499
|
+
);
|
|
1500
|
+
console.log("");
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
/**
|
|
1504
|
+
* Handler for the /review command (git diff analysis).
|
|
1505
|
+
*/
|
|
1506
|
+
async function handleReviewCommand(ctx) {
|
|
1507
|
+
console.log("\n" + label.system + " " + colors.muted("Running git diff to fetch repository changes..."));
|
|
1508
|
+
try {
|
|
1509
|
+
const { diff, isStaged } = await getGitDiff();
|
|
1510
|
+
if (!diff) {
|
|
1511
|
+
console.log(label.system + " " + colors.success("✓ No changes detected in the repository to review.\n"));
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
const specialLabel = `Reviewing ${isStaged ? "staged" : "unstaged"} changes...`;
|
|
1516
|
+
const prompt = `Review the following git diff. Identify potential bugs, logical issues, security concerns, performance problems, and recommend optimization or code cleanup. Keep it concise, practical, and highly technical:\n\n\`\`\`diff\n${diff}\n\`\`\``;
|
|
1517
|
+
|
|
1518
|
+
await executeAISpecialCommand(prompt, specialLabel, ctx);
|
|
1519
|
+
} catch (err) {
|
|
1520
|
+
console.log(label.system + " " + colors.danger(`Error: ${err.message}\n`));
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
/**
|
|
1525
|
+
* Handler for the /diagnose command (build & test diagnostics execution).
|
|
1526
|
+
*/
|
|
1527
|
+
async function handleDiagnoseCommand(args, ctx) {
|
|
1528
|
+
const defaultCmd = ctx.aiConfig.DIAGNOSE_CMD || "npm test";
|
|
1529
|
+
const cmdToRun = args.join(" ").trim() || defaultCmd;
|
|
1530
|
+
|
|
1531
|
+
console.log("\n" + label.system + " " + colors.muted(`Running diagnostics command: "${cmdToRun}"...`));
|
|
1532
|
+
|
|
1533
|
+
const spinner = createSpinner("Executing diagnostics").start();
|
|
1534
|
+
try {
|
|
1535
|
+
const { exec } = await import("node:child_process");
|
|
1536
|
+
const { promisify } = await import("node:util");
|
|
1537
|
+
const execAsync = promisify(exec);
|
|
1538
|
+
await execAsync(cmdToRun);
|
|
1539
|
+
spinner.succeed("Diagnostics complete!");
|
|
1540
|
+
console.log("\n" + label.system + " " + colors.success("✓ Diagnostics clean! Build and tests passed successfully.\n"));
|
|
1541
|
+
} catch (err) {
|
|
1542
|
+
spinner.fail("Diagnostics failed!");
|
|
1543
|
+
|
|
1544
|
+
const output = (err.stdout || "") + "\n" + (err.stderr || "");
|
|
1545
|
+
console.log("\n" + label.system + " " + colors.warning(`Diagnostics returned exit code ${err.code}.`));
|
|
1546
|
+
console.log(colors.muted("Analyzing compiler/test output logs...\n"));
|
|
1547
|
+
|
|
1548
|
+
const prompt = `The diagnostics command "${cmdToRun}" failed with exit code ${err.code}. Analyze the following stdout and stderr logs to determine the root cause, identify the files/lines causing the failure, and provide a step-by-step resolution and debugging plan:\n\n\`\`\`\n${output.slice(0, 15000)}\n\`\`\``;
|
|
1549
|
+
|
|
1550
|
+
await executeAISpecialCommand(prompt, "Analyzing diagnostics logs...", ctx);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
/**
|
|
1555
|
+
* Handler for file analysis commands: /explain, /refactor, /bug, /doc, /translate.
|
|
1556
|
+
*/
|
|
1557
|
+
async function handleFileAICommand(cmdName, args, ctx) {
|
|
1558
|
+
const filePath = args[0];
|
|
1559
|
+
if (!filePath) {
|
|
1560
|
+
console.log("\n" + label.system + " " + colors.warning(`Usage: ${cmdName} <file_path>\n`));
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
// Resolve path
|
|
1565
|
+
const resolvedPath = resolve(process.cwd(), filePath);
|
|
1566
|
+
|
|
1567
|
+
// Verify path is inside the workspace
|
|
1568
|
+
const { isInsideWorkspace } = await import("./agent.js");
|
|
1569
|
+
if (!isInsideWorkspace(resolvedPath)) {
|
|
1570
|
+
console.log("\n" + label.system + " " + colors.danger("Error: Path is outside the current workspace sandbox.\n"));
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
if (!existsSync(resolvedPath)) {
|
|
1575
|
+
console.log("\n" + label.system + " " + colors.danger(`Error: File does not exist at "${filePath}"\n`));
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
const stat = statSync(resolvedPath);
|
|
1580
|
+
if (stat.isDirectory()) {
|
|
1581
|
+
console.log("\n" + label.system + " " + colors.danger(`Error: "${filePath}" is a directory. File path required.\n`));
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
if (stat.size > 150 * 1024) { // 150KB limit
|
|
1586
|
+
console.log("\n" + label.system + " " + colors.warning(`Warning: File "${filePath}" is too large (${Math.round(stat.size / 1024)}KB). Limits are 150KB to protect context limit.\n`));
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
// Read file content
|
|
1591
|
+
let content;
|
|
1592
|
+
try {
|
|
1593
|
+
const { parseFile } = await import("./file-parser.js");
|
|
1594
|
+
const parsed = await parseFile(resolvedPath);
|
|
1595
|
+
content = parsed.content;
|
|
1596
|
+
} catch (err) {
|
|
1597
|
+
console.log("\n" + label.system + " " + colors.danger(`Error parsing file: ${err.message}\n`));
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
let prompt = "";
|
|
1602
|
+
let labelText = "";
|
|
1603
|
+
|
|
1604
|
+
switch (cmdName.toLowerCase()) {
|
|
1605
|
+
case "/explain":
|
|
1606
|
+
labelText = `Explaining ${filePath}...`;
|
|
1607
|
+
prompt = `Explain the architecture, design patterns, logic flow, and purpose of the following code. Be clear, technical, and structured:\n\n\`\`\`\n${content}\n\`\`\``;
|
|
1608
|
+
break;
|
|
1609
|
+
case "/refactor":
|
|
1610
|
+
labelText = `Refactoring ${filePath}...`;
|
|
1611
|
+
prompt = `Suggest refactoring improvements for the following code. Focus on clean code design principles, optimization, readability, reducing complexity, and fixing potential logic bugs. Return both the refactored code block and explanations:\n\n\`\`\`\n${content}\n\`\`\``;
|
|
1612
|
+
break;
|
|
1613
|
+
case "/bug":
|
|
1614
|
+
labelText = `Auditing bugs in ${filePath}...`;
|
|
1615
|
+
prompt = `Perform a thorough static analysis and code review of the following code. Identify potential logical bugs, race conditions, edge case failures, performance bottlenecks, and security hazards. Suggest fixes:\n\n\`\`\`\n${content}\n\`\`\``;
|
|
1616
|
+
break;
|
|
1617
|
+
case "/doc":
|
|
1618
|
+
labelText = `Generating documentation for ${filePath}...`;
|
|
1619
|
+
prompt = `Generate comprehensive API documentation, JSDoc/docstrings, and comments for the following code. Ensure code parameters, return values, and types are documented:\n\n\`\`\`\n${content}\n\`\`\``;
|
|
1620
|
+
break;
|
|
1621
|
+
case "/translate":
|
|
1622
|
+
const targetLang = args[1];
|
|
1623
|
+
if (!targetLang) {
|
|
1624
|
+
console.log("\n" + label.system + " " + colors.warning(`Usage: /translate <file_path> <target_language>\n`));
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
labelText = `Translating ${filePath} to ${targetLang}...`;
|
|
1628
|
+
prompt = `Translate the following code into ${targetLang}. Return a clean, syntactically correct, and beautifully structured code block of the translated code:\n\n\`\`\`\n${content}\n\`\`\``;
|
|
1629
|
+
break;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
try {
|
|
1633
|
+
await executeAISpecialCommand(prompt, labelText, ctx);
|
|
1634
|
+
} catch (err) {
|
|
1635
|
+
console.log("\n" + label.system + " " + colors.danger(`Error: ${err.message}\n`));
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
/**
|
|
1640
|
+
* Handler for the /search command (workspace file crawler and AI semantic finder).
|
|
1641
|
+
*/
|
|
1642
|
+
async function handleSearchCommand(args, ctx) {
|
|
1643
|
+
const isAi = args[0] === "--ai";
|
|
1644
|
+
const queryArgs = isAi ? args.slice(1) : args;
|
|
1645
|
+
const query = queryArgs.join(" ").trim();
|
|
1646
|
+
|
|
1647
|
+
if (!query) {
|
|
1648
|
+
console.log("\n" + label.system + " " + colors.warning("Usage: /search [--ai] <query_string>\n"));
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
const { workspaceSearch, crawlDirectory } = await import("./search.js");
|
|
1653
|
+
|
|
1654
|
+
if (isAi) {
|
|
1655
|
+
console.log("\n" + label.system + " " + colors.muted("Scanning workspace project tree for semantic search..."));
|
|
1656
|
+
const files = crawlDirectory(process.cwd());
|
|
1657
|
+
const { relative } = await import("node:path");
|
|
1658
|
+
const relativePaths = files.map((f) => relative(process.cwd(), f).replace(/\\/g, "/"));
|
|
1659
|
+
|
|
1660
|
+
// Construct semantic prompt
|
|
1661
|
+
const prompt = `Here is the directory structure / file listing of the current workspace:\n\n${relativePaths.slice(0, 100).join("\n")}\n\nBased on this file listing, identify and explain where the following logic or system is implemented, listing the relevant files: ${query}`;
|
|
1662
|
+
|
|
1663
|
+
await executeAISpecialCommand(prompt, `Semantic search: "${query}"`, ctx);
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
console.log("\n" + label.system + " " + colors.muted(`Searching workspace for "${query}"...`));
|
|
1668
|
+
const results = workspaceSearch(query);
|
|
1669
|
+
|
|
1670
|
+
if (results.length === 0) {
|
|
1671
|
+
console.log("\n" + label.system + " " + colors.warning(`✓ No matches found for "${query}" in workspace.\n`));
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
console.log("\n" + separator("━"));
|
|
1676
|
+
console.log(colors.accent.bold(` ★ WORKSPACE SEARCH RESULTS FOR "${query.toUpperCase()}" ★`));
|
|
1677
|
+
console.log(separator("─"));
|
|
1678
|
+
|
|
1679
|
+
// Print header
|
|
1680
|
+
console.log(
|
|
1681
|
+
colors.brand(" " + "File Path".padEnd(45) + "Line".padStart(6) + " " + "Preview")
|
|
1682
|
+
);
|
|
1683
|
+
console.log(colors.dim(" " + "─".repeat(80)));
|
|
1684
|
+
|
|
1685
|
+
// Display matches (limit to top 50 to prevent terminal overflow)
|
|
1686
|
+
const displayLimit = 50;
|
|
1687
|
+
const visibleResults = results.slice(0, displayLimit);
|
|
1688
|
+
|
|
1689
|
+
for (const match of visibleResults) {
|
|
1690
|
+
const truncatedPath = match.relativePath.length > 43 ? "..." + match.relativePath.slice(-40) : match.relativePath;
|
|
1691
|
+
const truncatedLine = match.lineContent.length > 50 ? match.lineContent.slice(0, 47) + "..." : match.lineContent;
|
|
1692
|
+
console.log(
|
|
1693
|
+
" " + colors.text(truncatedPath.padEnd(45)) +
|
|
1694
|
+
colors.brand(match.lineNumber.toString().padStart(6)) +
|
|
1695
|
+
" " + colors.muted(truncatedLine)
|
|
1696
|
+
);
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
console.log(separator("─"));
|
|
1700
|
+
if (results.length > displayLimit) {
|
|
1701
|
+
console.log(" " + colors.warning(`⚠ Showing first ${displayLimit} of ${results.length} total matches.`));
|
|
1702
|
+
} else {
|
|
1703
|
+
console.log(" " + colors.success(`✓ Found ${results.length} matches across the workspace.`));
|
|
1704
|
+
}
|
|
1705
|
+
console.log(separator("━") + "\n");
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
/**
|
|
1709
|
+
* Runs an autonomous, self-correcting debug/test feedback loop.
|
|
1710
|
+
*/
|
|
1711
|
+
export async function handleAutopilotDebug(cmdArg, ctx) {
|
|
1712
|
+
const { exec } = await import("node:child_process");
|
|
1713
|
+
const { promisify } = await import("node:util");
|
|
1714
|
+
const execAsync = promisify(exec);
|
|
1715
|
+
|
|
1716
|
+
const testCmd = cmdArg.trim() || ctx.aiConfig.DIAGNOSE_CMD || "npm test";
|
|
1717
|
+
|
|
1718
|
+
console.log("\n" + label.system + " " + colors.brand("🤖 AUTOPILOT AUTONOMOUS DEBUG LOOP"));
|
|
1719
|
+
console.log(separator("─"));
|
|
1720
|
+
console.log(keyValue(" Diagnostic Command", testCmd));
|
|
1721
|
+
console.log("");
|
|
1722
|
+
|
|
1723
|
+
console.log(colors.cyan(`⚡ Running initial diagnostics: ${testCmd}`));
|
|
1724
|
+
|
|
1725
|
+
let stdout = "";
|
|
1726
|
+
let stderr = "";
|
|
1727
|
+
let passed = false;
|
|
1728
|
+
let runErr = null;
|
|
1729
|
+
|
|
1730
|
+
try {
|
|
1731
|
+
const res = await execAsync(testCmd);
|
|
1732
|
+
stdout = res.stdout;
|
|
1733
|
+
stderr = res.stderr;
|
|
1734
|
+
passed = true;
|
|
1735
|
+
} catch (err) {
|
|
1736
|
+
stdout = err.stdout || "";
|
|
1737
|
+
stderr = err.stderr || "";
|
|
1738
|
+
runErr = err;
|
|
1739
|
+
passed = false;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
if (passed) {
|
|
1743
|
+
console.log("\n" + label.system + " " + colors.success(`✓ Diagnostics passed successfully on the first run!\n`));
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
console.log("\n" + label.system + " " + colors.danger(`❌ Initial run failed. Starting self-correcting debug loop...\n`));
|
|
1748
|
+
|
|
1749
|
+
let iteration = 1;
|
|
1750
|
+
const maxIterations = 3;
|
|
1751
|
+
let currentPrompt = `
|
|
1752
|
+
The test/build command "${testCmd}" failed.
|
|
1753
|
+
Here is the execution output:
|
|
1754
|
+
--- STDOUT ---
|
|
1755
|
+
${stdout}
|
|
1756
|
+
--- STDERR ---
|
|
1757
|
+
${stderr}
|
|
1758
|
+
--- ERROR ---
|
|
1759
|
+
${runErr ? runErr.message : ""}
|
|
1760
|
+
|
|
1761
|
+
Please inspect the logs. If you need to read any files first to locate the bug, use the [READ_FILE: path] tool. If you know how to fix it, write the corrected files using [WRITE_FILE: path]...[END_WRITE].
|
|
1762
|
+
After you output your edits or read operations, we will apply them and re-run the command.
|
|
1763
|
+
`;
|
|
1764
|
+
|
|
1765
|
+
const debugSystemPrompt = `
|
|
1766
|
+
You are Aether Autopilot in Autonomous Debug Mode.
|
|
1767
|
+
A terminal command failed. Your goal is to analyze the error logs, read relevant source files to find the bug, write fixes to those files, and make sure the diagnostics pass.
|
|
1768
|
+
You can read files using: [READ_FILE: path/to/file]
|
|
1769
|
+
You can write files using:
|
|
1770
|
+
[WRITE_FILE: path/to/file]
|
|
1771
|
+
<new file content>
|
|
1772
|
+
[END_WRITE]
|
|
1773
|
+
|
|
1774
|
+
Rules:
|
|
1775
|
+
- You must identify the root cause of the error.
|
|
1776
|
+
- First, read the relevant file(s) that might have caused the error.
|
|
1777
|
+
- Then, output the corrected file content.
|
|
1778
|
+
- Do not run any command blocks yourself. The environment will automatically re-run the test command for you after you output your modifications.
|
|
1779
|
+
- Keep your changes minimal and target only the bug.
|
|
1780
|
+
`;
|
|
1781
|
+
|
|
1782
|
+
while (iteration <= maxIterations) {
|
|
1783
|
+
console.log(colors.accent(`\n🤖 [Autopilot Debug - Iteration ${iteration}/${maxIterations}]`));
|
|
1784
|
+
|
|
1785
|
+
const spinner = createSpinner(colors.muted(`Aether analyzing diagnostics & planning fixes...`));
|
|
1786
|
+
spinner.start();
|
|
1787
|
+
|
|
1788
|
+
let streamedText = "";
|
|
1789
|
+
let hasStartedStreaming = false;
|
|
1790
|
+
const filter = new StreamFilter(process.stdout.write.bind(process.stdout));
|
|
1791
|
+
const onToken = (token) => {
|
|
1792
|
+
if (!hasStartedStreaming) {
|
|
1793
|
+
hasStartedStreaming = true;
|
|
1794
|
+
spinner.stop();
|
|
1795
|
+
}
|
|
1796
|
+
filter.write(token);
|
|
1797
|
+
streamedText += token;
|
|
1798
|
+
};
|
|
1799
|
+
|
|
1800
|
+
let result;
|
|
1801
|
+
try {
|
|
1802
|
+
result = await routePrompt(currentPrompt, debugSystemPrompt, ctx.aiConfig, onToken, ctx.history);
|
|
1803
|
+
spinner.stop();
|
|
1804
|
+
filter.flush();
|
|
1805
|
+
} catch (routeErr) {
|
|
1806
|
+
spinner.stop();
|
|
1807
|
+
console.log("\n" + label.error + " " + colors.danger(`AI Routing Failed: ${routeErr.message}`));
|
|
1808
|
+
break;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
if (hasStartedStreaming) {
|
|
1812
|
+
clearStreamedText(filter.filteredText);
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
console.log("");
|
|
1816
|
+
console.log(label.aether + " " + providerBadge(result));
|
|
1817
|
+
console.log(separator("─"));
|
|
1818
|
+
console.log("");
|
|
1819
|
+
|
|
1820
|
+
const rendered = getMarked().parse(result.text);
|
|
1821
|
+
console.log(rendered);
|
|
1822
|
+
console.log(separator("─"));
|
|
1823
|
+
|
|
1824
|
+
ctx.history.push({ role: "user", content: currentPrompt, timestamp: new Date() });
|
|
1825
|
+
ctx.history.push({
|
|
1826
|
+
role: "assistant",
|
|
1827
|
+
content: result.text,
|
|
1828
|
+
provider: result.provider,
|
|
1829
|
+
model: result.model,
|
|
1830
|
+
node: result.node,
|
|
1831
|
+
timestamp: new Date(),
|
|
1832
|
+
});
|
|
1833
|
+
await saveHistory(ctx.history, ctx.currentMode.name);
|
|
1834
|
+
|
|
1835
|
+
const { processAgentBlocks } = await import("./agent.js");
|
|
1836
|
+
const toolResults = await processAgentBlocks(result.text, ctx.aiConfig, ctx.rl);
|
|
1837
|
+
|
|
1838
|
+
console.log(colors.cyan(`\n⚡ Re-running diagnostic command (Attempt ${iteration}/${maxIterations}): ${testCmd}`));
|
|
1839
|
+
|
|
1840
|
+
let testStdout = "";
|
|
1841
|
+
let testStderr = "";
|
|
1842
|
+
let testPassed = false;
|
|
1843
|
+
let testRunErr = null;
|
|
1844
|
+
|
|
1845
|
+
try {
|
|
1846
|
+
const res = await execAsync(testCmd);
|
|
1847
|
+
testStdout = res.stdout;
|
|
1848
|
+
testStderr = res.stderr;
|
|
1849
|
+
testPassed = true;
|
|
1850
|
+
} catch (err) {
|
|
1851
|
+
testStdout = err.stdout || "";
|
|
1852
|
+
testStderr = err.stderr || "";
|
|
1853
|
+
testRunErr = err;
|
|
1854
|
+
testPassed = false;
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
if (testPassed) {
|
|
1858
|
+
console.log("\n" + label.system + " " + colors.success(`✓ Diagnostics passed successfully after autopilot debug corrections!\n`));
|
|
1859
|
+
break;
|
|
1860
|
+
} else {
|
|
1861
|
+
console.log("\n" + label.system + " " + colors.danger(`❌ Diagnostic check still failing (Attempt ${iteration}/${maxIterations}).`));
|
|
1862
|
+
|
|
1863
|
+
let toolOutputs = "### Agent Tool Outputs:\n";
|
|
1864
|
+
for (const tr of toolResults) {
|
|
1865
|
+
if (tr.success) {
|
|
1866
|
+
if (tr.tool === "READ_FILE") {
|
|
1867
|
+
toolOutputs += `\n- READ_FILE "${tr.arg}" succeeded. Content:\n\`\`\`\n${tr.content}\n\`\`\`;`;
|
|
1868
|
+
} else if (tr.tool === "WRITE_FILE") {
|
|
1869
|
+
toolOutputs += `\n- WRITE_FILE "${tr.arg}" succeeded.`;
|
|
1870
|
+
} else if (tr.tool === "SEARCH_WEB") {
|
|
1871
|
+
const list = tr.results.map((r, i) => `${i+1}. [${r.title}](${r.url})\n ${r.snippet}`).join("\n");
|
|
1872
|
+
toolOutputs += `\n- SEARCH_WEB "${tr.arg}" succeeded. Results:\n${list}`;
|
|
1873
|
+
}
|
|
1874
|
+
} else {
|
|
1875
|
+
toolOutputs += `\n- ${tr.tool} "${tr.arg}" failed: ${tr.error}`;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
currentPrompt = `
|
|
1880
|
+
${toolOutputs}
|
|
1881
|
+
|
|
1882
|
+
The test/build command "${testCmd}" is still failing.
|
|
1883
|
+
Here is the new execution output:
|
|
1884
|
+
--- STDOUT ---
|
|
1885
|
+
${testStdout}
|
|
1886
|
+
--- STDERR ---
|
|
1887
|
+
${testStderr}
|
|
1888
|
+
--- ERROR ---
|
|
1889
|
+
${testRunErr ? testRunErr.message : ""}
|
|
1890
|
+
|
|
1891
|
+
Please analyze the remaining issues, read any other files you need, and apply further fixes.
|
|
1892
|
+
`;
|
|
1893
|
+
iteration++;
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
if (iteration > maxIterations) {
|
|
1898
|
+
console.log("\n" + label.system + " " + colors.warning(`⚠️ Max autopilot debug iterations reached. Review the diagnostics manually.\n`));
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
/**
|
|
1903
|
+
* Renders the custom interactive Git TUI file stager and branch tree.
|
|
1904
|
+
*/
|
|
1905
|
+
export async function handleGitTUI(ctx) {
|
|
1906
|
+
const { execSync } = await import("node:child_process");
|
|
1907
|
+
|
|
1908
|
+
try {
|
|
1909
|
+
execSync("git rev-parse --is-inside-work-tree", { stdio: "ignore" });
|
|
1910
|
+
} catch (e) {
|
|
1911
|
+
console.log("\n" + label.error + " " + colors.danger("Not a git repository (or git is not installed).\n"));
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
const stdin = process.stdin;
|
|
1916
|
+
const stdout = process.stdout;
|
|
1917
|
+
const wasRaw = stdin.isRaw;
|
|
1918
|
+
|
|
1919
|
+
stdin.setRawMode(true);
|
|
1920
|
+
stdin.resume();
|
|
1921
|
+
stdin.setEncoding("utf8");
|
|
1922
|
+
stdout.write("\x1b[?25l"); // Hide cursor
|
|
1923
|
+
|
|
1924
|
+
let files = getGitStatusFiles();
|
|
1925
|
+
let activeIndex = 0;
|
|
1926
|
+
let renderedLines = 0;
|
|
1927
|
+
|
|
1928
|
+
function getGitStatusFiles() {
|
|
1929
|
+
try {
|
|
1930
|
+
const out = execSync("git status --porcelain", { encoding: "utf8" }).trim();
|
|
1931
|
+
if (!out) return [];
|
|
1932
|
+
return out.split("\n").map(line => {
|
|
1933
|
+
const status = line.slice(0, 2);
|
|
1934
|
+
const file = line.slice(3).trim();
|
|
1935
|
+
const isStaged = status[0] !== " " && status[0] !== "?";
|
|
1936
|
+
const isUnstaged = status[1] !== " " || status[0] === "?";
|
|
1937
|
+
return {
|
|
1938
|
+
path: file,
|
|
1939
|
+
status,
|
|
1940
|
+
staged: isStaged,
|
|
1941
|
+
unstaged: isUnstaged
|
|
1942
|
+
};
|
|
1943
|
+
});
|
|
1944
|
+
} catch (e) {
|
|
1945
|
+
return [];
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
function render() {
|
|
1950
|
+
if (renderedLines > 0) {
|
|
1951
|
+
stdout.write(`\x1b[${renderedLines}A\x1b[J`);
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
let lines = [];
|
|
1955
|
+
lines.push(colors.brand("🌿 AETHER INTERACTIVE GIT TUI"));
|
|
1956
|
+
lines.push(separator("─"));
|
|
1957
|
+
|
|
1958
|
+
let branchGraph = "";
|
|
1959
|
+
try {
|
|
1960
|
+
branchGraph = execSync("git log --graph --oneline --decorate -n 6", { encoding: "utf8" }).trim();
|
|
1961
|
+
} catch (e) {
|
|
1962
|
+
branchGraph = " No git history found.";
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
lines.push(colors.accent("Commit Graph & History:"));
|
|
1966
|
+
if (branchGraph) {
|
|
1967
|
+
lines.push(branchGraph.split("\n").map(l => " " + colors.muted(l)).join("\n"));
|
|
1968
|
+
}
|
|
1969
|
+
lines.push(separator("─"));
|
|
1970
|
+
|
|
1971
|
+
lines.push(colors.accent("Modified Files:"));
|
|
1972
|
+
|
|
1973
|
+
if (files.length === 0) {
|
|
1974
|
+
lines.push(colors.success(" Clean working directory. Nothing to stage/commit."));
|
|
1975
|
+
} else {
|
|
1976
|
+
files.forEach((file, index) => {
|
|
1977
|
+
const isActive = index === activeIndex;
|
|
1978
|
+
const pointer = isActive ? colors.accent("❯ ") : " ";
|
|
1979
|
+
const checkbox = file.staged ? colors.success("[⬢] ") : colors.muted("[⬡] ");
|
|
1980
|
+
|
|
1981
|
+
let statusColor = colors.text;
|
|
1982
|
+
if (file.status[0] === "?" || file.status[1] === "?") {
|
|
1983
|
+
statusColor = colors.warning;
|
|
1984
|
+
} else if (file.staged && !file.unstaged) {
|
|
1985
|
+
statusColor = colors.success;
|
|
1986
|
+
} else if (file.unstaged) {
|
|
1987
|
+
statusColor = colors.danger;
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
const pathText = isActive ? colors.brand(file.path) : statusColor(file.path);
|
|
1991
|
+
const statusText = colors.dim(`(${file.status})`);
|
|
1992
|
+
lines.push(pointer + checkbox + pathText + " " + statusText);
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
lines.push(separator("─"));
|
|
1997
|
+
lines.push(colors.muted("Hotkeys: [Space] Stage/Unstage | [D] Discard | [C] Commit | [P] Push | [Q/Esc] Quit"));
|
|
1998
|
+
|
|
1999
|
+
const outputStr = lines.join("\n") + "\n";
|
|
2000
|
+
stdout.write(outputStr);
|
|
2001
|
+
renderedLines = lines.length;
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
render();
|
|
2005
|
+
|
|
2006
|
+
return new Promise((resolve) => {
|
|
2007
|
+
async function handleKey(key) {
|
|
2008
|
+
console.log("TUI_KEY:", JSON.stringify(key));
|
|
2009
|
+
if (key === "\u0003" || key === "q" || key === "Q" || key === "\u001b") {
|
|
2010
|
+
cleanup();
|
|
2011
|
+
resolve();
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
if (key === "\u001b[A") { // Up Arrow
|
|
2016
|
+
if (files.length > 0) {
|
|
2017
|
+
activeIndex = (activeIndex - 1 + files.length) % files.length;
|
|
2018
|
+
render();
|
|
2019
|
+
}
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
2022
|
+
if (key === "\u001b[B") { // Down Arrow
|
|
2023
|
+
if (files.length > 0) {
|
|
2024
|
+
activeIndex = (activeIndex + 1) % files.length;
|
|
2025
|
+
render();
|
|
2026
|
+
}
|
|
2027
|
+
return;
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
if (key === " ") { // Stage/Unstage
|
|
2031
|
+
if (files.length > 0) {
|
|
2032
|
+
const file = files[activeIndex];
|
|
2033
|
+
try {
|
|
2034
|
+
if (file.staged) {
|
|
2035
|
+
execSync(`git restore --staged "${file.path}"`);
|
|
2036
|
+
} else {
|
|
2037
|
+
execSync(`git add "${file.path}"`);
|
|
2038
|
+
}
|
|
2039
|
+
} catch (err) {
|
|
2040
|
+
// Ignore
|
|
2041
|
+
}
|
|
2042
|
+
files = getGitStatusFiles();
|
|
2043
|
+
if (activeIndex >= files.length) {
|
|
2044
|
+
activeIndex = Math.max(0, files.length - 1);
|
|
2045
|
+
}
|
|
2046
|
+
render();
|
|
2047
|
+
}
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
if (key === "d" || key === "D") { // Discard
|
|
2052
|
+
if (files.length > 0) {
|
|
2053
|
+
const file = files[activeIndex];
|
|
2054
|
+
try {
|
|
2055
|
+
if (file.status[0] === "?" || file.status[1] === "?") {
|
|
2056
|
+
const fs = await import("node:fs");
|
|
2057
|
+
fs.rmSync(file.path, { force: true });
|
|
2058
|
+
} else {
|
|
2059
|
+
execSync(`git restore "${file.path}"`);
|
|
2060
|
+
}
|
|
2061
|
+
} catch (err) {
|
|
2062
|
+
// Ignore
|
|
2063
|
+
}
|
|
2064
|
+
files = getGitStatusFiles();
|
|
2065
|
+
if (activeIndex >= files.length) {
|
|
2066
|
+
activeIndex = Math.max(0, files.length - 1);
|
|
2067
|
+
}
|
|
2068
|
+
render();
|
|
2069
|
+
}
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
if (key === "c" || key === "C") { // Commit
|
|
2074
|
+
cleanup();
|
|
2075
|
+
|
|
2076
|
+
const hasStaged = files.some(f => f.staged);
|
|
2077
|
+
if (!hasStaged) {
|
|
2078
|
+
console.log("\n" + label.warning + " " + colors.warning("No staged changes to commit. Stage some files first!\n"));
|
|
2079
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
2080
|
+
|
|
2081
|
+
stdin.setRawMode(true);
|
|
2082
|
+
stdin.resume();
|
|
2083
|
+
stdout.write("\x1b[?25l");
|
|
2084
|
+
renderedLines = 0;
|
|
2085
|
+
files = getGitStatusFiles();
|
|
2086
|
+
render();
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
ctx.rl.pause();
|
|
2091
|
+
const commitMsg = await new Promise((resMsg) => {
|
|
2092
|
+
ctx.rl.question(
|
|
2093
|
+
colors.accent("\n💬 Enter commit message: "),
|
|
2094
|
+
resMsg
|
|
2095
|
+
);
|
|
2096
|
+
});
|
|
2097
|
+
ctx.rl.resume();
|
|
2098
|
+
|
|
2099
|
+
if (commitMsg.trim()) {
|
|
2100
|
+
try {
|
|
2101
|
+
execSync(`git commit -m "${commitMsg.trim()}"`);
|
|
2102
|
+
console.log("\n" + label.system + " " + colors.success("✓ Changes committed successfully!\n"));
|
|
2103
|
+
} catch (err) {
|
|
2104
|
+
console.log("\n" + label.error + " " + colors.danger("Failed to commit changes: " + err.message + "\n"));
|
|
2105
|
+
}
|
|
2106
|
+
} else {
|
|
2107
|
+
console.log("\n" + label.warning + " " + colors.muted("Commit aborted (empty message).\n"));
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
2111
|
+
|
|
2112
|
+
stdin.setRawMode(true);
|
|
2113
|
+
stdin.resume();
|
|
2114
|
+
stdout.write("\x1b[?25l");
|
|
2115
|
+
renderedLines = 0;
|
|
2116
|
+
files = getGitStatusFiles();
|
|
2117
|
+
activeIndex = 0;
|
|
2118
|
+
render();
|
|
2119
|
+
return;
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
if (key === "p" || key === "P") { // Push
|
|
2123
|
+
cleanup();
|
|
2124
|
+
console.log("\n" + label.system + " " + colors.brand("🚀 Pushing changes to remote branch..."));
|
|
2125
|
+
try {
|
|
2126
|
+
const out = execSync("git push", { encoding: "utf8" });
|
|
2127
|
+
console.log(colors.muted(out));
|
|
2128
|
+
console.log("\n" + label.system + " " + colors.success("✓ Push completed successfully!\n"));
|
|
2129
|
+
} catch (err) {
|
|
2130
|
+
console.log("\n" + label.error + " " + colors.danger("Failed to push changes: " + err.message + "\n"));
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
2134
|
+
|
|
2135
|
+
stdin.setRawMode(true);
|
|
2136
|
+
stdin.resume();
|
|
2137
|
+
stdout.write("\x1b[?25l");
|
|
2138
|
+
renderedLines = 0;
|
|
2139
|
+
files = getGitStatusFiles();
|
|
2140
|
+
render();
|
|
2141
|
+
return;
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
function cleanup() {
|
|
2146
|
+
stdin.removeListener("data", handleKey);
|
|
2147
|
+
stdin.setRawMode(wasRaw);
|
|
2148
|
+
stdin.pause();
|
|
2149
|
+
stdout.write("\x1b[?25h"); // Show cursor
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
stdin.on("data", handleKey);
|
|
2153
|
+
});
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
/**
|
|
2157
|
+
* Handles spawning and launching the local telemetry dashboard.
|
|
2158
|
+
*/
|
|
2159
|
+
export async function handleDashboardCommand(ctx) {
|
|
2160
|
+
const { startDashboardServer } = await import("./dashboard.js");
|
|
2161
|
+
const { exec } = await import("node:child_process");
|
|
2162
|
+
|
|
2163
|
+
try {
|
|
2164
|
+
const { port } = await startDashboardServer();
|
|
2165
|
+
console.log("\n" + label.system + " " + colors.brand("📊 AETHER WEB TELEMETRY DASHBOARD"));
|
|
2166
|
+
console.log(separator("─"));
|
|
2167
|
+
console.log(keyValue(" Status", colors.success("ONLINE")));
|
|
2168
|
+
console.log(keyValue(" Local URL", `http://localhost:${port}`));
|
|
2169
|
+
console.log("");
|
|
2170
|
+
console.log(" " + colors.muted("Launching browser companion automatically..."));
|
|
2171
|
+
|
|
2172
|
+
let startCmd = `start http://localhost:${port}`;
|
|
2173
|
+
if (process.platform === "darwin") {
|
|
2174
|
+
startCmd = `open http://localhost:${port}`;
|
|
2175
|
+
} else if (process.platform === "linux") {
|
|
2176
|
+
startCmd = `xdg-open http://localhost:${port}`;
|
|
2177
|
+
}
|
|
2178
|
+
exec(startCmd);
|
|
2179
|
+
console.log("\n" + label.system + " " + colors.success("✓ Dashboard launched. Press Ctrl+C in this session to stop dashboard at exit.\n"));
|
|
2180
|
+
} catch (err) {
|
|
2181
|
+
console.log("\n" + label.error + " " + colors.danger("Failed to start dashboard server: " + err.message + "\n"));
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
|