@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/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
- // ── Query AI ──────────────────────────────────────────
223
- const queryStartTime = Date.now();
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 hasStartedStreaming = false;
231
- let streamedText = "";
232
- const filter = new StreamFilter(process.stdout.write.bind(process.stdout));
233
- const onToken = (token) => {
234
- if (!hasStartedStreaming) {
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
- const result = await routePrompt(fullPrompt, currentMode.systemPrompt, aiConfig, onToken, history);
245
- spinner.stop();
246
- filter.flush();
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
- // Save to persistent file
260
- await saveHistory(history);
243
+ if (loopCount > 0) {
244
+ console.log(colors.accent(`\n🤖 [Aether Autopilot Mode - Iteration ${loopCount + 1}/${MAX_LOOPS}]`));
245
+ }
261
246
 
262
- if (hasStartedStreaming) {
263
- clearStreamedText(filter.filteredText);
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
- // Display response
267
- console.log("");
268
- console.log(label.aether + " " + providerBadge(result));
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
- if (result.provider === "local" || result.provider === "krylo-fallback") {
273
- console.log(colors.text(" " + result.text.split("\n").join("\n ")));
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
- const elapsedSec = ((Date.now() - queryStartTime) / 1000).toFixed(1);
284
- let speedText = "";
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
- console.log(separator("─"));
295
- console.log(
296
- " " + colors.dim(`Node ${result.node} • ${result.provider}`) +
297
- (result.model ? colors.dim(` • ${result.model}`) : "") +
298
- colors.dim(` • ${elapsedSec}s${speedText}`) +
299
- colors.dim(` • ${Math.floor(history.length / 2)} exchanges`)
300
- );
301
- console.log("");
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
- // Parse file write blocks
304
- const writeRegex = /\[WRITE_FILE:\s*([^\n\]]+)\]\n([\s\S]*?)\n\[END_WRITE\]/g;
305
- let match;
306
- const fileWrites = [];
307
- while ((match = writeRegex.exec(result.text)) !== null) {
308
- fileWrites.push({ path: match[1].trim(), content: match[2] });
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
- if (fileWrites.length > 0) {
312
- const { dirname } = await import("node:path");
313
- const { mkdir } = await import("node:fs/promises");
314
-
315
- for (const fileWrite of fileWrites) {
316
- const finalPath = resolve(fileWrite.path);
317
- console.log("");
318
- console.log(label.system + " " + colors.warning(`Auto-Writing File: ${colors.accent(finalPath)} (${fileWrite.content.length} bytes)`));
319
- try {
320
- const dir = dirname(finalPath);
321
- await mkdir(dir, { recursive: true });
322
- await writeFile(finalPath, fileWrite.content, "utf-8");
323
- console.log(" " + colors.success(`✓ File created successfully!\n`));
324
- } catch (err) {
325
- console.log(" " + colors.danger(`✗ Write failed: ${err.message}\n`));
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
- console.log("\n" + label.file + " " + colors.warning("Usage: /attach <path-to-file>\n"));
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
+ }