@krishivpb60/aether-ai-cli 1.1.5 → 1.1.7
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/package.json +1 -1
- package/src/agent.js +417 -0
- package/src/chat.js +413 -99
- package/src/cli.js +90 -1
- package/src/config.js +187 -10
- package/src/file-parser.js +48 -1
- package/src/git.js +41 -0
- package/src/ui/theme.js +218 -0
- package/test/agent.test.js +62 -0
- package/test/config.test.js +27 -0
- package/test/ux.test.js +12 -1
package/src/chat.js
CHANGED
|
@@ -21,9 +21,11 @@ import {
|
|
|
21
21
|
modeBadge,
|
|
22
22
|
clearStreamedText,
|
|
23
23
|
StreamFilter,
|
|
24
|
+
stripCodeFences,
|
|
24
25
|
getActiveTheme,
|
|
25
26
|
setTheme,
|
|
26
|
-
getThemesList
|
|
27
|
+
getThemesList,
|
|
28
|
+
interactiveMenu
|
|
27
29
|
} from "./ui/theme.js";
|
|
28
30
|
import { createSpinner } from "./ui/spinner.js";
|
|
29
31
|
import { showBanner } from "./ui/banner.js";
|
|
@@ -34,11 +36,15 @@ import {
|
|
|
34
36
|
loadHistory,
|
|
35
37
|
saveHistory,
|
|
36
38
|
clearHistory,
|
|
37
|
-
setConfigValue
|
|
39
|
+
setConfigValue,
|
|
40
|
+
listSessions,
|
|
41
|
+
switchSession,
|
|
42
|
+
startNewSession
|
|
38
43
|
} from "./config.js";
|
|
39
44
|
import { MODES, DEFAULT_MODE, getModeByName } from "./modes.js";
|
|
40
45
|
import { parseFile, formatContext } from "./file-parser.js";
|
|
41
46
|
import { runMainframeHack } from "./ai/fallback.js";
|
|
47
|
+
import { AGENT_INSTRUCTIONS } from "./agent.js";
|
|
42
48
|
|
|
43
49
|
// Configure marked dynamically for terminal output
|
|
44
50
|
const getMarked = () => new Marked(markedTerminal({
|
|
@@ -118,7 +124,8 @@ export async function startChat(options = {}) {
|
|
|
118
124
|
const builtIn = [
|
|
119
125
|
"/help", "/mode", "/modes", "/attach", "/files", "/clear",
|
|
120
126
|
"/providers", "/export", "/status", "/copy", "/exit", "/quit",
|
|
121
|
-
"/theme", "/themes", "/history-clear", "/game", "/abort", "/cmd", "/write"
|
|
127
|
+
"/theme", "/themes", "/history-clear", "/game", "/abort", "/cmd", "/write",
|
|
128
|
+
"/commit", "/run", "/history", "/autopilot"
|
|
122
129
|
];
|
|
123
130
|
const customCmds = aiConfig.CUSTOM_COMMANDS || {};
|
|
124
131
|
const commands = [...builtIn, ...Object.keys(customCmds)];
|
|
@@ -219,115 +226,151 @@ export async function startChat(options = {}) {
|
|
|
219
226
|
fullPrompt = `${contexts}\n\n${promptText}`;
|
|
220
227
|
}
|
|
221
228
|
|
|
222
|
-
//
|
|
223
|
-
const
|
|
224
|
-
let firstTokenTime = 0;
|
|
225
|
-
const spinner = createSpinner(
|
|
226
|
-
colors.muted(`Routing through mesh ${currentMode.label}...`)
|
|
227
|
-
);
|
|
228
|
-
spinner.start();
|
|
229
|
+
// Append AGENT_INSTRUCTIONS to mode systemPrompt
|
|
230
|
+
const systemPrompt = currentMode.systemPrompt + "\n" + AGENT_INSTRUCTIONS;
|
|
229
231
|
|
|
230
|
-
let
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
hasStartedStreaming = true;
|
|
236
|
-
firstTokenTime = Date.now();
|
|
237
|
-
spinner.stop();
|
|
238
|
-
}
|
|
239
|
-
filter.write(token);
|
|
240
|
-
streamedText += token;
|
|
241
|
-
};
|
|
232
|
+
let loopCount = 0;
|
|
233
|
+
const MAX_LOOPS = 5;
|
|
234
|
+
let currentQueryPrompt = fullPrompt;
|
|
235
|
+
let aiResponseText = "";
|
|
236
|
+
let lastResult = null;
|
|
242
237
|
|
|
243
238
|
try {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
// Store in history
|
|
249
|
-
history.push({ role: "user", content: originalInput, timestamp: new Date() });
|
|
250
|
-
history.push({
|
|
251
|
-
role: "assistant",
|
|
252
|
-
content: result.text,
|
|
253
|
-
provider: result.provider,
|
|
254
|
-
model: result.model,
|
|
255
|
-
node: result.node,
|
|
256
|
-
timestamp: new Date(),
|
|
257
|
-
});
|
|
239
|
+
while (loopCount < MAX_LOOPS) {
|
|
240
|
+
const queryStartTime = Date.now();
|
|
241
|
+
let firstTokenTime = 0;
|
|
258
242
|
|
|
259
|
-
|
|
260
|
-
|
|
243
|
+
if (loopCount > 0) {
|
|
244
|
+
console.log(colors.accent(`\n🤖 [Aether Autopilot Mode - Iteration ${loopCount + 1}/${MAX_LOOPS}]`));
|
|
245
|
+
}
|
|
261
246
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
247
|
+
const spinner = createSpinner(
|
|
248
|
+
colors.muted(loopCount === 0 ? `Routing through mesh ${currentMode.label}...` : `Agent executing tasks...`)
|
|
249
|
+
);
|
|
250
|
+
spinner.start();
|
|
251
|
+
|
|
252
|
+
let hasStartedStreaming = false;
|
|
253
|
+
let streamedText = "";
|
|
254
|
+
const filter = new StreamFilter(process.stdout.write.bind(process.stdout));
|
|
255
|
+
const onToken = (token) => {
|
|
256
|
+
if (!hasStartedStreaming) {
|
|
257
|
+
hasStartedStreaming = true;
|
|
258
|
+
firstTokenTime = Date.now();
|
|
259
|
+
spinner.stop();
|
|
260
|
+
}
|
|
261
|
+
filter.write(token);
|
|
262
|
+
streamedText += token;
|
|
263
|
+
};
|
|
265
264
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
console.log(separator("─"));
|
|
270
|
-
console.log("");
|
|
265
|
+
const result = await routePrompt(currentQueryPrompt, systemPrompt, aiConfig, onToken, history);
|
|
266
|
+
spinner.stop();
|
|
267
|
+
filter.flush();
|
|
271
268
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
} else {
|
|
275
|
-
let displayText = result.text;
|
|
276
|
-
const cleanedText = displayText.replace(/\[WRITE_FILE:\s*([^\n\]]+)\][\s\S]*?\[END_WRITE\]/g, (match, p1) => {
|
|
277
|
-
return `\n\n${colors.brand("⚡ [File creation request: " + p1 + "]")}\n\n`;
|
|
278
|
-
});
|
|
279
|
-
const rendered = getMarked().parse(cleanedText);
|
|
280
|
-
console.log(rendered);
|
|
281
|
-
}
|
|
269
|
+
aiResponseText = result.text;
|
|
270
|
+
lastResult = result;
|
|
282
271
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
if (firstTokenTime > 0) {
|
|
286
|
-
const streamElapsed = (Date.now() - firstTokenTime) / 1000;
|
|
287
|
-
if (streamElapsed > 0.05) {
|
|
288
|
-
const estimatedTokens = Math.max(1, Math.round(streamedText.length / 4));
|
|
289
|
-
const tps = (estimatedTokens / streamElapsed).toFixed(1);
|
|
290
|
-
speedText = ` • ${tps} tok/s`;
|
|
272
|
+
if (hasStartedStreaming) {
|
|
273
|
+
clearStreamedText(filter.filteredText);
|
|
291
274
|
}
|
|
292
|
-
}
|
|
293
275
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
"
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
276
|
+
// Display response
|
|
277
|
+
console.log("");
|
|
278
|
+
console.log(label.aether + " " + providerBadge(result));
|
|
279
|
+
console.log(separator("─"));
|
|
280
|
+
console.log("");
|
|
281
|
+
|
|
282
|
+
if (result.provider === "local" || result.provider === "krylo-fallback") {
|
|
283
|
+
console.log(colors.text(" " + result.text.split("\n").join("\n ")));
|
|
284
|
+
} else {
|
|
285
|
+
let displayText = result.text;
|
|
286
|
+
const rendered = getMarked().parse(displayText);
|
|
287
|
+
console.log(rendered);
|
|
288
|
+
}
|
|
302
289
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
290
|
+
const elapsedSec = ((Date.now() - queryStartTime) / 1000).toFixed(1);
|
|
291
|
+
let speedText = "";
|
|
292
|
+
if (firstTokenTime > 0) {
|
|
293
|
+
const streamElapsed = (Date.now() - firstTokenTime) / 1000;
|
|
294
|
+
if (streamElapsed > 0.05) {
|
|
295
|
+
const estimatedTokens = Math.max(1, Math.round(streamedText.length / 4));
|
|
296
|
+
const tps = (estimatedTokens / streamElapsed).toFixed(1);
|
|
297
|
+
speedText = ` • ${tps} tok/s`;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
310
300
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
301
|
+
console.log(separator("─"));
|
|
302
|
+
console.log(
|
|
303
|
+
" " + colors.dim(`Node ${result.node} • ${result.provider}`) +
|
|
304
|
+
(result.model ? colors.dim(` • ${result.model}`) : "") +
|
|
305
|
+
colors.dim(` • ${elapsedSec}s${speedText}`) +
|
|
306
|
+
colors.dim(` • ${Math.floor(history.length / 2)} exchanges`)
|
|
307
|
+
);
|
|
308
|
+
console.log("");
|
|
309
|
+
|
|
310
|
+
// Process any agent tools output by the AI
|
|
311
|
+
const { processAgentBlocks } = await import("./agent.js");
|
|
312
|
+
const toolResults = await processAgentBlocks(aiResponseText, aiConfig, rl);
|
|
313
|
+
|
|
314
|
+
if (toolResults.length === 0) {
|
|
315
|
+
// No tools executed, end loop
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Store this turn in history so AI knows what happened
|
|
320
|
+
history.push({ role: "user", content: currentQueryPrompt, timestamp: new Date() });
|
|
321
|
+
history.push({
|
|
322
|
+
role: "assistant",
|
|
323
|
+
content: aiResponseText,
|
|
324
|
+
provider: result.provider,
|
|
325
|
+
model: result.model,
|
|
326
|
+
node: result.node,
|
|
327
|
+
timestamp: new Date(),
|
|
328
|
+
});
|
|
329
|
+
await saveHistory(history, currentMode.name);
|
|
330
|
+
|
|
331
|
+
// Format tool outputs as next prompt
|
|
332
|
+
let formattedResults = "### Agent Tool Outputs:\n";
|
|
333
|
+
for (const tr of toolResults) {
|
|
334
|
+
if (tr.success) {
|
|
335
|
+
if (tr.tool === "RUN_COMMAND") {
|
|
336
|
+
formattedResults += `\n- RUN_COMMAND "${tr.arg}" succeeded. Output:\n\`\`\`\n${tr.stdout || ""}${tr.stderr || ""}\n\`\`\`;`;
|
|
337
|
+
} else if (tr.tool === "READ_FILE") {
|
|
338
|
+
formattedResults += `\n- READ_FILE "${tr.arg}" succeeded. File Content:\n\`\`\`\n${tr.content}\n\`\`\`;`;
|
|
339
|
+
} else if (tr.tool === "WRITE_FILE") {
|
|
340
|
+
formattedResults += `\n- WRITE_FILE "${tr.arg}" succeeded.`;
|
|
341
|
+
} else if (tr.tool === "SEARCH_WEB") {
|
|
342
|
+
const resultsList = tr.results.map((r, i) => `${i+1}. [${r.title}](${r.url})\n ${r.snippet}`).join("\n");
|
|
343
|
+
formattedResults += `\n- SEARCH_WEB "${tr.arg}" succeeded. Results:\n${resultsList}`;
|
|
344
|
+
}
|
|
345
|
+
} else {
|
|
346
|
+
formattedResults += `\n- ${tr.tool} "${tr.arg}" failed: ${tr.error}`;
|
|
326
347
|
}
|
|
327
348
|
}
|
|
349
|
+
formattedResults += "\n\nPlease continue and finalize your task or perform next steps.";
|
|
350
|
+
|
|
351
|
+
currentQueryPrompt = formattedResults;
|
|
352
|
+
loopCount++;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Store final state in history
|
|
356
|
+
if (loopCount > 0) {
|
|
357
|
+
// Just save to disk to persist
|
|
358
|
+
await saveHistory(history, currentMode.name);
|
|
359
|
+
} else {
|
|
360
|
+
// Standard single-turn save
|
|
361
|
+
history.push({ role: "user", content: originalInput, timestamp: new Date() });
|
|
362
|
+
history.push({
|
|
363
|
+
role: "assistant",
|
|
364
|
+
content: aiResponseText,
|
|
365
|
+
provider: lastResult.provider,
|
|
366
|
+
model: lastResult.model,
|
|
367
|
+
node: lastResult.node,
|
|
368
|
+
timestamp: new Date(),
|
|
369
|
+
});
|
|
370
|
+
await saveHistory(history, currentMode.name);
|
|
328
371
|
}
|
|
372
|
+
|
|
329
373
|
} catch (err) {
|
|
330
|
-
spinner.fail("Request failed");
|
|
331
374
|
console.log("\n" + label.error + " " + colors.danger(err.message) + "\n");
|
|
332
375
|
}
|
|
333
376
|
|
|
@@ -361,7 +404,7 @@ export async function startChat(options = {}) {
|
|
|
361
404
|
"/", "/help", "/mode", "/modes", "/attach", "/files", "/clear",
|
|
362
405
|
"/providers", "/export", "/status", "/copy", "/exit", "/quit",
|
|
363
406
|
"/theme", "/themes", "/history-clear", "/game", "/abort", "/cmd",
|
|
364
|
-
"/guess", "/write"
|
|
407
|
+
"/guess", "/write", "/commit", "/run", "/history", "/autopilot"
|
|
365
408
|
];
|
|
366
409
|
|
|
367
410
|
const customCmds = aiConfig.CUSTOM_COMMANDS || {};
|
|
@@ -492,6 +535,22 @@ async function handleCommand(input, ctx) {
|
|
|
492
535
|
await handleWriteFile(args, ctx);
|
|
493
536
|
break;
|
|
494
537
|
|
|
538
|
+
case "/commit":
|
|
539
|
+
await handleCommitInsideChat(ctx);
|
|
540
|
+
break;
|
|
541
|
+
|
|
542
|
+
case "/run":
|
|
543
|
+
await handleRunCommand(args, ctx);
|
|
544
|
+
break;
|
|
545
|
+
|
|
546
|
+
case "/history":
|
|
547
|
+
await handleHistorySwitch(ctx);
|
|
548
|
+
break;
|
|
549
|
+
|
|
550
|
+
case "/autopilot":
|
|
551
|
+
await handleAutopilotSwitch(args, ctx);
|
|
552
|
+
break;
|
|
553
|
+
|
|
495
554
|
case "/exit":
|
|
496
555
|
case "/quit":
|
|
497
556
|
ctx.rl.close();
|
|
@@ -520,11 +579,15 @@ function showHelp(aiConfig) {
|
|
|
520
579
|
console.log(keyValue("/clear", "Clear terminal screen and reprint banner"));
|
|
521
580
|
console.log(keyValue("/providers", "Show active AI providers"));
|
|
522
581
|
console.log(keyValue("/export", "Export conversation to file"));
|
|
582
|
+
console.log(keyValue("/history", "List, switch, and resume past interactive chat sessions"));
|
|
523
583
|
console.log(keyValue("/history-clear", "Clear saved persistent chat history"));
|
|
584
|
+
console.log(keyValue("/autopilot <mode>", "View or switch agent autopilot level (off, safe, workspace, machine)"));
|
|
524
585
|
console.log(keyValue("/game", "Start the local mainframe hacking mini-game"));
|
|
525
586
|
console.log(keyValue("/copy", "Copy the last assistant response to clipboard"));
|
|
526
587
|
console.log(keyValue("/cmd <list|add|remove>", "Manage custom command shortcuts"));
|
|
527
588
|
console.log(keyValue("/write <filename>", "Extract last code block and save to file"));
|
|
589
|
+
console.log(keyValue("/commit", "Generate conventional commit message and commit changes"));
|
|
590
|
+
console.log(keyValue("/run <command>", "Execute a shell command interactively"));
|
|
528
591
|
console.log(keyValue("/exit", "End session"));
|
|
529
592
|
|
|
530
593
|
if (aiConfig && aiConfig.CUSTOM_COMMANDS) {
|
|
@@ -583,9 +646,48 @@ function showModes() {
|
|
|
583
646
|
}
|
|
584
647
|
|
|
585
648
|
async function handleAttach(args, ctx) {
|
|
586
|
-
const filePath = args.join(" ");
|
|
649
|
+
const filePath = args.join(" ").trim();
|
|
587
650
|
if (!filePath) {
|
|
588
|
-
|
|
651
|
+
const { scanWorkspaceFiles } = await import("./file-parser.js");
|
|
652
|
+
const { interactiveCheckbox } = await import("./ui/theme.js");
|
|
653
|
+
|
|
654
|
+
const workspaceFiles = scanWorkspaceFiles(process.cwd());
|
|
655
|
+
if (workspaceFiles.length === 0) {
|
|
656
|
+
console.log("\n" + label.file + " " + colors.muted("No supported files found in this workspace.\n"));
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
ctx.rl.pause();
|
|
661
|
+
const selected = await interactiveCheckbox(
|
|
662
|
+
"Attach files (Arrow Keys to navigate, Space to toggle, Enter to confirm, Esc/q to cancel):\n",
|
|
663
|
+
workspaceFiles,
|
|
664
|
+
ctx.attachedFiles.map(f => f.relativePath || f.name)
|
|
665
|
+
);
|
|
666
|
+
ctx.rl.resume();
|
|
667
|
+
|
|
668
|
+
if (selected === null) {
|
|
669
|
+
console.log("\n" + label.file + " " + colors.muted("Selection canceled.\n"));
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
ctx.clearFiles();
|
|
674
|
+
if (selected.length === 0) {
|
|
675
|
+
console.log("\n" + label.file + " " + colors.success("Cleared all attachments.\n"));
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
let successCount = 0;
|
|
680
|
+
for (const file of selected) {
|
|
681
|
+
try {
|
|
682
|
+
const fileData = await parseFile(file);
|
|
683
|
+
fileData.relativePath = file;
|
|
684
|
+
ctx.addFile(fileData);
|
|
685
|
+
successCount++;
|
|
686
|
+
} catch (err) {
|
|
687
|
+
console.log(label.error + " " + colors.danger(`Failed to attach ${file}: ${err.message}`));
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
console.log("\n" + label.file + " " + colors.success(`Successfully attached ${successCount} file(s).\n`));
|
|
589
691
|
return;
|
|
590
692
|
}
|
|
591
693
|
|
|
@@ -712,6 +814,100 @@ async function handleHistoryClear(history, rl) {
|
|
|
712
814
|
console.log("\n" + label.system + " " + colors.success("✓ Persistent chat history and prompt history cleared.\n"));
|
|
713
815
|
}
|
|
714
816
|
|
|
817
|
+
async function handleAutopilotSwitch(args, ctx) {
|
|
818
|
+
const setting = args[0]?.toLowerCase().trim();
|
|
819
|
+
if (!setting) {
|
|
820
|
+
const current = (ctx.aiConfig.AUTOPILOT || "off").toUpperCase();
|
|
821
|
+
console.log("\n" + label.system + " " + colors.brand("🤖 AUTOPILOT AGENT CONFIGURATION"));
|
|
822
|
+
console.log(separator("─"));
|
|
823
|
+
console.log(keyValue(" Current Setting", current));
|
|
824
|
+
console.log("");
|
|
825
|
+
console.log(" " + colors.muted("Available Modes:"));
|
|
826
|
+
console.log(" • " + colors.accent("off") + colors.text(" - Always ask user for confirmation before executing any actions."));
|
|
827
|
+
console.log(" • " + colors.accent("safe") + colors.text(" - Run read-only/safe terminal commands and searches automatically."));
|
|
828
|
+
console.log(" • " + colors.accent("workspace") + colors.text(" - Run any actions automatically if they stay inside the workspace."));
|
|
829
|
+
console.log(" • " + colors.accent("machine") + colors.text(" - Complete autopilot. Run any action automatically (Full access)."));
|
|
830
|
+
console.log("");
|
|
831
|
+
console.log(" " + colors.muted("To change setting: ") + colors.accent("/autopilot <mode>") + "\n");
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const valid = ["off", "safe", "workspace", "machine"];
|
|
836
|
+
if (!valid.includes(setting)) {
|
|
837
|
+
console.log("\n" + label.system + " " + colors.danger(`ERROR: Unknown autopilot mode "${setting}".`) + " " + colors.muted("Choose from: off, safe, workspace, machine.\n"));
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
await setConfigValue("AUTOPILOT", setting);
|
|
842
|
+
ctx.aiConfig.AUTOPILOT = setting;
|
|
843
|
+
console.log("\n" + label.system + " " + colors.success(`✓ Autopilot setting updated to ${setting.toUpperCase()} successfully.\n`));
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
async function handleHistorySwitch(ctx) {
|
|
847
|
+
const sessions = listSessions();
|
|
848
|
+
if (sessions.length === 0) {
|
|
849
|
+
console.log("\n" + label.system + " " + colors.muted("No past chat sessions found.\n"));
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const items = sessions.map((s) => {
|
|
854
|
+
const dateStr = new Date(s.timestamp).toLocaleString();
|
|
855
|
+
const count = s.messages.length;
|
|
856
|
+
const exchanges = Math.floor(count / 2);
|
|
857
|
+
// Find first user query preview
|
|
858
|
+
const firstQuery = s.messages.find((m) => m.role === "user")?.content || "Empty conversation";
|
|
859
|
+
const preview = firstQuery.length > 50 ? firstQuery.slice(0, 47) + "..." : firstQuery;
|
|
860
|
+
const modeBadgeText = `[${s.mode}]`;
|
|
861
|
+
return `${colors.dim(dateStr)} ${colors.brand(modeBadgeText.padEnd(12))} ${colors.muted(exchanges + " exch")} • ${colors.text(preview)}`;
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
// Add an option to start a new session
|
|
865
|
+
items.push(colors.accent("➕ Start a new chat session"));
|
|
866
|
+
|
|
867
|
+
ctx.rl.pause();
|
|
868
|
+
const selectedIndex = await interactiveMenu(
|
|
869
|
+
"Select a past chat session to resume (Arrow Keys to navigate, Enter to select, Esc/q to cancel):\n",
|
|
870
|
+
items
|
|
871
|
+
);
|
|
872
|
+
ctx.rl.resume();
|
|
873
|
+
|
|
874
|
+
if (selectedIndex === null) {
|
|
875
|
+
console.log("\n" + label.system + " " + colors.muted("Selection canceled.\n"));
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Clear screen and load the selected session
|
|
880
|
+
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
881
|
+
|
|
882
|
+
if (selectedIndex === sessions.length) {
|
|
883
|
+
// Start new session
|
|
884
|
+
const newSessionFile = startNewSession();
|
|
885
|
+
ctx.history.length = 0;
|
|
886
|
+
showBanner(ctx.currentMode.name);
|
|
887
|
+
console.log("\n" + label.system + " " + colors.success("Started a new chat session.\n"));
|
|
888
|
+
} else {
|
|
889
|
+
const selectedSession = sessions[selectedIndex];
|
|
890
|
+
switchSession(selectedSession.file);
|
|
891
|
+
|
|
892
|
+
// Load history
|
|
893
|
+
const loadedHistory = await loadHistory();
|
|
894
|
+
ctx.history.length = 0;
|
|
895
|
+
for (const msg of loadedHistory) {
|
|
896
|
+
ctx.history.push(msg);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
showBanner(ctx.currentMode.name);
|
|
900
|
+
console.log("\n" + label.system + " " + colors.success(`✓ Switched to chat session from ${new Date(selectedSession.timestamp).toLocaleString()}`));
|
|
901
|
+
console.log(" " + colors.muted(`Restored ${Math.floor(ctx.history.length / 2)} message exchanges.\n`));
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Sync shell's recall history list
|
|
905
|
+
const userQueries = ctx.history
|
|
906
|
+
.filter((h) => h.role === "user")
|
|
907
|
+
.map((h) => h.content);
|
|
908
|
+
ctx.rl.history = [...new Set(userQueries)].reverse();
|
|
909
|
+
}
|
|
910
|
+
|
|
715
911
|
function handleGameStart(game) {
|
|
716
912
|
if (game.active) {
|
|
717
913
|
console.log("\n" + label.system + " " + colors.warning("Mainframe breach is already in progress. Type /abort to cancel.\n"));
|
|
@@ -1019,3 +1215,121 @@ async function handleWriteFile(args, ctx) {
|
|
|
1019
1215
|
console.log("\n" + label.error + " " + colors.danger(`Write failed: ${err.message}\n`));
|
|
1020
1216
|
}
|
|
1021
1217
|
}
|
|
1218
|
+
|
|
1219
|
+
/**
|
|
1220
|
+
* Interactive git commit command inside chat loop.
|
|
1221
|
+
*/
|
|
1222
|
+
async function handleCommitInsideChat(ctx) {
|
|
1223
|
+
const { getGitDiff, runGitCommit } = await import("./git.js");
|
|
1224
|
+
const { exec } = await import("node:child_process");
|
|
1225
|
+
const { promisify } = await import("node:util");
|
|
1226
|
+
const execAsync = promisify(exec);
|
|
1227
|
+
|
|
1228
|
+
try {
|
|
1229
|
+
const { diff, isStaged } = await getGitDiff();
|
|
1230
|
+
if (!diff) {
|
|
1231
|
+
console.log("\n" + label.system + " " + colors.warning("No staged or unstaged changes detected. Stage your files using 'git add' first.\n"));
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
if (!isStaged) {
|
|
1236
|
+
ctx.rl.pause();
|
|
1237
|
+
const stageAnswer = await new Promise((resolve) => {
|
|
1238
|
+
ctx.rl.question(colors.warning("\nNo staged changes found. Do you want to stage all changes automatically? [y/N]: "), resolve);
|
|
1239
|
+
});
|
|
1240
|
+
ctx.rl.resume();
|
|
1241
|
+
|
|
1242
|
+
if (stageAnswer.toLowerCase().trim() === "y" || stageAnswer.toLowerCase().trim() === "yes") {
|
|
1243
|
+
await execAsync("git add .");
|
|
1244
|
+
console.log(label.system + " " + colors.success("Staged all changes successfully."));
|
|
1245
|
+
} else {
|
|
1246
|
+
console.log("\n" + label.system + " " + colors.muted("Aborted. Please stage files using 'git add' first.\n"));
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
console.log("\n" + label.system + " " + colors.brand("Reading git diff and generating conventional commit message..."));
|
|
1252
|
+
console.log("");
|
|
1253
|
+
|
|
1254
|
+
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.";
|
|
1255
|
+
const userPrompt = `Here is the git diff:\n\n${diff}`;
|
|
1256
|
+
|
|
1257
|
+
let firstToken = true;
|
|
1258
|
+
let commitMessage = "";
|
|
1259
|
+
const onToken = (token) => {
|
|
1260
|
+
if (firstToken) {
|
|
1261
|
+
firstToken = false;
|
|
1262
|
+
process.stdout.write(label.aether + " Suggested Commit Message: " + colors.success(token));
|
|
1263
|
+
} else {
|
|
1264
|
+
process.stdout.write(colors.success(token));
|
|
1265
|
+
}
|
|
1266
|
+
commitMessage += token;
|
|
1267
|
+
};
|
|
1268
|
+
|
|
1269
|
+
const result = await routePrompt(userPrompt, systemPrompt, ctx.aiConfig, onToken);
|
|
1270
|
+
console.log("\n");
|
|
1271
|
+
|
|
1272
|
+
const cleanMessage = result.text.trim().replace(/^`+|`+$/g, ""); // strip quotes/backticks
|
|
1273
|
+
|
|
1274
|
+
ctx.rl.pause();
|
|
1275
|
+
const answer = await new Promise((resolve) => {
|
|
1276
|
+
ctx.rl.question(colors.muted("Commit with this message? [Y/n]: "), resolve);
|
|
1277
|
+
});
|
|
1278
|
+
ctx.rl.resume();
|
|
1279
|
+
|
|
1280
|
+
if (answer.toLowerCase().trim() === "n" || answer.toLowerCase().trim() === "no") {
|
|
1281
|
+
console.log("\n" + label.system + " " + colors.muted("Commit aborted.\n"));
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
console.log("\n" + label.system + " " + colors.brand("Executing git commit..."));
|
|
1286
|
+
const output = await runGitCommit(cleanMessage);
|
|
1287
|
+
console.log("\n" + colors.success(output) + "\n");
|
|
1288
|
+
|
|
1289
|
+
} catch (err) {
|
|
1290
|
+
console.log("\n" + label.error + " " + colors.danger(err.message) + "\n");
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/**
|
|
1295
|
+
* Sandboxed interactive shell command execution.
|
|
1296
|
+
*/
|
|
1297
|
+
async function handleRunCommand(args, ctx) {
|
|
1298
|
+
const command = args.join(" ").trim();
|
|
1299
|
+
if (!command) {
|
|
1300
|
+
console.log("\n" + label.system + " " + colors.warning("Usage: /run <command>\n"));
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
const { spawn } = await import("node:child_process");
|
|
1305
|
+
|
|
1306
|
+
console.log("\n" + label.system + " " + colors.brand(`Running command: ${command}`));
|
|
1307
|
+
console.log(separator("─") + "\n");
|
|
1308
|
+
|
|
1309
|
+
ctx.rl.pause();
|
|
1310
|
+
|
|
1311
|
+
return new Promise((resolve) => {
|
|
1312
|
+
const isWindows = process.platform === "win32";
|
|
1313
|
+
const shell = isWindows ? "cmd.exe" : "/bin/sh";
|
|
1314
|
+
const shellArgs = isWindows ? ["/c", command] : ["-c", command];
|
|
1315
|
+
|
|
1316
|
+
const child = spawn(shell, shellArgs, { stdio: "inherit" });
|
|
1317
|
+
|
|
1318
|
+
child.on("close", (code) => {
|
|
1319
|
+
ctx.rl.resume();
|
|
1320
|
+
console.log("\n" + separator("─"));
|
|
1321
|
+
if (code === 0) {
|
|
1322
|
+
console.log(label.system + " " + colors.success(`✓ Command exited successfully (code 0).\n`));
|
|
1323
|
+
} else {
|
|
1324
|
+
console.log(label.system + " " + colors.danger(`✗ Command failed with exit status ${code}.\n`));
|
|
1325
|
+
}
|
|
1326
|
+
resolve();
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
child.on("error", (err) => {
|
|
1330
|
+
ctx.rl.resume();
|
|
1331
|
+
console.log("\n" + label.error + " " + colors.danger(`Failed to start command: ${err.message}\n`));
|
|
1332
|
+
resolve();
|
|
1333
|
+
});
|
|
1334
|
+
});
|
|
1335
|
+
}
|