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