@krishivpb60/aether-ai-cli 1.1.6 → 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 +412 -99
- package/src/cli.js +88 -0
- package/src/config.js +187 -10
- package/src/file-parser.js +48 -1
- package/src/git.js +41 -0
- package/src/ui/theme.js +202 -5
- package/test/agent.test.js +62 -0
- package/test/config.test.js +27 -0
package/src/chat.js
CHANGED
|
@@ -24,7 +24,8 @@ import {
|
|
|
24
24
|
stripCodeFences,
|
|
25
25
|
getActiveTheme,
|
|
26
26
|
setTheme,
|
|
27
|
-
getThemesList
|
|
27
|
+
getThemesList,
|
|
28
|
+
interactiveMenu
|
|
28
29
|
} from "./ui/theme.js";
|
|
29
30
|
import { createSpinner } from "./ui/spinner.js";
|
|
30
31
|
import { showBanner } from "./ui/banner.js";
|
|
@@ -35,11 +36,15 @@ import {
|
|
|
35
36
|
loadHistory,
|
|
36
37
|
saveHistory,
|
|
37
38
|
clearHistory,
|
|
38
|
-
setConfigValue
|
|
39
|
+
setConfigValue,
|
|
40
|
+
listSessions,
|
|
41
|
+
switchSession,
|
|
42
|
+
startNewSession
|
|
39
43
|
} from "./config.js";
|
|
40
44
|
import { MODES, DEFAULT_MODE, getModeByName } from "./modes.js";
|
|
41
45
|
import { parseFile, formatContext } from "./file-parser.js";
|
|
42
46
|
import { runMainframeHack } from "./ai/fallback.js";
|
|
47
|
+
import { AGENT_INSTRUCTIONS } from "./agent.js";
|
|
43
48
|
|
|
44
49
|
// Configure marked dynamically for terminal output
|
|
45
50
|
const getMarked = () => new Marked(markedTerminal({
|
|
@@ -119,7 +124,8 @@ export async function startChat(options = {}) {
|
|
|
119
124
|
const builtIn = [
|
|
120
125
|
"/help", "/mode", "/modes", "/attach", "/files", "/clear",
|
|
121
126
|
"/providers", "/export", "/status", "/copy", "/exit", "/quit",
|
|
122
|
-
"/theme", "/themes", "/history-clear", "/game", "/abort", "/cmd", "/write"
|
|
127
|
+
"/theme", "/themes", "/history-clear", "/game", "/abort", "/cmd", "/write",
|
|
128
|
+
"/commit", "/run", "/history", "/autopilot"
|
|
123
129
|
];
|
|
124
130
|
const customCmds = aiConfig.CUSTOM_COMMANDS || {};
|
|
125
131
|
const commands = [...builtIn, ...Object.keys(customCmds)];
|
|
@@ -220,115 +226,151 @@ export async function startChat(options = {}) {
|
|
|
220
226
|
fullPrompt = `${contexts}\n\n${promptText}`;
|
|
221
227
|
}
|
|
222
228
|
|
|
223
|
-
//
|
|
224
|
-
const
|
|
225
|
-
let firstTokenTime = 0;
|
|
226
|
-
const spinner = createSpinner(
|
|
227
|
-
colors.muted(`Routing through mesh ${currentMode.label}...`)
|
|
228
|
-
);
|
|
229
|
-
spinner.start();
|
|
229
|
+
// Append AGENT_INSTRUCTIONS to mode systemPrompt
|
|
230
|
+
const systemPrompt = currentMode.systemPrompt + "\n" + AGENT_INSTRUCTIONS;
|
|
230
231
|
|
|
231
|
-
let
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
hasStartedStreaming = true;
|
|
237
|
-
firstTokenTime = Date.now();
|
|
238
|
-
spinner.stop();
|
|
239
|
-
}
|
|
240
|
-
filter.write(token);
|
|
241
|
-
streamedText += token;
|
|
242
|
-
};
|
|
232
|
+
let loopCount = 0;
|
|
233
|
+
const MAX_LOOPS = 5;
|
|
234
|
+
let currentQueryPrompt = fullPrompt;
|
|
235
|
+
let aiResponseText = "";
|
|
236
|
+
let lastResult = null;
|
|
243
237
|
|
|
244
238
|
try {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
// Store in history
|
|
250
|
-
history.push({ role: "user", content: originalInput, timestamp: new Date() });
|
|
251
|
-
history.push({
|
|
252
|
-
role: "assistant",
|
|
253
|
-
content: result.text,
|
|
254
|
-
provider: result.provider,
|
|
255
|
-
model: result.model,
|
|
256
|
-
node: result.node,
|
|
257
|
-
timestamp: new Date(),
|
|
258
|
-
});
|
|
239
|
+
while (loopCount < MAX_LOOPS) {
|
|
240
|
+
const queryStartTime = Date.now();
|
|
241
|
+
let firstTokenTime = 0;
|
|
259
242
|
|
|
260
|
-
|
|
261
|
-
|
|
243
|
+
if (loopCount > 0) {
|
|
244
|
+
console.log(colors.accent(`\n🤖 [Aether Autopilot Mode - Iteration ${loopCount + 1}/${MAX_LOOPS}]`));
|
|
245
|
+
}
|
|
262
246
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
+
};
|
|
266
264
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
console.log(separator("─"));
|
|
271
|
-
console.log("");
|
|
265
|
+
const result = await routePrompt(currentQueryPrompt, systemPrompt, aiConfig, onToken, history);
|
|
266
|
+
spinner.stop();
|
|
267
|
+
filter.flush();
|
|
272
268
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
} else {
|
|
276
|
-
let displayText = result.text;
|
|
277
|
-
const cleanedText = displayText.replace(/\[WRITE_FILE:\s*([^\n\]]+)\][\s\S]*?\[END_WRITE\]/g, (match, p1) => {
|
|
278
|
-
return `\n\n${colors.brand("⚡ [File creation request: " + p1 + "]")}\n\n`;
|
|
279
|
-
});
|
|
280
|
-
const rendered = getMarked().parse(cleanedText);
|
|
281
|
-
console.log(rendered);
|
|
282
|
-
}
|
|
269
|
+
aiResponseText = result.text;
|
|
270
|
+
lastResult = result;
|
|
283
271
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if (firstTokenTime > 0) {
|
|
287
|
-
const streamElapsed = (Date.now() - firstTokenTime) / 1000;
|
|
288
|
-
if (streamElapsed > 0.05) {
|
|
289
|
-
const estimatedTokens = Math.max(1, Math.round(streamedText.length / 4));
|
|
290
|
-
const tps = (estimatedTokens / streamElapsed).toFixed(1);
|
|
291
|
-
speedText = ` • ${tps} tok/s`;
|
|
272
|
+
if (hasStartedStreaming) {
|
|
273
|
+
clearStreamedText(filter.filteredText);
|
|
292
274
|
}
|
|
293
|
-
}
|
|
294
275
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
"
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
+
}
|
|
303
289
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
+
}
|
|
311
300
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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}`;
|
|
327
347
|
}
|
|
328
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);
|
|
329
371
|
}
|
|
372
|
+
|
|
330
373
|
} catch (err) {
|
|
331
|
-
spinner.fail("Request failed");
|
|
332
374
|
console.log("\n" + label.error + " " + colors.danger(err.message) + "\n");
|
|
333
375
|
}
|
|
334
376
|
|
|
@@ -362,7 +404,7 @@ export async function startChat(options = {}) {
|
|
|
362
404
|
"/", "/help", "/mode", "/modes", "/attach", "/files", "/clear",
|
|
363
405
|
"/providers", "/export", "/status", "/copy", "/exit", "/quit",
|
|
364
406
|
"/theme", "/themes", "/history-clear", "/game", "/abort", "/cmd",
|
|
365
|
-
"/guess", "/write"
|
|
407
|
+
"/guess", "/write", "/commit", "/run", "/history", "/autopilot"
|
|
366
408
|
];
|
|
367
409
|
|
|
368
410
|
const customCmds = aiConfig.CUSTOM_COMMANDS || {};
|
|
@@ -493,6 +535,22 @@ async function handleCommand(input, ctx) {
|
|
|
493
535
|
await handleWriteFile(args, ctx);
|
|
494
536
|
break;
|
|
495
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
|
+
|
|
496
554
|
case "/exit":
|
|
497
555
|
case "/quit":
|
|
498
556
|
ctx.rl.close();
|
|
@@ -521,11 +579,15 @@ function showHelp(aiConfig) {
|
|
|
521
579
|
console.log(keyValue("/clear", "Clear terminal screen and reprint banner"));
|
|
522
580
|
console.log(keyValue("/providers", "Show active AI providers"));
|
|
523
581
|
console.log(keyValue("/export", "Export conversation to file"));
|
|
582
|
+
console.log(keyValue("/history", "List, switch, and resume past interactive chat sessions"));
|
|
524
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)"));
|
|
525
585
|
console.log(keyValue("/game", "Start the local mainframe hacking mini-game"));
|
|
526
586
|
console.log(keyValue("/copy", "Copy the last assistant response to clipboard"));
|
|
527
587
|
console.log(keyValue("/cmd <list|add|remove>", "Manage custom command shortcuts"));
|
|
528
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"));
|
|
529
591
|
console.log(keyValue("/exit", "End session"));
|
|
530
592
|
|
|
531
593
|
if (aiConfig && aiConfig.CUSTOM_COMMANDS) {
|
|
@@ -584,9 +646,48 @@ function showModes() {
|
|
|
584
646
|
}
|
|
585
647
|
|
|
586
648
|
async function handleAttach(args, ctx) {
|
|
587
|
-
const filePath = args.join(" ");
|
|
649
|
+
const filePath = args.join(" ").trim();
|
|
588
650
|
if (!filePath) {
|
|
589
|
-
|
|
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`));
|
|
590
691
|
return;
|
|
591
692
|
}
|
|
592
693
|
|
|
@@ -713,6 +814,100 @@ async function handleHistoryClear(history, rl) {
|
|
|
713
814
|
console.log("\n" + label.system + " " + colors.success("✓ Persistent chat history and prompt history cleared.\n"));
|
|
714
815
|
}
|
|
715
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
|
+
|
|
716
911
|
function handleGameStart(game) {
|
|
717
912
|
if (game.active) {
|
|
718
913
|
console.log("\n" + label.system + " " + colors.warning("Mainframe breach is already in progress. Type /abort to cancel.\n"));
|
|
@@ -1020,3 +1215,121 @@ async function handleWriteFile(args, ctx) {
|
|
|
1020
1215
|
console.log("\n" + label.error + " " + colors.danger(`Write failed: ${err.message}\n`));
|
|
1021
1216
|
}
|
|
1022
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
|
+
}
|