jinzd-ai-cli 0.4.284 → 0.4.286

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.
Files changed (43) hide show
  1. package/README.md +3 -3
  2. package/README.zh-CN.md +3 -3
  3. package/dist/{batch-DA7VA35X.js → batch-GY3I53DE.js} +13 -10
  4. package/dist/{chat-index-TDU3S2MG.js → chat-index-DGUOUUQW.js} +2 -2
  5. package/dist/{chunk-VFVN5CWA.js → chunk-2YDLBM3Z.js} +1 -1
  6. package/dist/{chunk-OZKTHQWA.js → chunk-3ZQZO5F7.js} +3 -3
  7. package/dist/chunk-53V3RQY5.js +5684 -0
  8. package/dist/{chunk-C4CIIIDT.js → chunk-6SCELZG6.js} +3 -3
  9. package/dist/{chunk-XSBGCZOP.js → chunk-7G7RZHJI.js} +1 -1
  10. package/dist/{chunk-I4FJGILK.js → chunk-BRLLV3FX.js} +10 -10
  11. package/dist/chunk-CGLYJRFA.js +95 -0
  12. package/dist/{chunk-FZ4THGUP.js → chunk-D7HYLMCC.js} +18 -110
  13. package/dist/{chunk-BXG44VIQ.js → chunk-EYUCJBGS.js} +30 -30
  14. package/dist/{chunk-3DRSBZQ7.js → chunk-GIJL4EFA.js} +10 -2
  15. package/dist/{chunk-QKGKWCXE.js → chunk-L4FDTQ42.js} +1 -1
  16. package/dist/{chunk-DPTEC6AQ.js → chunk-LWPWBRAC.js} +12 -10
  17. package/dist/chunk-RS7SKYTS.js +45 -0
  18. package/dist/{chunk-PWLQU43X.js → chunk-S7ABWTDV.js} +1 -1
  19. package/dist/{chunk-RV2YOTEQ.js → chunk-THUXZNJL.js} +1 -1
  20. package/dist/{chunk-PPPSK4MA.js → chunk-UKITSNSH.js} +1 -1
  21. package/dist/{ci-HG2TPYVY.js → ci-JBUIVM7P.js} +4 -4
  22. package/dist/{ci-format-UB7K63UZ.js → ci-format-B4AHV5SI.js} +2 -2
  23. package/dist/{constants-3DDHA3LT.js → constants-5FYUQLKN.js} +1 -1
  24. package/dist/constants-P75MLFV7.js +80 -0
  25. package/dist/{doctor-cli-ISFDY7LZ.js → doctor-cli-VLFE46PX.js} +5 -4
  26. package/dist/electron-server.js +3654 -310
  27. package/dist/git-context-VNCA2GU7.js +10 -0
  28. package/dist/{hub-MP2CNDMZ.js → hub-CNORVRYW.js} +1 -1
  29. package/dist/index.js +304 -3589
  30. package/dist/{persistent-memory-AMDRRB5E.js → persistent-memory-3KL4N27E.js} +3 -2
  31. package/dist/{persistent-memory-J2OVPLWI.js → persistent-memory-LVX2OT4L.js} +2 -2
  32. package/dist/{pr-BREZ3DJX.js → pr-2YUV4KII.js} +4 -4
  33. package/dist/{run-tests-XHLV74GU.js → run-tests-ERD6X534.js} +2 -2
  34. package/dist/{run-tests-DPO23PKA.js → run-tests-KZ2IUZHK.js} +2 -2
  35. package/dist/{semantic-FF6DDJI6.js → semantic-VPNKXKKK.js} +2 -2
  36. package/dist/{server-KK7DIV3J.js → server-7VSLW446.js} +6 -5
  37. package/dist/{server-W2VIUKTQ.js → server-A3OX52UC.js} +78 -76
  38. package/dist/{task-orchestrator-YM5IMIKL.js → task-orchestrator-XAMKOFUE.js} +6 -5
  39. package/dist/{usage-TXPZY7EB.js → usage-ZXNYCAZB.js} +4 -3
  40. package/package.json +1 -1
  41. package/dist/chunk-Z23JYUOL.js +0 -2296
  42. package/dist/{chunk-AEAYUKSY.js → chunk-7XU4BATV.js} +3 -3
  43. package/dist/{chunk-YOVHMQEJ.js → chunk-I4GQRMFV.js} +3 -3
package/dist/index.js CHANGED
@@ -1,93 +1,47 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  AUTO_COMPACT_INSTRUCTION,
4
- MEMORY_USAGE,
4
+ CommandRegistry,
5
5
  McpManager,
6
6
  SNAPSHOT_PROMPT,
7
7
  SkillManager,
8
- applyPlanCommand,
9
- applyProfileCommand,
10
- applyThinkCommand,
11
8
  armDeferredSave,
12
9
  autoTrimSessionIfNeeded,
13
- buildInitPrompt,
14
- buildMemoryStatusView,
15
- buildRecallView,
16
- buildRewindList,
17
- buildSessionCostReport,
18
- buildSessionExport,
19
- checkpointDeletedMessage,
20
- checkpointNotFoundMessage,
21
- checkpointRestoredMessage,
22
- checkpointSavedMessage,
23
10
  clearDevState,
24
- clearedSessionMessage,
25
- costSourceNote,
11
+ createDefaultCommands,
26
12
  decideDeferredSave,
27
13
  decideModel,
28
- describeRouting,
29
14
  estimateConversationTokens,
30
15
  evaluateContextPressure,
31
- filterMemoryEntries,
32
- formatCheckpointList,
33
- formatForkReport,
34
- formatRewindReport,
35
- formatSessionSummaryLine,
16
+ getClipboardHint,
36
17
  loadDevState,
37
- parseBatchDeleteArgs,
38
- parseCheckpointCommand,
39
- parseCleanArgs,
40
- parseExportArgs,
41
- parseMemoryCommand,
42
- parseMemoryShowFilters,
43
- parseModelCommand,
44
- parseRewindArgs,
45
- parseRouteCommand,
46
- parseSessionListArgs,
47
18
  parseSimpleYaml,
48
19
  persistToolRound,
20
+ readClipboardImage,
49
21
  readGitDiff,
50
- rebuildDoneMessage,
51
- rebuildStartMessage,
52
- resolveForkPoint,
53
- resolveModelSwitch,
54
- resolveProviderSwitch,
55
- resolveSessionRef,
56
- routeDecisionRows,
57
- routingEmptyRulesNote,
58
22
  saveDevState,
59
- scanProject,
60
- selectSessions,
61
- selectStaleSessions,
62
23
  sessionHasMeaningfulContent,
63
- sessionListJsonRow,
64
- sessionListSummary,
65
24
  setupProxy,
66
25
  snapshotFile,
67
26
  stripRoutingTags
68
- } from "./chunk-Z23JYUOL.js";
27
+ } from "./chunk-53V3RQY5.js";
69
28
  import {
70
29
  CostTracker,
71
- computeCost,
72
- formatCost,
73
30
  getPricing,
74
31
  setCostCurrency
75
- } from "./chunk-DPTEC6AQ.js";
32
+ } from "./chunk-LWPWBRAC.js";
76
33
  import {
77
34
  ToolExecutor,
78
35
  ToolRegistry,
79
36
  askUserContext,
80
37
  cleanupAllSessions,
81
38
  cleanupRejectedTeeFile,
82
- clearRecentlyDeniedAutoActions,
83
39
  estimateTokens,
84
40
  formatPermissionProfileWarning,
85
- getRecentlyDeniedAutoActions,
86
41
  googleSearchContext,
87
42
  isInterrupted,
88
43
  isToolCapableProvider,
89
44
  lastResponseStore,
90
- renderDiff,
91
45
  requestInterrupt,
92
46
  resetInterrupt,
93
47
  rlInternal,
@@ -95,48 +49,20 @@ import {
95
49
  setMaxOutputCap,
96
50
  spawnAgentContext,
97
51
  undoStack
98
- } from "./chunk-I4FJGILK.js";
99
- import {
100
- buildReviewPrompt,
101
- buildSecurityReviewPrompt
102
- } from "./chunk-HLWUDRBO.js";
52
+ } from "./chunk-BRLLV3FX.js";
53
+ import "./chunk-HLWUDRBO.js";
103
54
  import {
104
55
  NO_THINKING,
105
56
  applyThinkingFlag,
106
57
  chatParamsFrom,
107
58
  thinkingParamsFor
108
59
  } from "./chunk-6YZEZFX5.js";
60
+ import "./chunk-GHWZVPUV.js";
109
61
  import {
110
- buildChatIndex,
111
- clearChatIndex,
112
- getChatIndexStatus,
113
- searchChatMemory
114
- } from "./chunk-GHWZVPUV.js";
115
- import {
116
- addMemoryEntry,
117
- backupMemoryStore,
118
- clearMemoryEntries,
119
62
  countPendingMemories,
120
- deleteMemoryEntry,
121
63
  detectStaleLegacyMarkdownEdit,
122
- expireMemoryEntry,
123
- exportMemoryEntries,
124
- findSimilarMemory,
125
- formatMemoryForPrompt,
126
- isMemoryExpired,
127
- listMemoryEntries,
128
- listOrphanedProjectMemories,
129
- loadMemoryEntries,
130
- memoryMarkdownPath,
131
- memoryStorePath,
132
- parseWritableScope,
133
- rebindMemoryProject,
134
- restoreMemoryEntries,
135
- searchPersistentMemories,
136
- touchMemoryReferences,
137
- updateMemoryApproval,
138
- updateMemoryEntry
139
- } from "./chunk-PPPSK4MA.js";
64
+ formatMemoryForPrompt
65
+ } from "./chunk-UKITSNSH.js";
140
66
  import "./chunk-4DEMGCK4.js";
141
67
  import "./chunk-UUSRWSSX.js";
142
68
  import "./chunk-CKH4KQ4E.js";
@@ -152,39 +78,27 @@ import {
152
78
  } from "./chunk-IUVDZVZ4.js";
153
79
  import "./chunk-NYNSYLFY.js";
154
80
  import {
155
- DEFAULT_PATTERNS,
156
81
  scanString
157
82
  } from "./chunk-3FEZSLGI.js";
158
83
  import {
159
- buildDoctorReport,
160
- formatDoctorReport,
161
84
  loadContextFiles,
162
85
  writeCrashLog
163
- } from "./chunk-C4CIIIDT.js";
86
+ } from "./chunk-6SCELZG6.js";
164
87
  import {
165
- describePlugin,
166
88
  getActivePluginAssets,
167
- getInstalledPlugin,
168
89
  getPendingHookTrust,
169
90
  installFlushOnExit,
170
- installPlugin,
171
- listHooks,
172
- listInstalledPlugins,
173
91
  mergePluginHooks,
174
- pluginRoot,
175
- runLifecycleHooks,
176
- setPluginEnabled,
177
- trustHook,
178
- trustPlugin,
179
- untrustHook
180
- } from "./chunk-BXG44VIQ.js";
92
+ runLifecycleHooks
93
+ } from "./chunk-EYUCJBGS.js";
94
+ import "./chunk-RS7SKYTS.js";
181
95
  import {
182
96
  ProviderRegistry,
183
97
  listBuiltInProviderMetadata
184
98
  } from "./chunk-UGY5SVNX.js";
185
99
  import {
186
100
  ConfigManager
187
- } from "./chunk-XSBGCZOP.js";
101
+ } from "./chunk-7G7RZHJI.js";
188
102
  import {
189
103
  CONTENT_ONLY_STREAM_REMINDER,
190
104
  TEE_FINAL_USER_NUDGE,
@@ -216,13 +130,9 @@ import {
216
130
  import {
217
131
  atomicWriteFileSync
218
132
  } from "./chunk-IW3Q7AE5.js";
219
- import {
220
- applyConfigCommand
221
- } from "./chunk-XNFIYS7G.js";
222
- import {
223
- fileCheckpoints
224
- } from "./chunk-4BKXL7SM.js";
225
- import "./chunk-PWLQU43X.js";
133
+ import "./chunk-XNFIYS7G.js";
134
+ import "./chunk-4BKXL7SM.js";
135
+ import "./chunk-S7ABWTDV.js";
226
136
  import {
227
137
  AGENTIC_BEHAVIOR_GUIDELINE,
228
138
  AUTHOR,
@@ -236,20 +146,19 @@ import {
236
146
  MCP_PROJECT_CONFIG_NAME,
237
147
  PLAN_MODE_READONLY_TOOLS,
238
148
  PLAN_MODE_SYSTEM_ADDON,
239
- REPO_URL,
240
149
  SKILLS_DIR_NAME,
241
150
  VERSION,
242
151
  buildUserIdentityPrompt
243
- } from "./chunk-RV2YOTEQ.js";
152
+ } from "./chunk-THUXZNJL.js";
244
153
 
245
154
  // src/index.ts
246
155
  import { program } from "commander";
247
156
 
248
157
  // src/repl/repl.ts
249
158
  import * as readline from "readline";
250
- import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, statSync as statSync4, mkdirSync as mkdirSync4 } from "fs";
251
- import { join as join5, resolve as resolve3, dirname as dirname3, basename as basename2 } from "path";
252
- import chalk5 from "chalk";
159
+ import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync as readdirSync3, statSync as statSync3, mkdirSync as mkdirSync3 } from "fs";
160
+ import { join as join3, resolve as resolve2, dirname as dirname2, basename } from "path";
161
+ import chalk3 from "chalk";
253
162
 
254
163
  // src/session/title-generator.ts
255
164
  var SYSTEM_PROMPT = 'You generate a very short title for a conversation. Rules: output ONLY the title (no quotes, no prefix like "Title:", no trailing punctuation). Length: \u2264 12 Chinese characters, or \u2264 6 English words. Match the language the user used. Capture the topic, not generic words like "question" or "help".';
@@ -550,3412 +459,218 @@ var Renderer = class {
550
459
  console.log(HR);
551
460
  console.log(theme.dim(" Key Features:"));
552
461
  for (const f of ABOUT_FEATURES) console.log(feat(f));
553
- console.log();
554
- console.log(theme.dim(" \u2726 ") + theme.accent("Full per-version history \u2192 CHANGELOG.md"));
555
- console.log();
556
- }
557
- printPrompt(provider, _model) {
558
- process.stdout.write(theme.prompt(`[${provider}]`) + chalk.white(" > "));
559
- }
560
- async renderStream(stream, options) {
561
- process.stdout.write(theme.accent("Assistant: "));
562
- let fileStream = null;
563
- if (options?.saveToFile) {
564
- mkdirSync(dirname(options.saveToFile), { recursive: true });
565
- fileStream = createWriteStream(options.saveToFile, { encoding: "utf-8" });
566
- }
567
- let fullContent = "";
568
- let usage;
569
- const thinkFilter = new ThinkTagFilter();
570
- const writeVisible = (visible) => {
571
- if (!visible) return;
572
- process.stdout.write(visible);
573
- if (fileStream) fileStream.write(visible);
574
- fullContent += visible;
575
- };
576
- const emitText = (raw) => writeVisible(thinkFilter.push(raw));
577
- const flushTail = () => writeVisible(thinkFilter.flush());
578
- let interrupted = false;
579
- let streamErr = null;
580
- try {
581
- for await (const chunk of stream) {
582
- if (options?.signal?.aborted) {
583
- interrupted = true;
584
- break;
585
- }
586
- if (chunk.usage) {
587
- usage = chunk.usage;
588
- }
589
- if (chunk.done) {
590
- break;
591
- }
592
- if (!chunk.delta) continue;
593
- emitText(chunk.delta);
594
- }
595
- } catch (err) {
596
- if (err?.name === "AbortError") {
597
- interrupted = true;
598
- } else {
599
- streamErr = err;
600
- }
601
- }
602
- flushTail();
603
- if (interrupted) {
604
- process.stdout.write(theme.dim(" [interrupted]\n"));
605
- }
606
- let tokensShown = false;
607
- if (!streamErr) {
608
- if (options?.showTokens) {
609
- process.stdout.write("\n");
610
- if (usage) {
611
- const sessionTotal = options.sessionTotal;
612
- const updatedTotal = sessionTotal ? { inputTokens: sessionTotal.inputTokens + usage.inputTokens, outputTokens: sessionTotal.outputTokens + usage.outputTokens } : void 0;
613
- this.renderUsage(usage, updatedTotal);
614
- tokensShown = true;
615
- } else if (fullContent.length > 0) {
616
- const est = Math.ceil(fullContent.length / 2.5);
617
- process.stdout.write(theme.dim(`\u{1F4CA} ~${est.toLocaleString()} output tokens (estimated)
618
-
619
- `));
620
- tokensShown = true;
621
- } else {
622
- process.stdout.write("\n");
623
- }
624
- } else {
625
- process.stdout.write("\n\n");
626
- }
627
- } else {
628
- process.stdout.write("\n");
629
- }
630
- if (fileStream) {
631
- try {
632
- await new Promise((resolve4) => {
633
- fileStream.end(() => resolve4());
634
- });
635
- } catch {
636
- }
637
- if (!streamErr) {
638
- const kb = (Buffer.byteLength(fullContent, "utf-8") / 1024).toFixed(1);
639
- process.stdout.write(theme.success(` \u2705 Saved: ${options.saveToFile} (${kb} KB)
640
-
641
- `));
642
- }
643
- }
644
- if (streamErr) throw streamErr;
645
- return { content: fullContent, usage, tokensShown };
646
- }
647
- renderResponse(content) {
648
- process.stdout.write(theme.accent("Assistant: "));
649
- let displayed = content;
650
- if (content.includes("<think>")) {
651
- displayed = content.replace(/<think>[\s\S]*?<\/think>/g, "").trimStart();
652
- }
653
- const finalOutput = this.wrapWidth > 0 ? wrapText(displayed, this.wrapWidth) : displayed;
654
- process.stdout.write(finalOutput);
655
- process.stdout.write("\n\n");
656
- }
657
- /**
658
- * 将已有 content 以模拟流式效果逐块输出(打字机体验),零额外 API 调用。
659
- * 支持 AbortSignal 中断。
660
- */
661
- async renderContentAsStream(content, options) {
662
- process.stdout.write(theme.accent("Assistant: "));
663
- let displayed = content;
664
- if (content.includes("<think>")) {
665
- displayed = content.replace(/<think>[\s\S]*?<\/think>/g, "").trimStart();
666
- }
667
- if (this.wrapWidth > 0) {
668
- displayed = wrapText(displayed, this.wrapWidth);
669
- }
670
- const CHUNK_SIZE = 12;
671
- const DELAY_MS = 8;
672
- let pos = 0;
673
- while (pos < displayed.length) {
674
- if (options?.signal?.aborted) {
675
- process.stdout.write(theme.dim("\n[interrupted]"));
676
- break;
677
- }
678
- const end = Math.min(pos + CHUNK_SIZE, displayed.length);
679
- process.stdout.write(displayed.slice(pos, end));
680
- pos = end;
681
- if (pos < displayed.length) {
682
- await new Promise((resolve4) => setTimeout(resolve4, DELAY_MS));
683
- }
684
- }
685
- process.stdout.write("\n\n");
686
- }
687
- renderError(err) {
688
- const message = err instanceof Error ? err.message : String(err);
689
- const lines = [message];
690
- if (err instanceof Error && err.cause) {
691
- let cause = err.cause;
692
- let depth = 0;
693
- while (cause && depth < 3) {
694
- const causeMsg = cause instanceof Error ? cause.message : String(cause);
695
- lines.push(` Caused by: ${causeMsg}`);
696
- cause = cause instanceof Error ? cause.cause : void 0;
697
- depth++;
698
- }
699
- }
700
- const typeName = err instanceof Error && err.name !== "Error" ? ` [${err.name}]` : "";
701
- console.error(theme.error(`
702
- Error${typeName}: ${lines.join("\n")}
703
- `));
704
- }
705
- /**
706
- * 在 AI 回复后显示 token 用量。
707
- * @param usage 本次调用的 input/output tokens
708
- * @param sessionTotal 本次会话累计 tokens(可选)
709
- */
710
- renderUsage(usage, sessionTotal) {
711
- const total = usage.inputTokens + usage.outputTokens;
712
- let line = theme.dim("\u{1F4CA} ") + theme.dim(`in ${usage.inputTokens.toLocaleString()}`) + theme.dim(" + ") + theme.dim(`out ${usage.outputTokens.toLocaleString()}`) + theme.dim(` = ${total.toLocaleString()} tokens`);
713
- const cacheRead = usage.cacheReadTokens ?? 0;
714
- if (cacheRead > 0) {
715
- line += theme.dim(` \u2502 cache: ${cacheRead.toLocaleString()}`);
716
- }
717
- if (sessionTotal) {
718
- const sessionSum = sessionTotal.inputTokens + sessionTotal.outputTokens + (sessionTotal.cacheCreationTokens ?? 0) + (sessionTotal.cacheReadTokens ?? 0);
719
- line += theme.dim(` \u2502 session total: ${sessionSum.toLocaleString()}`);
720
- }
721
- process.stdout.write(line + "\n\n");
722
- }
723
- printInfo(message) {
724
- console.log(theme.warning(` ${message}`));
725
- }
726
- printSuccess(message) {
727
- console.log(theme.success(` ${message}`));
728
- }
729
- showSpinner(text) {
730
- return createSpinner(text);
731
- }
732
- printTable(headers, rows) {
733
- const colWidths = headers.map((h, i) => {
734
- const maxRow = Math.max(...rows.map((r) => stripAnsi(String(r[i] ?? "")).length));
735
- return Math.max(stripAnsi(h).length, maxRow);
736
- });
737
- const separator = colWidths.map((w) => "-".repeat(w + 2)).join("+");
738
- const headerLine = headers.map((h, i) => ` ${h.padEnd(colWidths[i])} `).join("|");
739
- console.log(theme.dim(separator));
740
- console.log(theme.heading(headerLine));
741
- console.log(theme.dim(separator));
742
- for (const row of rows) {
743
- const line = row.map((cell, i) => {
744
- const val = String(cell ?? "");
745
- const colored = cell === true ? theme.success(val) : cell === false ? theme.error(val) : val;
746
- const visLen = stripAnsi(colored).length;
747
- const pad = Math.max(0, colWidths[i] - visLen);
748
- return ` ${colored}${" ".repeat(pad)} `;
749
- }).join("|");
750
- console.log(line);
751
- }
752
- console.log(theme.dim(separator));
753
- }
754
- };
755
-
756
- // src/repl/commands/utils.ts
757
- import { execSync } from "child_process";
758
- import { platform } from "os";
759
-
760
- // src/repl/commands/constants.ts
761
- var SESSION_PAGE_SIZE = 20;
762
- var MAX_TITLE_WIDTH = 40;
763
-
764
- // src/repl/commands/utils.ts
765
- function fmtCtx(tokens) {
766
- if (tokens >= 1e6) return `${Math.round(tokens / 1e5) / 10}M`;
767
- if (tokens >= 1e3) return `${Math.round(tokens / 1024)}K`;
768
- return `${tokens}`;
769
- }
770
- function copyToClipboard(text) {
771
- const plat = platform();
772
- if (plat === "win32") {
773
- execSync("clip", { input: text, stdio: ["pipe", "ignore", "ignore"] });
774
- } else if (plat === "darwin") {
775
- execSync("pbcopy", { input: text, stdio: ["pipe", "ignore", "ignore"] });
776
- } else {
777
- try {
778
- execSync("xclip -selection clipboard", { input: text, stdio: ["pipe", "ignore", "ignore"] });
779
- } catch {
780
- execSync("xsel --clipboard --input", { input: text, stdio: ["pipe", "ignore", "ignore"] });
781
- }
782
- }
783
- }
784
- function oneLinePreview(s, max) {
785
- const flat = s.replace(/\s+/g, " ").trim();
786
- return flat.length > max ? flat.slice(0, max - 1) + "\u2026" : flat;
787
- }
788
- function formatFileSize(bytes) {
789
- if (bytes < 1024) return `${bytes} B`;
790
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
791
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
792
- }
793
- function visualWidth(s) {
794
- const clean = s.replace(/\x1b\[[0-9;]*m/g, "");
795
- let w = 0;
796
- for (const ch of clean) {
797
- const cp = ch.codePointAt(0);
798
- if (cp >= 4352 && cp <= 4447 || // Hangul Jamo
799
- cp >= 11904 && cp <= 42191 || // CJK Radicals … Yi
800
- cp >= 44032 && cp <= 55203 || // Hangul Syllables
801
- cp >= 63744 && cp <= 64255 || // CJK Compatibility Ideographs
802
- cp >= 65072 && cp <= 65135 || // CJK Compatibility Forms
803
- cp >= 65281 && cp <= 65376 || // Fullwidth Forms
804
- cp >= 65504 && cp <= 65510 || // Fullwidth Signs
805
- cp >= 126976 && cp <= 129791 || // Mahjong / Domino / …
806
- cp >= 131072 && cp <= 196605 || // CJK Extension B+
807
- cp >= 196608 && cp <= 262141) {
808
- w += 2;
809
- } else {
810
- w += 1;
811
- }
812
- }
813
- return w;
814
- }
815
- function truncateTitle(title, maxWidth) {
816
- if (visualWidth(title) <= maxWidth) return title;
817
- let w = 0;
818
- let end = 0;
819
- for (const ch of title) {
820
- const chW = visualWidth(ch);
821
- if (w + chW > maxWidth - 1) break;
822
- w += chW;
823
- end++;
824
- }
825
- return title.slice(0, end) + "\u2026";
826
- }
827
- function formatSessionDate(date) {
828
- const pad = (n) => n.toString().padStart(2, "0");
829
- const ymd = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
830
- const hm = `${pad(date.getHours())}:${pad(date.getMinutes())}`;
831
- const abs = `${ymd} ${hm}`;
832
- const now = /* @__PURE__ */ new Date();
833
- const startOfDay = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
834
- const diffDays = Math.round((startOfDay(now) - startOfDay(date)) / (1e3 * 60 * 60 * 24));
835
- if (diffDays <= 0) return `${abs} (today)`;
836
- if (diffDays === 1) return `${abs} (yesterday)`;
837
- if (diffDays < 7) return `${abs} (${diffDays} days ago)`;
838
- return abs;
839
- }
840
- function renderTranscriptMessage(msg, renderer) {
841
- if (msg.role === "user") {
842
- const text = getContentText(msg.content);
843
- console.log(theme.success("You: ") + (text.includes("\n") ? "\n" + text : text));
844
- console.log();
845
- } else if (msg.role === "assistant") {
846
- const text = getContentText(msg.content);
847
- if (msg.toolCalls && msg.toolCalls.length > 0) {
848
- if (text.trim()) {
849
- console.log(theme.dim(`Assistant: ${oneLinePreview(text, 100)}`));
850
- }
851
- for (const tc of msg.toolCalls) {
852
- console.log(theme.dim(` \u2699 ${tc.name} ${oneLinePreview(JSON.stringify(tc.arguments ?? {}), 80)}`));
853
- }
854
- } else if (text.trim()) {
855
- renderer.renderResponse(text);
856
- console.log();
857
- }
858
- } else if (msg.role === "tool") {
859
- const preview = oneLinePreview(getContentText(msg.content), 80);
860
- const paint = msg.isError ? theme.error : theme.dim;
861
- console.log(paint(` \u2192 ${msg.toolName ?? "tool"}: ${preview}`));
862
- }
863
- }
864
- function renderTranscriptPage(messages, renderer, requestedPage) {
865
- const total = messages.length;
866
- if (total === 0) {
867
- renderer.printInfo("Session has no messages yet.");
868
- return;
869
- }
870
- const totalPages = Math.max(1, Math.ceil(total / SESSION_PAGE_SIZE));
871
- let page = requestedPage === void 0 || !Number.isFinite(requestedPage) ? totalPages : requestedPage;
872
- if (page < 1) page = 1;
873
- if (page > totalPages) page = totalPages;
874
- const start = (page - 1) * SESSION_PAGE_SIZE;
875
- const slice = messages.slice(start, start + SESSION_PAGE_SIZE);
876
- console.log();
877
- console.log(theme.info(` \u2500\u2500 Transcript page ${page}/${totalPages} (messages ${start + 1}\u2013${start + slice.length} of ${total}) \u2500\u2500`));
878
- console.log();
879
- for (const msg of slice) {
880
- renderTranscriptMessage(msg, renderer);
881
- }
882
- const hints = [];
883
- if (page > 1) hints.push(`/session page ${page - 1} = earlier`);
884
- if (page < totalPages) hints.push(`/session page ${page + 1} = later`);
885
- if (hints.length > 0) {
886
- console.log(theme.dim(` \u2500\u2500 ${hints.join(" \xB7 ")} \u2500\u2500`));
887
- }
888
- console.log();
889
- }
890
- async function promptSkillHistoryAction(ctx, fromLabel, toLabel) {
891
- const session = ctx.sessions.current;
892
- const msgCount = session?.messages.length ?? 0;
893
- if (msgCount === 0 || !session) return true;
894
- const choice = await ctx.select(
895
- `Switching skill: ${fromLabel} \u2192 ${toLabel} (${msgCount} message(s) in context)`,
896
- [
897
- { value: "clear", label: "Clear history", hint: "recommended \u2014 fresh start under new skill" },
898
- { value: "fork", label: "Fork to new branch", hint: "preserve old branch, start fresh on new one" },
899
- { value: "keep", label: "Keep history", hint: "old skill actions may bias new skill" },
900
- { value: "cancel", label: "Cancel", hint: "do not switch skill" }
901
- ],
902
- 0
903
- );
904
- if (!choice || choice === "cancel") return false;
905
- if (choice === "clear") {
906
- session.clear();
907
- ctx.resetSessionTokenUsage();
908
- ctx.clearDevState();
909
- ctx.renderer.printInfo(` History cleared (${msgCount} message(s) removed).`);
910
- } else if (choice === "fork") {
911
- try {
912
- const branchTitle = toLabel === "(none)" ? `pre-deactivate-${fromLabel}` : `skill-${toLabel}`;
913
- const newId = session.createBranch(0, branchTitle);
914
- await ctx.sessions.save();
915
- ctx.renderer.printInfo(` Forked to fresh branch "${newId}" (old branch preserved).`);
916
- } catch (err) {
917
- ctx.renderer.renderError(`Fork failed: ${err.message}`);
918
- return false;
919
- }
920
- }
921
- if (choice === "keep") {
922
- ctx.renderer.printInfo(" Keeping history. Use /clear later if the new skill misbehaves.");
923
- }
924
- return true;
925
- }
926
-
927
- // src/repl/commands/provider-commands.ts
928
- function createProviderCommands() {
929
- return [
930
- {
931
- name: "provider",
932
- description: "Switch AI provider (interactive selector when no arg)",
933
- usage: "/provider [name]",
934
- async execute(args, ctx) {
935
- let targetId;
936
- if (!args[0]) {
937
- const all = ctx.providers.listAll();
938
- const currentProvider = ctx.getCurrentProvider();
939
- const items = all.map((p) => ({
940
- value: p.id,
941
- label: p.displayName,
942
- hint: p.configured ? "\u2713 configured" : "\u2717 not configured"
943
- }));
944
- const initialIdx = Math.max(0, items.findIndex((i) => i.value === currentProvider));
945
- const chosen = await ctx.select("Select provider:", items, initialIdx);
946
- if (!chosen) return;
947
- targetId = chosen;
948
- } else {
949
- targetId = args[0];
950
- }
951
- const configured = ctx.providers.has(targetId);
952
- let availableModels = [];
953
- let providerDefaultModel = "";
954
- if (configured) {
955
- const target = ctx.providers.get(targetId);
956
- providerDefaultModel = target.info.defaultModel;
957
- availableModels = target.info.models.map((m) => m.id);
958
- if (availableModels.length === 0) {
959
- availableModels = (await target.listModels()).map((m) => m.id);
960
- }
961
- }
962
- const session = ctx.sessions.current;
963
- const outcome = resolveProviderSwitch({
964
- targetId,
965
- currentProvider: ctx.getCurrentProvider(),
966
- configured,
967
- availableModels,
968
- configuredDefaultModel: ctx.config.get("defaultModels")[targetId],
969
- providerDefaultModel,
970
- messageCount: session?.messages.length ?? 0
971
- });
972
- if (outcome.status === "not-configured" || outcome.status === "no-models") {
973
- ctx.renderer.renderError(outcome.message);
974
- return;
975
- }
976
- if (outcome.status === "unchanged") {
977
- ctx.renderer.printInfo(outcome.message);
978
- return;
979
- }
980
- ctx.setProvider(outcome.providerId, outcome.model);
981
- if (session) {
982
- session.updateProvider(outcome.providerId, outcome.model);
983
- await ctx.sessions.save();
984
- }
985
- ctx.renderer.printSuccess(outcome.message);
986
- }
987
- },
988
- {
989
- name: "model",
990
- description: "Switch model, refresh live model list, or inspect model cache",
991
- usage: "/model [name|refresh [provider]|cache [clear [provider]]]",
992
- async execute(args, ctx) {
993
- const intent = parseModelCommand(args, ctx.getCurrentProvider());
994
- if (intent.kind === "refresh") {
995
- if (!ctx.providers.has(intent.providerId)) {
996
- ctx.renderer.printInfo(`Provider is not configured: ${intent.providerId}`);
997
- return;
998
- }
999
- ctx.renderer.printInfo(`Refreshing models for ${intent.providerId}...`);
1000
- try {
1001
- const models = await ctx.providers.refreshModels(intent.providerId);
1002
- ctx.renderer.printSuccess(`Refreshed ${models.length} model(s) for ${intent.providerId}.`);
1003
- } catch (err) {
1004
- ctx.renderer.printInfo(`Model refresh failed: ${err instanceof Error ? err.message : String(err)}`);
1005
- }
1006
- return;
1007
- }
1008
- if (intent.kind === "cache-clear") {
1009
- ctx.providers.clearModelCache(intent.providerId);
1010
- ctx.renderer.printSuccess(
1011
- intent.providerId ? `Cleared model cache for ${intent.providerId}.` : "Cleared model cache."
1012
- );
1013
- return;
1014
- }
1015
- if (intent.kind === "cache-show") {
1016
- const rows = ctx.providers.getModelCacheStatus();
1017
- if (rows.length === 0) {
1018
- ctx.renderer.printInfo("Model cache is empty. Use /model refresh to populate it.");
1019
- return;
1020
- }
1021
- ctx.renderer.printTable(
1022
- ["Provider", "Models", "Fetched"],
1023
- rows.map((row) => [row.providerId, row.count, row.fetchedAt])
1024
- );
1025
- return;
1026
- }
1027
- let knownModels = ctx.providers.has(ctx.getCurrentProvider()) ? ctx.providers.get(ctx.getCurrentProvider()).info.models.map((m) => m.id) : [];
1028
- let targetModel;
1029
- if (intent.kind === "select") {
1030
- const models = await ctx.providers.listModels(ctx.getCurrentProvider());
1031
- knownModels = models.map((m) => m.id);
1032
- const currentModel = ctx.getCurrentModel();
1033
- const items = models.map((m) => ({
1034
- value: m.id,
1035
- label: m.id,
1036
- hint: [
1037
- m.displayName,
1038
- `ctx:${fmtCtx(m.contextWindow)}`,
1039
- m.supportsThinking ? "thinking" : "",
1040
- m.metadataUnknown ? "metadata?" : "",
1041
- m.source && m.source !== "curated" ? m.source : "",
1042
- m.id === currentModel ? "(current)" : ""
1043
- ].filter(Boolean).join(" ")
1044
- }));
1045
- const initialIdx = Math.max(0, items.findIndex((i) => i.value === currentModel));
1046
- const chosen = await ctx.select("Select model:", items, initialIdx);
1047
- if (!chosen) return;
1048
- targetModel = chosen;
1049
- } else {
1050
- targetModel = intent.modelId;
1051
- }
1052
- const session = ctx.sessions.current;
1053
- const outcome = resolveModelSwitch({
1054
- targetModel,
1055
- currentModel: ctx.getCurrentModel(),
1056
- provider: ctx.getCurrentProvider(),
1057
- knownModels,
1058
- messageCount: session?.messages.length ?? 0
1059
- });
1060
- if (outcome.status === "unchanged") {
1061
- ctx.renderer.printInfo(outcome.message);
1062
- return;
1063
- }
1064
- ctx.setProvider(ctx.getCurrentProvider(), outcome.model);
1065
- if (session) {
1066
- session.updateProvider(ctx.getCurrentProvider(), outcome.model);
1067
- await ctx.sessions.save();
1068
- }
1069
- ctx.renderer.printSuccess(outcome.message);
1070
- if (outcome.warning) console.log(theme.warning(` ${outcome.warning}`));
1071
- }
1072
- },
1073
- {
1074
- name: "route",
1075
- description: "Smart model routing \u2014 enable/disable or inspect routing rules",
1076
- usage: "/route [on|off|show|test <message>]",
1077
- async execute(args, ctx) {
1078
- const intent = parseRouteCommand(args);
1079
- const routing = ctx.config.get("routing");
1080
- if (intent.kind === "enable") {
1081
- ctx.config.setByPath("routing.enabled", "true");
1082
- ctx.renderer.printSuccess("Smart model routing enabled.");
1083
- const note = routingEmptyRulesNote(routing);
1084
- if (note) ctx.renderer.printInfo(note);
1085
- return;
1086
- }
1087
- if (intent.kind === "disable") {
1088
- ctx.config.setByPath("routing.enabled", "false");
1089
- ctx.renderer.printSuccess("Smart model routing disabled.");
1090
- return;
1091
- }
1092
- if (intent.kind === "usage" || intent.kind === "unknown") {
1093
- console.log(theme.warning(` ${intent.message}`));
1094
- return;
1095
- }
1096
- if (intent.kind === "test") {
1097
- const decision = ctx.computeRoutingDecision(intent.message);
1098
- console.log();
1099
- for (const row of routeDecisionRows(intent.message, ctx.getCurrentModel(), decision)) {
1100
- const pad = `${row.label}:`.padEnd(10);
1101
- const value = row.label === "Input" || row.label === "Reason" || row.label === "Rule" ? theme.dim(row.value) : theme.info(row.value);
1102
- console.log(` ${pad}${value}`);
1103
- }
1104
- console.log();
1105
- return;
1106
- }
1107
- const view = describeRouting({
1108
- routing,
1109
- provider: ctx.getCurrentProvider(),
1110
- currentModel: ctx.getCurrentModel()
1111
- });
1112
- console.log();
1113
- console.log(` ${theme.heading("Smart Model Routing")}`);
1114
- console.log(` Status: ${view.enabled ? theme.success("enabled") : theme.dim("disabled")}`);
1115
- console.log(` Provider: ${theme.info(view.provider)}`);
1116
- console.log(` Current: ${theme.info(view.currentModel)}`);
1117
- if (view.fallback) console.log(` Fallback: ${theme.info(view.fallback)}`);
1118
- console.log();
1119
- if (view.noRulesNote) {
1120
- console.log(` ${theme.dim(view.noRulesNote)}`);
1121
- } else {
1122
- console.log(` ${theme.heading("Rules")} ${theme.dim(`(evaluated top-to-bottom)`)}:`);
1123
- for (const rule of view.rules) {
1124
- const cond = rule.empty ? theme.warning(rule.condition) : rule.condition;
1125
- console.log(
1126
- ` ${theme.dim(`#${rule.index}`)} ${rule.name ? theme.accent(rule.name) + " " : ""}${cond} ${theme.dim("\u2192")} ${theme.info(rule.model)}`
1127
- );
1128
- }
1129
- }
1130
- console.log();
1131
- console.log(` ${theme.dim(view.hint)}`);
1132
- console.log();
1133
- }
1134
- },
1135
- {
1136
- name: "think",
1137
- description: "Toggle Extended Thinking mode (deep reasoning for supported models)",
1138
- usage: "/think [on|off|low|high|max|status]",
1139
- execute(args, ctx) {
1140
- const outcome = applyThinkCommand(args[0], {
1141
- runtimeThinking: ctx.getThinkingMode(),
1142
- runtimeEffort: ctx.getThinkingEffort(),
1143
- configuredThinking: ctx.getConfiguredThinking(),
1144
- configuredEffort: ctx.getConfiguredThinkingEffort(),
1145
- supportsEffort: ctx.getProviderSupportsReasoningEffort(),
1146
- provider: ctx.getCurrentProvider()
1147
- });
1148
- if (outcome.changed) {
1149
- ctx.setThinkingMode(outcome.runtimeThinking);
1150
- ctx.setThinkingEffort(outcome.runtimeEffort);
1151
- ctx.renderer.printSuccess(outcome.effective ? `${outcome.summary} \u{1F4AD}` : outcome.summary);
1152
- } else if (outcome.summary.startsWith("Unknown")) {
1153
- console.log(theme.warning(` ${outcome.summary}`));
1154
- } else {
1155
- console.log(` ${outcome.effective ? theme.accent(outcome.summary) : theme.dim(outcome.summary)}`);
1156
- }
1157
- for (const line of outcome.details) console.log(theme.dim(` ${line}`));
1158
- console.log();
1159
- }
1160
- },
1161
- {
1162
- name: "plan",
1163
- description: "Enter Plan Mode (read-only): AI can only read files and plan, not modify anything",
1164
- usage: "/plan [execute|exit|status]",
1165
- execute(args, ctx) {
1166
- const outcome = applyPlanCommand(args[0], ctx.getPlanMode());
1167
- if (outcome.changed) ctx.setPlanMode(outcome.planMode);
1168
- switch (outcome.status) {
1169
- case "entered":
1170
- console.log();
1171
- console.log(theme.warning(` ${outcome.summary}`));
1172
- console.log(theme.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1173
- for (const line of outcome.details) console.log(theme.dim(` ${line}`));
1174
- console.log();
1175
- return;
1176
- case "executing":
1177
- ctx.renderer.printSuccess(outcome.summary);
1178
- return;
1179
- case "already-active":
1180
- case "not-active":
1181
- case "cancelled":
1182
- ctx.renderer.printInfo(outcome.summary);
1183
- return;
1184
- case "status":
1185
- console.log();
1186
- console.log(` ${outcome.planMode ? theme.warning(outcome.summary) : theme.dim(outcome.summary)}`);
1187
- for (const line of outcome.details) console.log(theme.dim(` ${line}`));
1188
- console.log();
1189
- return;
1190
- default:
1191
- ctx.renderer.renderError(outcome.summary);
1192
- }
1193
- }
1194
- }
1195
- ];
1196
- }
1197
-
1198
- // src/repl/commands/tooling-commands.ts
1199
- function createToolingCommands() {
1200
- return [
1201
- {
1202
- name: "tools",
1203
- description: "List all AI tools, grouped by source",
1204
- usage: "/tools [search]",
1205
- execute(args, ctx) {
1206
- const filter = args.join(" ").trim().toLowerCase();
1207
- const all = ctx.tools.listAll();
1208
- const builtin = [];
1209
- const byServer = /* @__PURE__ */ new Map();
1210
- for (const t of all) {
1211
- const name = t.definition.name;
1212
- if (name.startsWith("mcp__")) {
1213
- const rest = name.slice(5);
1214
- const sep = rest.indexOf("__");
1215
- const serverId = sep > 0 ? rest.slice(0, sep) : "(unknown)";
1216
- const toolName = sep > 0 ? rest.slice(sep + 2) : rest;
1217
- if (!byServer.has(serverId)) byServer.set(serverId, []);
1218
- byServer.get(serverId).push({ name: toolName, description: t.definition.description });
1219
- } else {
1220
- builtin.push({ name, description: t.definition.description });
1221
- }
1222
- }
1223
- const matches = (row) => !filter || row.name.toLowerCase().includes(filter) || row.description.toLowerCase().includes(filter);
1224
- const builtinShown = builtin.filter(matches);
1225
- const serverShown = [...byServer.entries()].map(([id, rows]) => [id, rows.filter(matches)]).filter(([, rows]) => rows.length > 0);
1226
- const total = builtinShown.length + serverShown.reduce((a, [, r]) => a + r.length, 0);
1227
- if (total === 0) {
1228
- console.log(filter ? `
1229
- No tools match "${filter}".
1230
- ` : "\nNo tools registered.\n");
1231
- return;
1232
- }
1233
- console.log();
1234
- if (filter) console.log(theme.dim(` Filtering by "${filter}"
1235
- `));
1236
- const allShownRows = [...builtinShown, ...serverShown.flatMap(([, r]) => r)];
1237
- const nameWidth = Math.min(32, Math.max(...allShownRows.map((r) => r.name.length)) + 2);
1238
- if (builtinShown.length > 0) {
1239
- console.log(theme.heading(` Built-in (${builtinShown.length})`));
1240
- for (const t of builtinShown) {
1241
- console.log(` ${theme.accent(t.name.padEnd(nameWidth))} ${theme.dim(t.description)}`);
1242
- }
1243
- console.log();
1244
- }
1245
- for (const [serverId, rows] of serverShown) {
1246
- console.log(theme.heading(` MCP \xB7 ${serverId} (${rows.length})`));
1247
- for (const t of rows) {
1248
- console.log(` ${theme.accent(t.name.padEnd(nameWidth))} ${theme.dim(t.description)}`);
1249
- }
1250
- console.log();
1251
- }
1252
- console.log(theme.dim(` Total: ${total} tool(s) shown
1253
- `));
1254
- }
1255
- },
1256
- {
1257
- name: "plugin",
1258
- description: "Manage plugins (.aicli-plugin/ + legacy JS tools)",
1259
- usage: "/plugin [list|install <path>|enable <name>|disable <name>|inspect <name>|trust <name>]",
1260
- async execute(args, ctx) {
1261
- const sub = args[0] ?? "list";
1262
- const configDir = ctx.config.getConfigDir();
1263
- try {
1264
- if (sub === "list") {
1265
- const plugins = listInstalledPlugins(configDir);
1266
- const pluginTools = ctx.tools.listPluginTools();
1267
- console.log(theme.heading("\n\u{1F50C} Plugins\n"));
1268
- if (plugins.length === 0) {
1269
- console.log(theme.dim(" No package plugins installed."));
1270
- console.log(theme.dim(` Dir: ${pluginRoot(configDir)}`));
1271
- console.log(theme.dim(" Use: /plugin install <path-to-plugin-dir>"));
1272
- } else {
1273
- console.log(theme.dim(` Package dir: ${pluginRoot(configDir)}`));
1274
- for (const p of plugins) {
1275
- const status = `${p.enabled ? theme.success("enabled") : theme.dim("disabled")} / ${p.trusted ? theme.success("trusted") : theme.warning("untrusted")}`;
1276
- console.log(` ${theme.accent(p.name.padEnd(22))} ${p.manifest.version.padEnd(10)} ${status}`);
1277
- if (p.manifest.description) console.log(theme.dim(` ${p.manifest.description}`));
1278
- if (!p.valid && p.error) console.log(theme.warning(` invalid: ${p.error}`));
1279
- }
1280
- }
1281
- if (pluginTools.length > 0) {
1282
- console.log();
1283
- const legacyDir = ctx.config.getPluginsDir();
1284
- console.log(theme.dim(` Legacy JS dir: ${legacyDir}`));
1285
- console.log(` Legacy JS tools (${pluginTools.length}):`);
1286
- for (const t of pluginTools) {
1287
- console.log(` ${theme.accent(t.definition.name.padEnd(22))}${theme.dim(t.definition.description)}`);
1288
- }
1289
- }
1290
- console.log();
1291
- return;
1292
- }
1293
- if (sub === "install") {
1294
- const input = args[1];
1295
- if (!input) {
1296
- ctx.renderer.printInfo("Usage: /plugin install <path-to-plugin-dir>");
1297
- return;
1298
- }
1299
- const plugin = installPlugin(configDir, input);
1300
- ctx.renderer.printSuccess(`Installed plugin ${plugin.name}@${plugin.manifest.version}. Run /plugin trust ${plugin.name}, then /plugin enable ${plugin.name}.`);
1301
- return;
1302
- }
1303
- if (sub === "trust") {
1304
- const name = args[1];
1305
- if (!name) {
1306
- ctx.renderer.printInfo("Usage: /plugin trust <name>");
1307
- return;
1308
- }
1309
- const state = trustPlugin(configDir, name);
1310
- ctx.renderer.printSuccess(`Trusted plugin ${state.name}.`);
1311
- return;
1312
- }
1313
- if (sub === "enable" || sub === "disable") {
1314
- const name = args[1];
1315
- if (!name) {
1316
- ctx.renderer.printInfo(`Usage: /plugin ${sub} <name>`);
1317
- return;
1318
- }
1319
- const state = setPluginEnabled(configDir, name, sub === "enable");
1320
- ctx.renderer.printSuccess(`${sub === "enable" ? "Enabled" : "Disabled"} plugin ${state.name}. Restart aicli or reload affected managers for changes to take effect.`);
1321
- return;
1322
- }
1323
- if (sub === "inspect") {
1324
- const name = args[1];
1325
- if (!name) {
1326
- ctx.renderer.printInfo("Usage: /plugin inspect <name>");
1327
- return;
1328
- }
1329
- const plugin = getInstalledPlugin(configDir, name);
1330
- console.log();
1331
- for (const line of describePlugin(plugin)) console.log(line);
1332
- console.log();
1333
- return;
1334
- }
1335
- if (sub === "active") {
1336
- const assets = getActivePluginAssets(configDir);
1337
- console.log(JSON.stringify({
1338
- plugins: assets.plugins.map((p) => p.name),
1339
- skillDirs: assets.skillDirs,
1340
- commandDirs: assets.commandDirs,
1341
- agentDirs: assets.agentDirs,
1342
- mcpServers: Object.keys(assets.mcpServers)
1343
- }, null, 2));
1344
- return;
1345
- }
1346
- ctx.renderer.printInfo("Usage: /plugin [list|install|enable|disable|inspect|trust]");
1347
- } catch (err) {
1348
- ctx.renderer.renderError(err instanceof Error ? err.message : String(err));
1349
- }
1350
- }
1351
- },
1352
- {
1353
- name: "mcp",
1354
- description: "Show MCP server connections and tools",
1355
- usage: "/mcp [reconnect [serverId] | trust-project]",
1356
- async execute(args, ctx) {
1357
- if (args[0] === "trust-project") {
1358
- const { join: join6 } = await import("path");
1359
- const { existsSync: existsSync6 } = await import("fs");
1360
- const { getGitRoot: getGitRoot2 } = await import("./git-context-EXOEHQSF.js");
1361
- const { MCP_PROJECT_CONFIG_NAME: MCP_PROJECT_CONFIG_NAME2 } = await import("./constants-3DDHA3LT.js");
1362
- const { approveProject, hashMcpFile } = await import("./project-trust-NKYHL3VZ.js");
1363
- const cwd = process.cwd();
1364
- const projectRoot = getGitRoot2(cwd) ?? cwd;
1365
- const mcpPath = join6(projectRoot, MCP_PROJECT_CONFIG_NAME2);
1366
- console.log();
1367
- if (!existsSync6(mcpPath)) {
1368
- console.log(theme.dim(` No .mcp.json in ${projectRoot}.`));
1369
- console.log();
1370
- return;
1371
- }
1372
- approveProject(ctx.config.getConfigDir(), mcpPath);
1373
- console.log(theme.success(` \u2713 Approved ${mcpPath}`));
1374
- console.log(theme.dim(` sha256:${hashMcpFile(mcpPath).slice(0, 16)}\u2026`));
1375
- console.log(theme.dim(` Restart aicli to load these MCP servers.`));
1376
- console.log();
1377
- return;
1378
- }
1379
- const manager = ctx.getMcpManager();
1380
- if (!manager) {
1381
- console.log();
1382
- console.log(theme.dim(" No MCP servers configured."));
1383
- console.log(theme.dim(' Add "mcpServers" to ~/.aicli/config.json to connect MCP servers.'));
1384
- console.log(theme.dim(" Example:"));
1385
- console.log(theme.dim(' "mcpServers": {'));
1386
- console.log(theme.dim(' "filesystem": {'));
1387
- console.log(theme.dim(' "command": "npx",'));
1388
- console.log(theme.dim(' "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path"]'));
1389
- console.log(theme.dim(" }"));
1390
- console.log(theme.dim(" }"));
1391
- console.log();
1392
- return;
1393
- }
1394
- if (args[0] === "reconnect") {
1395
- const targetId = args[1]?.trim();
1396
- console.log();
1397
- if (targetId) {
1398
- console.log(theme.accent(` Reconnecting ${targetId}...`));
1399
- const ok = await manager.reconnectServer(targetId);
1400
- if (ok) {
1401
- ctx.tools.unregisterMcpTools();
1402
- for (const tool of manager.getAllTools()) {
1403
- ctx.tools.registerMcpTool(tool);
1404
- }
1405
- console.log(theme.success(` \u2713 ${targetId} reconnected, tools refreshed.`));
1406
- } else {
1407
- console.log(theme.error(` \u2717 ${targetId} reconnect failed.`));
1408
- }
1409
- } else {
1410
- console.log(theme.accent(" Reconnecting all disconnected servers..."));
1411
- const count = await manager.reconnectAll();
1412
- if (count > 0) {
1413
- ctx.tools.unregisterMcpTools();
1414
- for (const tool of manager.getAllTools()) {
1415
- ctx.tools.registerMcpTool(tool);
1416
- }
1417
- console.log(theme.success(` \u2713 ${count} server(s) reconnected, tools refreshed.`));
1418
- } else {
1419
- console.log(theme.dim(" No servers needed reconnection (or all failed)."));
1420
- }
1421
- }
1422
- console.log();
1423
- return;
1424
- }
1425
- const statuses = manager.getStatus();
1426
- console.log();
1427
- console.log(theme.heading(" \u{1F50C} MCP Servers:"));
1428
- console.log();
1429
- if (statuses.length === 0) {
1430
- console.log(theme.dim(" No MCP servers configured."));
1431
- console.log();
1432
- return;
1433
- }
1434
- for (const s of statuses) {
1435
- const statusBadge = s.connected ? theme.success("connected") : theme.error("disconnected");
1436
- const toolCountStr = s.connected ? theme.dim(` \u2014 ${s.toolCount} tool(s)`) : "";
1437
- const errorStr = s.error ? theme.error(`
1438
- Error: ${s.error}`) : "";
1439
- const serverLabel = s.serverName !== s.serverId ? ` (${s.serverName})` : "";
1440
- const source = ctx.getMcpServerSource(s.serverId);
1441
- const sourceTag = source === "project" ? theme.accent(" [project]") : theme.dim(" [global]");
1442
- console.log(` ${theme.accent(s.serverId)}${theme.dim(serverLabel)}${sourceTag} [${statusBadge}]${toolCountStr}${errorStr}`);
1443
- if (s.connected) {
1444
- const mcpTools = ctx.tools.listMcpTools().filter(
1445
- (t) => t.definition.name.startsWith(`mcp__${s.serverId}__`)
1446
- );
1447
- for (const t of mcpTools) {
1448
- const shortName = t.definition.name.replace(`mcp__${s.serverId}__`, "");
1449
- console.log(theme.dim(` - ${shortName}`));
1450
- }
1451
- }
1452
- }
1453
- console.log();
1454
- }
1455
- },
1456
- {
1457
- name: "skill",
1458
- description: "Manage agent skills (reusable prompt packs)",
1459
- usage: "/skill [name|off|list|reload]",
1460
- async execute(args, ctx) {
1461
- const sub = args[0]?.toLowerCase();
1462
- const manager = ctx.getSkillManager();
1463
- if (!manager) {
1464
- ctx.renderer.printInfo("Skill system not initialized.");
1465
- return;
1466
- }
1467
- if (!sub || sub === "list") {
1468
- const skills = manager.listSkills();
1469
- const active = manager.getActive();
1470
- console.log();
1471
- console.log(theme.heading(" \u{1F3AF} Agent Skills"));
1472
- console.log(theme.dim(" " + "\u2500".repeat(50)));
1473
- if (skills.length === 0) {
1474
- console.log(theme.dim(" No skills available."));
1475
- console.log(theme.dim(` Place .md files in: ${manager.getSkillsDir()}`));
1476
- console.log(theme.dim(" Format: YAML frontmatter (name/description/tools) + prompt body"));
1477
- } else {
1478
- for (const s of skills) {
1479
- const isActive = active?.meta.name === s.meta.name;
1480
- const badge = isActive ? theme.success(" \u2713 active") : "";
1481
- const toolInfo = s.meta.tools ? theme.dim(` [${s.meta.tools.join(", ")}]`) : "";
1482
- console.log(
1483
- ` ${theme.accent(s.meta.name.padEnd(20))}${theme.dim(s.meta.description)}${toolInfo}${badge}`
1484
- );
1485
- }
1486
- }
1487
- console.log(theme.dim(" " + "\u2500".repeat(50)));
1488
- console.log(theme.dim(` Directory: ${manager.getSkillsDir()}`));
1489
- console.log();
1490
- return;
1491
- }
1492
- if (sub === "off" || sub === "none" || sub === "deactivate") {
1493
- const prev2 = manager.getActive();
1494
- if (!prev2) {
1495
- ctx.renderer.printInfo("No skill is currently active.");
1496
- return;
1497
- }
1498
- const proceed2 = await promptSkillHistoryAction(ctx, prev2.meta.name, "(none)");
1499
- if (!proceed2) return;
1500
- manager.deactivate();
1501
- ctx.refreshPrompt();
1502
- ctx.renderer.printSuccess("Skill deactivated.");
1503
- return;
1504
- }
1505
- if (sub === "reload") {
1506
- const count = manager.loadSkills(getActivePluginAssets(ctx.config.getConfigDir()).skillDirs);
1507
- ctx.renderer.printSuccess(`Reloaded: ${count} skill(s) found.`);
1508
- return;
1509
- }
1510
- const target = manager.listSkills().find((s) => s.meta.name === sub);
1511
- if (!target) {
1512
- ctx.renderer.renderError(`Skill '${sub}' not found. Use /skill list to see available skills.`);
1513
- return;
1514
- }
1515
- const prev = manager.getActive();
1516
- if (prev?.meta.name === sub) {
1517
- ctx.renderer.printInfo(`Skill '${sub}' is already active.`);
1518
- return;
1519
- }
1520
- const proceed = await promptSkillHistoryAction(ctx, prev?.meta.name ?? "(none)", sub);
1521
- if (!proceed) return;
1522
- const skill = manager.activate(sub);
1523
- ctx.refreshPrompt();
1524
- ctx.renderer.printSuccess(`Skill activated: ${skill.meta.name} \u2014 ${skill.meta.description}`);
1525
- if (skill.meta.tools) {
1526
- ctx.renderer.printInfo(`Tool whitelist: ${skill.meta.tools.join(", ")}`);
1527
- }
1528
- }
1529
- },
1530
- {
1531
- name: "commands",
1532
- description: "List or reload custom commands from ~/.aicli/commands/",
1533
- usage: "/commands [reload]",
1534
- execute(args, ctx) {
1535
- const mgr = ctx.getCustomCommandManager();
1536
- if (!mgr) {
1537
- console.log(theme.dim(" Custom commands not initialized."));
1538
- return;
1539
- }
1540
- const sub = args[0]?.toLowerCase();
1541
- if (sub === "reload") {
1542
- const count = mgr.loadCommands(getActivePluginAssets(ctx.config.getConfigDir()).commandDirs);
1543
- console.log(theme.success(` \u2713 Reloaded ${count} custom command(s).`));
1544
- return;
1545
- }
1546
- const cmds = mgr.listCommands();
1547
- if (cmds.length === 0) {
1548
- console.log(theme.dim(" No custom commands found in ~/.aicli/commands/"));
1549
- console.log(theme.dim(" Create .md files with YAML frontmatter (name/description/usage)."));
1550
- return;
1551
- }
1552
- console.log(theme.dim(` Custom commands (${cmds.length}):`));
1553
- for (const cmd of cmds) {
1554
- console.log(theme.accent(` /${cmd.meta.name.padEnd(18)} `) + theme.dim(cmd.meta.description));
1555
- }
1556
- console.log();
1557
- }
1558
- }
1559
- ];
1560
- }
1561
-
1562
- // src/repl/commands/security-commands.ts
1563
- import { platform as platform2 } from "os";
1564
- import chalk2 from "chalk";
1565
- function createSecurityCommands() {
1566
- return [
1567
- {
1568
- name: "config",
1569
- description: "Configuration: wizard, get/set values, show all",
1570
- usage: "/config [set <path> <value> | get <path> | show]",
1571
- async execute(args, ctx) {
1572
- const outcome = applyConfigCommand(args, ctx.config);
1573
- if (outcome.action === "wizard") {
1574
- await ctx.runSetupWizard();
1575
- return;
1576
- }
1577
- if (outcome.action === "show") {
1578
- console.log(outcome.json);
1579
- return;
1580
- }
1581
- if (outcome.isError) {
1582
- console.log(theme.warning(` ${outcome.summary}`));
1583
- return;
1584
- }
1585
- if (outcome.changed) {
1586
- console.log(theme.success(` \u2713 ${outcome.summary}`));
1587
- return;
1588
- }
1589
- console.log(theme.dim(` ${outcome.summary}`));
1590
- }
1591
- },
1592
- {
1593
- name: "profile",
1594
- description: 'View or edit your identity profile (the "soul" of AI)',
1595
- usage: "/profile [show|set <field> <value>|clear|edit]",
1596
- execute(args, ctx) {
1597
- const outcome = applyProfileCommand(args, ctx.config.get("userProfile") ?? {});
1598
- if (outcome.profile) {
1599
- ctx.config.set("userProfile", outcome.profile);
1600
- ctx.config.save();
1601
- }
1602
- if (outcome.isError) {
1603
- ctx.renderer.printInfo(outcome.summary);
1604
- } else if (outcome.changed) {
1605
- ctx.renderer.printSuccess(outcome.summary);
1606
- } else if (outcome.rows.length > 0) {
1607
- console.log(theme.heading(`
1608
- ${outcome.summary}
1609
- `) + theme.dim("\u2500".repeat(50)));
1610
- const pad = Math.max(...outcome.rows.map(([label]) => label.length)) + 1;
1611
- for (const [label, value] of outcome.rows) {
1612
- console.log(` ${theme.dim((label + ":").padEnd(pad + 1))} ${chalk2.white(value)}`);
1613
- }
1614
- console.log(theme.dim("\u2500".repeat(50)));
1615
- } else {
1616
- console.log(theme.dim(`
1617
- ${outcome.summary}`));
1618
- }
1619
- for (const line of outcome.details) console.log(theme.dim(` ${line}`));
1620
- console.log();
1621
- }
1622
- },
1623
- // ── /memory ────────────────────────────────────────────────────────────────
1624
- {
1625
- name: "doctor",
1626
- description: "Health check: context, permissions, hooks, plugins, MCP, config",
1627
- usage: "/doctor",
1628
- execute(_args, ctx) {
1629
- const report = buildDoctorReport({
1630
- config: ctx.config,
1631
- providers: ctx.providers,
1632
- cwd: process.cwd(),
1633
- mcpStatuses: ctx.getMcpManager()?.getStatus() ?? []
1634
- });
1635
- console.log(formatDoctorReport(report, true));
1636
- }
1637
- },
1638
- {
1639
- name: "bug",
1640
- description: "Generate a bug report template with system info",
1641
- usage: "/bug [--copy]",
1642
- execute(args, ctx) {
1643
- const doCopy = args.includes("--copy");
1644
- const os = platform2();
1645
- const nodeVer = process.version;
1646
- const provider = ctx.getCurrentProvider();
1647
- const model = ctx.getCurrentModel();
1648
- const report = [
1649
- "## Bug Report",
1650
- "",
1651
- `**ai-cli version**: ${VERSION}`,
1652
- `**OS**: ${os}`,
1653
- `**Node.js**: ${nodeVer}`,
1654
- `**Provider**: ${provider}`,
1655
- `**Model**: ${model}`,
1656
- "",
1657
- "### Description",
1658
- "<!-- What happened? -->",
1659
- "",
1660
- "### Steps to Reproduce",
1661
- "1. ",
1662
- "2. ",
1663
- "3. ",
1664
- "",
1665
- "### Expected Behavior",
1666
- "<!-- What should have happened? -->",
1667
- "",
1668
- "### Actual Behavior",
1669
- "<!-- What actually happened? -->",
1670
- "",
1671
- "### Logs / Error Messages",
1672
- "```",
1673
- "<!-- Paste relevant output here -->",
1674
- "```"
1675
- ].join("\n");
1676
- console.log(report);
1677
- if (doCopy) {
1678
- try {
1679
- copyToClipboard(report);
1680
- ctx.renderer.printInfo("Bug report template copied to clipboard.");
1681
- } catch {
1682
- ctx.renderer.renderError("Failed to copy to clipboard.");
1683
- }
1684
- } else {
1685
- ctx.renderer.printInfo(`Submit bug reports at: ${REPO_URL}/issues`);
1686
- }
1687
- }
1688
- },
1689
- {
1690
- name: "hooks",
1691
- description: "Manage lifecycle hooks and project hook trust",
1692
- usage: "/hooks [list|inspect <id>|trust <id>|untrust <id>|disable]",
1693
- execute(args, ctx) {
1694
- const sub = (args[0] || "list").toLowerCase();
1695
- const hooks = listHooks(mergePluginHooks(ctx.config.get("hooks") ?? void 0, ctx.config.getConfigDir()), ctx.config.getConfigDir());
1696
- const findHook = (id) => hooks.find((h) => h.id === id || h.hash.startsWith(id ?? ""));
1697
- if (sub === "list") {
1698
- console.log(theme.heading(" Hooks"));
1699
- if (hooks.length === 0) {
1700
- console.log(theme.dim(" No hooks configured."));
1701
- } else {
1702
- for (const h of hooks) {
1703
- const trust = h.disabled ? theme.dim("disabled") : h.trusted ? theme.success("trusted") : theme.warning("pending");
1704
- console.log(` ${h.id.padEnd(18)} ${h.source.padEnd(7)} ${trust} ${h.hash.slice(0, 12)} ${theme.dim(h.command.slice(0, 80))}`);
1705
- }
1706
- }
1707
- console.log();
1708
- return;
1709
- }
1710
- if (sub === "inspect") {
1711
- const h = findHook(args[1]);
1712
- if (!h) {
1713
- console.log(theme.warning(" Hook not found. Use /hooks list."));
1714
- return;
1715
- }
1716
- console.log(theme.heading(" Hook"));
1717
- console.log(` id : ${h.id}`);
1718
- console.log(` event : ${h.event}`);
1719
- console.log(` source : ${h.source}`);
1720
- console.log(` hash : ${h.hash}`);
1721
- console.log(` trusted : ${h.trusted ? "yes" : "no"}`);
1722
- console.log(` disabled: ${h.disabled ? "yes" : "no"}`);
1723
- console.log(` command : ${h.command}`);
1724
- console.log();
1725
- return;
1726
- }
1727
- if (sub === "trust" || sub === "untrust") {
1728
- const h = findHook(args[1]);
1729
- if (!h) {
1730
- console.log(theme.warning(" Hook not found. Use /hooks list."));
1731
- return;
1732
- }
1733
- if (sub === "trust") {
1734
- trustHook(ctx.config.getConfigDir(), h);
1735
- console.log(theme.success(` \u2713 trusted ${h.id} (${h.hash.slice(0, 12)})`));
1736
- } else {
1737
- untrustHook(ctx.config.getConfigDir(), h);
1738
- console.log(theme.success(` \u2713 untrusted ${h.id}`));
1739
- }
1740
- console.log();
1741
- return;
1742
- }
1743
- if (sub === "disable") {
1744
- const cfg = ctx.config.get("hooks") ?? {};
1745
- ctx.config.set("hooks", { ...cfg, enabled: false });
1746
- console.log(theme.success(" \u2713 hooks disabled in user config"));
1747
- console.log();
1748
- return;
1749
- }
1750
- console.log(theme.warning(" Usage: /hooks [list|inspect <id>|trust <id>|untrust <id>|disable]"));
1751
- console.log();
1752
- }
1753
- },
1754
- {
1755
- name: "security",
1756
- description: "Sensitive-data redaction \u2014 status, scan sessions, toggle on/off",
1757
- usage: "/security [status|scan [--all]|on|off]",
1758
- async execute(args, ctx) {
1759
- const sub = (args[0] || "status").toLowerCase();
1760
- const security = ctx.config.get("security") ?? {
1761
- redactOnSave: true,
1762
- redactOnSend: false,
1763
- mode: "default",
1764
- customPatterns: []
1765
- };
1766
- if (sub === "status") {
1767
- console.log(theme.heading(" \u{1F6E1} Security / Redaction"));
1768
- console.log(` redactOnSave : ${security.redactOnSave ? theme.success("on") : theme.warning("off")}`);
1769
- console.log(` redactOnSend : ${security.redactOnSend ? theme.success("on") : theme.dim("off")}`);
1770
- console.log(` mode : ${security.mode}`);
1771
- console.log(" profile : " + (ctx.config.get("defaultPermissionProfile") ?? "legacy"));
1772
- console.log(" network : " + (ctx.config.get("networkPolicy")?.enabled ?? false ? "on" : "off"));
1773
- console.log(" autoMode : " + (ctx.getToolExecutor().sessionAutoMode ? "on" : "off"));
1774
- console.log(" hooks : " + getPendingHookTrust(mergePluginHooks(ctx.config.get("hooks") ?? void 0, ctx.config.getConfigDir()), ctx.config.getConfigDir()).length + " pending trust");
1775
- console.log(` patterns : ${DEFAULT_PATTERNS.length} built-in + ${security.customPatterns?.length ?? 0} custom`);
1776
- console.log(` last save : ${ctx.sessions.lastRedactionHits} hit(s) redacted`);
1777
- console.log();
1778
- console.log(theme.dim(" /security scan \u2014 scan current session for leaks"));
1779
- console.log(theme.dim(" /security scan --all \u2014 scan all saved sessions"));
1780
- console.log(theme.dim(" /security on|off \u2014 toggle redactOnSave"));
1781
- console.log();
1782
- return;
1783
- }
1784
- if (sub === "on" || sub === "off") {
1785
- ctx.config.set("security", { ...security, redactOnSave: sub === "on" });
1786
- console.log(theme.success(` \u2713 redactOnSave = ${sub}`));
1787
- console.log();
1788
- return;
1789
- }
1790
- if (sub === "scan") {
1791
- const scanAll = args.includes("--all");
1792
- const opts = { customRegexes: security.customPatterns ?? [] };
1793
- let totalHits = 0;
1794
- let filesWithHits = 0;
1795
- if (scanAll) {
1796
- const metas = ctx.sessions.listSessions();
1797
- console.log(theme.info(` Scanning ${metas.length} session(s)\u2026`));
1798
- const { readFileSync: readFileSync6 } = await import("fs");
1799
- const { join: join6 } = await import("path");
1800
- const historyDir = ctx.config.getHistoryDir();
1801
- for (const m of metas) {
1802
- try {
1803
- const content = readFileSync6(join6(historyDir, `${m.id}.json`), "utf-8");
1804
- const hits2 = scanString(content, opts);
1805
- if (hits2.length) {
1806
- filesWithHits++;
1807
- totalHits += hits2.length;
1808
- const kinds = [...new Set(hits2.map((h) => h.kind))].join(", ");
1809
- console.log(` ${theme.warning("\u26A0")} ${m.id.slice(0, 8)}\u2026 ${theme.dim(m.title || "")} \u2014 ${hits2.length} hit(s) [${kinds}]`);
1810
- }
1811
- } catch {
1812
- }
1813
- }
1814
- console.log();
1815
- console.log(
1816
- totalHits === 0 ? theme.success(` \u2713 No secrets found in ${metas.length} session(s).`) : theme.warning(` Found ${totalHits} hit(s) across ${filesWithHits} session(s). Re-save them with redactOnSave=on to sanitize.`)
1817
- );
1818
- console.log();
1819
- return;
1820
- }
1821
- const current = ctx.sessions.current;
1822
- if (!current) {
1823
- console.log(theme.warning(" No active session to scan."));
1824
- console.log();
1825
- return;
1826
- }
1827
- const json = JSON.stringify(current.toJSON());
1828
- const hits = scanString(json, opts);
1829
- if (hits.length === 0) {
1830
- console.log(theme.success(" \u2713 Current session: no secrets detected."));
1831
- } else {
1832
- const byKind = /* @__PURE__ */ new Map();
1833
- for (const h of hits) byKind.set(h.kind, (byKind.get(h.kind) ?? 0) + 1);
1834
- console.log(theme.warning(` \u26A0 Current session: ${hits.length} hit(s)`));
1835
- for (const [k, n] of byKind) console.log(` \u2022 ${k}: ${n}`);
1836
- console.log(theme.dim(" (save the session to replace them with placeholders)"));
1837
- }
1838
- console.log();
1839
- return;
1840
- }
1841
- console.log(theme.error(` Unknown subcommand: ${sub}`));
1842
- console.log(theme.dim(" Usage: /security [status|scan [--all]|on|off]"));
1843
- console.log();
1844
- }
1845
- }
1846
- ];
1847
- }
1848
-
1849
- // src/repl/commands/review-commands.ts
1850
- import { existsSync, readFileSync, writeFileSync } from "fs";
1851
- import { join } from "path";
1852
- function createReviewCommands() {
1853
- return [
1854
- {
1855
- name: "review",
1856
- description: "AI code review based on git diff",
1857
- usage: "/review [--staged] [--detailed]",
1858
- async execute(args, ctx) {
1859
- const gitCtx = getGitContext();
1860
- if (!gitCtx) {
1861
- ctx.renderer.renderError("Not a git repository.");
1862
- return;
1863
- }
1864
- const staged = args.includes("--staged");
1865
- const detailed = args.includes("--detailed");
1866
- let diff;
1867
- try {
1868
- diff = readGitDiff({ staged });
1869
- } catch {
1870
- ctx.renderer.renderError("Failed to run git diff.");
1871
- return;
1872
- }
1873
- if (!diff) {
1874
- console.log(theme.dim(" No changes to review." + (staged ? "" : " Try --staged for staged changes.")));
1875
- return;
1876
- }
1877
- const MAX_DIFF = 8e3;
1878
- let truncated = false;
1879
- if (diff.length > MAX_DIFF) {
1880
- const head = diff.slice(0, Math.floor(MAX_DIFF * 0.7));
1881
- const tail = diff.slice(diff.length - Math.floor(MAX_DIFF * 0.2));
1882
- diff = head + "\n\n... [diff truncated, " + diff.length + " chars total] ...\n\n" + tail;
1883
- truncated = true;
1884
- }
1885
- const prompt = buildReviewPrompt(diff, formatGitContextForPrompt(gitCtx), detailed);
1886
- console.log(theme.dim(" Analyzing changes..."));
1887
- try {
1888
- const review = await ctx.chatOnce(prompt, { temperature: 0.3, maxTokens: 8192 });
1889
- console.log();
1890
- console.log(review);
1891
- console.log();
1892
- if (truncated) {
1893
- console.log(theme.warning(" \u26A0 Diff was truncated. Consider reviewing smaller changesets."));
1894
- }
1895
- } catch (err) {
1896
- ctx.renderer.renderError(`Review failed: ${err.message}`);
1897
- }
1898
- }
1899
- },
1900
- // ── /security-review ──────────────────────────────────────────
1901
- {
1902
- name: "security-review",
1903
- description: "Security vulnerability scan on git diff",
1904
- usage: "/security-review [--staged]",
1905
- async execute(args, ctx) {
1906
- const gitCtx = getGitContext();
1907
- if (!gitCtx) {
1908
- ctx.renderer.renderError("Not a git repository.");
1909
- return;
1910
- }
1911
- const staged = args.includes("--staged");
1912
- let diff;
1913
- try {
1914
- diff = readGitDiff({ staged });
1915
- } catch {
1916
- ctx.renderer.renderError("Failed to run git diff.");
1917
- return;
1918
- }
1919
- if (!diff) {
1920
- console.log(theme.dim(" No changes to review." + (staged ? "" : " Try --staged for staged changes.")));
1921
- return;
1922
- }
1923
- const MAX_DIFF = 8e3;
1924
- let truncated = false;
1925
- if (diff.length > MAX_DIFF) {
1926
- const head = diff.slice(0, Math.floor(MAX_DIFF * 0.7));
1927
- const tail = diff.slice(diff.length - Math.floor(MAX_DIFF * 0.2));
1928
- diff = head + "\n\n... [diff truncated, " + diff.length + " chars total] ...\n\n" + tail;
1929
- truncated = true;
1930
- }
1931
- const prompt = buildSecurityReviewPrompt(diff, formatGitContextForPrompt(gitCtx));
1932
- console.log(theme.dim(" \u{1F512} Scanning for security vulnerabilities..."));
1933
- try {
1934
- const review = await ctx.chatOnce(prompt, { temperature: 0.2, maxTokens: 8192 });
1935
- console.log();
1936
- console.log(review);
1937
- console.log();
1938
- if (truncated) {
1939
- console.log(theme.warning(" \u26A0 Diff was truncated. Consider reviewing smaller changesets."));
1940
- }
1941
- } catch (err) {
1942
- ctx.renderer.renderError(`Security review failed: ${err.message}`);
1943
- }
1944
- }
1945
- },
1946
- // ── /rewind ───────────────────────────────────────────────────
1947
- {
1948
- name: "rewind",
1949
- description: "Rewind conversation to a previous message and restore file states",
1950
- usage: "/rewind [list | <n>]",
1951
- async execute(args, ctx) {
1952
- const session = ctx.sessions.current;
1953
- if (!session || session.messages.length === 0) {
1954
- console.log(theme.dim(" No messages to rewind."));
1955
- return;
1956
- }
1957
- const intent = parseRewindArgs(args, session.messages.length);
1958
- if (intent.kind === "invalid") {
1959
- ctx.renderer.renderError(intent.message);
1960
- return;
1961
- }
1962
- if (intent.kind === "list") {
1963
- const rows = buildRewindList(session.messages, fileCheckpoints.getMessageIndices());
1964
- console.log(theme.heading(`
1965
- Conversation messages (${rows.length} total):
1966
- `));
1967
- for (const row of rows) {
1968
- console.log(
1969
- theme.dim(` [${String(row.index).padStart(3)}]`) + ` ${theme.info(row.role.padEnd(10))} ${row.preview}${row.hasCheckpoint ? " \u{1F4CC}" : ""}`
1970
- );
1971
- }
1972
- console.log();
1973
- console.log(theme.dim(" Usage: /rewind <n> \u2014 rewind to message N (1-based)"));
1974
- console.log(theme.dim(" \u{1F4CC} = file checkpoint exists at this point"));
1975
- console.log();
1976
- return;
1977
- }
1978
- const messagesRemoved = session.messages.length - intent.target;
1979
- const { restored, deleted, files } = fileCheckpoints.restoreToMessageIndex(intent.target);
1980
- session.truncateTo(intent.target);
1981
- await ctx.sessions.save();
1982
- ctx.resetSessionTokenUsage();
1983
- const lines = formatRewindReport({
1984
- target: intent.target,
1985
- messagesRemoved,
1986
- filesRestored: restored,
1987
- filesDeleted: deleted,
1988
- files
1989
- });
1990
- console.log(theme.success(`
1991
- ${lines[0]}`));
1992
- for (const line of lines.slice(1)) console.log(theme.dim(` ${line}`));
1993
- console.log();
1994
- }
1995
- },
1996
- // ── /branch ───────────────────────────────────────────────────
1997
- {
1998
- name: "branch",
1999
- description: "Manage conversation branches (fork/switch/list/delete/rename)",
2000
- usage: "/branch [list | new <msgIndex> [title] | switch <id|title> | delete <id|title> | rename <id|title> <new> | diff <id|title> | cherry-pick <id|title> <msgIndex>]",
2001
- async execute(args, ctx) {
2002
- const session = ctx.sessions.current;
2003
- if (!session) {
2004
- ctx.renderer.renderError("No active session.");
2005
- return;
2006
- }
2007
- const sub = args[0]?.toLowerCase();
2008
- const resolve4 = (ref) => {
2009
- const r = session.resolveBranchRef(ref);
2010
- if (r.ok) return r.id;
2011
- if (r.reason === "ambiguous") {
2012
- const list = r.matches.map((m) => `${m.id} (${m.title})`).join(", ");
2013
- ctx.renderer.renderError(`Ambiguous branch reference "${ref}". Matches: ${list}. Use the 6-char id.`);
2014
- } else {
2015
- ctx.renderer.renderError(`Branch "${ref}" not found. Try /branch list to see all branches.`);
2016
- }
2017
- return null;
2018
- };
2019
- if (!sub || sub === "list") {
2020
- const branches = session.listBranches();
2021
- console.log(theme.heading(`
2022
- Branches (${branches.length}):
2023
- `));
2024
- for (const b of branches) {
2025
- const active = b.id === session.activeBranchId ? theme.success("\u25CF ") : " ";
2026
- const parent = b.parentBranchId ? theme.dim(` \u2190 ${b.parentBranchId}@${b.parentMessageIndex}`) : "";
2027
- const count = b.id === session.activeBranchId ? session.messages.length : session.getBranchMessages(b.id)?.length ?? 0;
2028
- console.log(
2029
- ` ${active}${theme.accent(b.id.padEnd(10))} ${b.title.padEnd(20)} ` + theme.dim(`(${count} msgs)`) + parent
2030
- );
2031
- }
2032
- console.log();
2033
- console.log(theme.dim(" /branch new <msgIndex> [title] \u2014 fork active branch at message N"));
2034
- console.log(theme.dim(" /branch switch <id|title> \u2014 switch active branch"));
2035
- console.log(theme.dim(" /branch delete <id|title> \u2014 delete inactive branch"));
2036
- console.log(theme.dim(" /branch rename <id|title> <new> \u2014 rename a branch"));
2037
- console.log(theme.dim(" /branch diff <id|title> \u2014 compare branch with active, list divergent messages"));
2038
- console.log(theme.dim(" /branch cherry-pick <id|title> <idx> \u2014 copy one message from branch into active"));
2039
- console.log(theme.dim(" (id, title, or unique id-prefix all work)"));
2040
- console.log();
2041
- return;
2042
- }
2043
- if (sub === "new") {
2044
- const idxArg = args[1];
2045
- if (!idxArg) {
2046
- ctx.renderer.renderError("Usage: /branch new <msgIndex> [title]");
2047
- return;
2048
- }
2049
- const fromIndex = parseInt(idxArg, 10);
2050
- if (isNaN(fromIndex) || fromIndex < 0 || fromIndex > session.messages.length) {
2051
- ctx.renderer.renderError(`Invalid msgIndex: ${idxArg}. Range: 0-${session.messages.length}`);
2052
- return;
2053
- }
2054
- const title = args.slice(2).join(" ").trim() || void 0;
2055
- try {
2056
- const newId = session.createBranch(fromIndex, title);
2057
- await ctx.sessions.save();
2058
- console.log(theme.success(`
2059
- \u2713 Created branch "${newId}" from message #${fromIndex}`));
2060
- console.log(theme.dim(` Now on branch "${newId}" (${session.messages.length} messages)
2061
- `));
2062
- } catch (err) {
2063
- ctx.renderer.renderError(err.message);
2064
- }
2065
- return;
2066
- }
2067
- if (sub === "switch") {
2068
- const ref = args[1];
2069
- if (!ref) {
2070
- ctx.renderer.renderError("Usage: /branch switch <id|title>");
2071
- return;
2072
- }
2073
- const id = resolve4(ref);
2074
- if (!id) return;
2075
- const ok = session.switchBranch(id);
2076
- if (ok) {
2077
- await ctx.sessions.save();
2078
- const b = session.getActiveBranch();
2079
- console.log(theme.success(`
2080
- \u2713 Switched to branch "${b.id}" \u2014 ${b.title}`));
2081
- console.log(theme.dim(` ${session.messages.length} messages on this branch
2082
- `));
2083
- } else {
2084
- ctx.renderer.renderError(`Cannot switch to "${id}" (already active).`);
2085
- }
2086
- return;
2087
- }
2088
- if (sub === "delete") {
2089
- const ref = args[1];
2090
- if (!ref) {
2091
- ctx.renderer.renderError("Usage: /branch delete <id|title>");
2092
- return;
2093
- }
2094
- const id = resolve4(ref);
2095
- if (!id) return;
2096
- const ok = session.deleteBranch(id);
2097
- if (ok) {
2098
- await ctx.sessions.save();
2099
- console.log(theme.success(`
2100
- \u2713 Deleted branch "${id}"
2101
- `));
2102
- } else {
2103
- ctx.renderer.renderError(
2104
- `Cannot delete "${id}" (active, or last remaining branch).`
2105
- );
2106
- }
2107
- return;
2108
- }
2109
- if (sub === "rename") {
2110
- const ref = args[1];
2111
- const title = args.slice(2).join(" ").trim();
2112
- if (!ref || !title) {
2113
- ctx.renderer.renderError("Usage: /branch rename <id|title> <new title>");
2114
- return;
2115
- }
2116
- const id = resolve4(ref);
2117
- if (!id) return;
2118
- const ok = session.renameBranch(id, title);
2119
- if (ok) {
2120
- await ctx.sessions.save();
2121
- console.log(theme.success(`
2122
- \u2713 Renamed branch "${id}" \u2192 "${title}"
2123
- `));
2124
- } else {
2125
- ctx.renderer.renderError(`Branch "${id}" not found.`);
2126
- }
2127
- return;
2128
- }
2129
- if (sub === "diff") {
2130
- const ref = args[1];
2131
- if (!ref) {
2132
- ctx.renderer.renderError("Usage: /branch diff <id|title> \u2014 diff the given branch against the active branch");
2133
- return;
2134
- }
2135
- const id = resolve4(ref);
2136
- if (!id) return;
2137
- const d = session.diffBranches(id);
2138
- if (!d) {
2139
- ctx.renderer.renderError(`Branch "${id}" not found.`);
2140
- return;
2141
- }
2142
- const active = session.activeBranchId;
2143
- console.log(`
2144
- ${theme.info("diff")} ${id} (source) \u2194 ${active} (active)`);
2145
- console.log(theme.dim(` Common prefix: ${d.commonPrefix} message(s)`));
2146
- if (d.sourceOnly.length === 0 && d.targetOnly.length === 0) {
2147
- console.log(theme.success(" Branches are identical.\n"));
2148
- return;
2149
- }
2150
- const preview = (m) => {
2151
- const text = typeof m.content === "string" ? m.content : "[complex content]";
2152
- return text.replace(/\s+/g, " ").trim().slice(0, 70);
2153
- };
2154
- if (d.sourceOnly.length > 0) {
2155
- console.log(theme.dim(`
2156
- Only in ${id} (${d.sourceOnly.length}):`));
2157
- d.sourceOnly.forEach((m, i) => {
2158
- console.log(` ${theme.dim(`[${d.commonPrefix + i}]`)} ${m.role}: ${preview(m)}`);
2159
- });
2160
- }
2161
- if (d.targetOnly.length > 0) {
2162
- console.log(theme.dim(`
2163
- Only in ${active} (active) (${d.targetOnly.length}):`));
2164
- d.targetOnly.forEach((m, i) => {
2165
- console.log(` ${theme.dim(`[${d.commonPrefix + i}]`)} ${m.role}: ${preview(m)}`);
2166
- });
2167
- }
2168
- console.log(theme.dim(`
2169
- Use /branch cherry-pick ${id} <index> to copy a message into the active branch.
2170
- `));
2171
- return;
2172
- }
2173
- if (sub === "cherry-pick") {
2174
- const ref = args[1];
2175
- const idxStr = args[2];
2176
- if (!ref || !idxStr) {
2177
- ctx.renderer.renderError("Usage: /branch cherry-pick <source-id|title> <msg-index>");
2178
- return;
2179
- }
2180
- const id = resolve4(ref);
2181
- if (!id) return;
2182
- const idx = parseInt(idxStr, 10);
2183
- if (Number.isNaN(idx)) {
2184
- ctx.renderer.renderError(`Invalid message index: "${idxStr}"`);
2185
- return;
2186
- }
2187
- const picked = session.cherryPickMessage(id, idx);
2188
- if (!picked) {
2189
- ctx.renderer.renderError(`Cherry-pick failed \u2014 index ${idx} out of range for branch "${id}".`);
2190
- return;
2191
- }
2192
- await ctx.sessions.save();
2193
- const preview = typeof picked.content === "string" ? picked.content.replace(/\s+/g, " ").trim().slice(0, 60) : "[complex content]";
2194
- console.log(theme.success(`
2195
- \u2713 Cherry-picked ${picked.role} message from "${id}"[${idx}] \u2192 active branch (now ${session.messages.length} msgs)`));
2196
- console.log(theme.dim(` ${preview}
2197
- `));
2198
- return;
2199
- }
2200
- ctx.renderer.renderError(`Unknown subcommand: ${sub}. Use list/new/switch/delete/rename/diff/cherry-pick.`);
2201
- }
2202
- },
2203
- // ── /index ────────────────────────────────────────────────────
2204
- {
2205
- name: "init",
2206
- description: "Generate project context file (AICLI.md) by scanning project structure",
2207
- usage: "/init [--force]",
2208
- async execute(args, ctx) {
2209
- const cwd = process.cwd();
2210
- const targetPath = join(cwd, "AICLI.md");
2211
- const force = args.includes("--force");
2212
- if (existsSync(targetPath) && !force) {
2213
- ctx.renderer.printInfo(`AICLI.md already exists at ${targetPath}`);
2214
- ctx.renderer.printInfo("Use /init --force to overwrite, or edit it manually.");
2215
- return;
2216
- }
2217
- ctx.renderer.printInfo("Scanning project structure...");
2218
- const projectInfo = scanProject(cwd);
2219
- if (projectInfo.type === "unknown" && projectInfo.configFiles.length === 0) {
2220
- ctx.renderer.printInfo("Could not detect project type. Generating generic template...");
2221
- }
2222
- const prompt = buildInitPrompt(projectInfo, cwd);
2223
- console.log(theme.dim(" Calling AI to generate AICLI.md..."));
2224
- try {
2225
- const content = await ctx.chatOnce(prompt, { temperature: 0.3, maxTokens: 4096 });
2226
- writeFileSync(targetPath, content, "utf-8");
2227
- ctx.renderer.printSuccess(`Generated: ${targetPath} (${content.length} chars)`);
2228
- ctx.renderer.printInfo("Review and edit as needed. Use /context reload to load it.");
2229
- } catch (err) {
2230
- ctx.renderer.renderError(`Failed to generate AICLI.md: ${err instanceof Error ? err.message : String(err)}`);
2231
- }
2232
- }
2233
- },
2234
- {
2235
- name: "scaffold",
2236
- description: "Generate project scaffolding with AI",
2237
- usage: "/scaffold <description>",
2238
- async execute(args, ctx) {
2239
- const description = args.join(" ").trim();
2240
- if (!description) {
2241
- console.log(theme.warning("Usage: /scaffold <project description>"));
2242
- console.log(theme.dim(" Example: /scaffold Spring Boot REST API with PostgreSQL for exam system"));
2243
- console.log(theme.dim(" Example: /scaffold React + TypeScript dashboard with authentication"));
2244
- return;
2245
- }
2246
- const cwd = process.cwd();
2247
- const prompt = [
2248
- "Please generate a complete project scaffold based on the following description:",
2249
- "",
2250
- `Project description: ${description}`,
2251
- `Working directory: ${cwd}`,
2252
- "",
2253
- "Requirements:",
2254
- "1. Create a complete directory structure and core files",
2255
- "2. Include dependency management files (pom.xml / package.json / requirements.txt, etc.)",
2256
- "3. Include basic configuration files (database connection, app config, etc.)",
2257
- "4. Include example code (main entry point, example Controller/Service/Repository)",
2258
- "5. Include README.md explaining how to build and run",
2259
- "6. Follow industry best practices and a reasonable project structure",
2260
- "",
2261
- "Please use tools to create files step by step. First use list_dir to check if the directory is empty, then create the project structure."
2262
- ].join("\n");
2263
- console.log(theme.accent(`
2264
- Scaffolding: ${description}`));
2265
- console.log(theme.dim(" AI will create the project structure using available tools...\n"));
2266
- await ctx.sendAsChat(prompt);
2267
- }
2268
- },
2269
- // ── /add-dir ───────────────────────────────────────────────────────────────
2270
- {
2271
- name: "test",
2272
- description: "Run project tests and show structured report",
2273
- usage: "/test [command|filter]",
2274
- async execute(args, ctx) {
2275
- try {
2276
- const { executeTests } = await import("./run-tests-DPO23PKA.js");
2277
- const argStr = args.join(" ").trim();
2278
- let testArgs = {};
2279
- if (argStr) {
2280
- const isCommand = argStr.includes(" ") || /^(mvn|gradle|npm|pytest|cargo|go)\b/.test(argStr);
2281
- testArgs = isCommand ? { command: argStr } : { filter: argStr };
2282
- }
2283
- const report = await executeTests(testArgs);
2284
- const firstLines = report.split("\n").slice(0, 3).join("\n");
2285
- console.log(theme.dim(firstLines));
2286
- } catch (err) {
2287
- ctx.renderer.renderError(`Test execution failed: ${err instanceof Error ? err.message : String(err)}`);
2288
- }
2289
- }
2290
- },
2291
- {
2292
- name: "diff",
2293
- description: "Show all file modifications in this session",
2294
- usage: "/diff [--stats]",
2295
- execute(args) {
2296
- const history = undoStack.getHistory();
2297
- if (history.length === 0) {
2298
- console.log(theme.dim("No file modifications in this session."));
2299
- return;
2300
- }
2301
- const fileMap = /* @__PURE__ */ new Map();
2302
- for (const entry of history) {
2303
- if (!fileMap.has(entry.filePath)) {
2304
- fileMap.set(entry.filePath, { earliest: entry.previousContent });
2305
- }
2306
- }
2307
- const statsOnly = args[0]?.toLowerCase() === "--stats";
2308
- let newFiles = 0;
2309
- let modifiedFiles = 0;
2310
- for (const [filePath, { earliest }] of fileMap) {
2311
- const currentContent = existsSync(filePath) ? (() => {
2312
- try {
2313
- return readFileSync(filePath, "utf-8");
2314
- } catch {
2315
- return null;
2316
- }
2317
- })() : null;
2318
- const isNew = earliest === null;
2319
- if (isNew) newFiles++;
2320
- else modifiedFiles++;
2321
- if (statsOnly) continue;
2322
- if (isNew) {
2323
- console.log(theme.success(`
2324
- \u{1F4C4} [NEW] ${filePath}`));
2325
- if (currentContent !== null) {
2326
- const lines = currentContent.split("\n").length;
2327
- console.log(theme.dim(` ${lines} lines`));
2328
- } else {
2329
- console.log(theme.dim(" (file was deleted after creation)"));
2330
- }
2331
- } else {
2332
- console.log(theme.accent(`
2333
- \u{1F4DD} [MODIFIED] ${filePath}`));
2334
- const oldText = earliest ?? "";
2335
- const newText = currentContent ?? "";
2336
- if (oldText === newText) {
2337
- console.log(theme.dim(" (no net change)"));
2338
- } else {
2339
- console.log(renderDiff(oldText, newText, { filePath, maxLines: 80 }));
2340
- }
2341
- }
2342
- }
2343
- console.log(theme.heading(`
2344
- Summary: ${fileMap.size} file(s) \u2014 ${newFiles} new, ${modifiedFiles} modified`));
2345
- console.log();
2346
- }
2347
- },
2348
- {
2349
- name: "fork",
2350
- description: "Fork current session from a checkpoint or current position",
2351
- usage: "/fork [checkpoint-name]",
2352
- async execute(args, ctx) {
2353
- const session = ctx.sessions.current;
2354
- if (!session) {
2355
- ctx.renderer.printInfo("No active session to fork.");
2356
- return;
2357
- }
2358
- const point = resolveForkPoint(session, args.join(" ").trim());
2359
- if (!point.ok) {
2360
- ctx.renderer.printInfo(point.message);
2361
- return;
2362
- }
2363
- try {
2364
- const originalId = session.id;
2365
- const originalTitle = session.title ?? "(untitled)";
2366
- const forked = await ctx.forkSession(point.messageCount);
2367
- const lines = formatForkReport({
2368
- originalId,
2369
- originalTitle,
2370
- forkedId: forked.id,
2371
- forkedTitle: forked.title ?? "(untitled)",
2372
- fromLabel: point.fromLabel,
2373
- messageCount: forked.messages.length,
2374
- checkpointCount: forked.checkpoints.length
2375
- });
2376
- console.log(theme.heading(`
2377
- ${lines[0]}
2378
- `));
2379
- for (const line of lines.slice(1)) console.log(theme.dim(line));
2380
- console.log();
2381
- ctx.refreshPrompt();
2382
- } catch (err) {
2383
- ctx.renderer.renderError(
2384
- `Fork failed: ${err instanceof Error ? err.message : String(err)}`
2385
- );
2386
- }
2387
- }
2388
- }
2389
- ];
2390
- }
2391
-
2392
- // src/repl/commands/memory-commands.ts
2393
- function createMemoryCommands() {
2394
- return [
2395
- {
2396
- name: "system",
2397
- description: "Set system prompt",
2398
- usage: "/system <prompt>",
2399
- execute(args, ctx) {
2400
- const prompt = args.join(" ");
2401
- if (!prompt) {
2402
- const current = ctx.config.get("session").systemPrompt;
2403
- ctx.renderer.printInfo(
2404
- `Current system prompt: ${current ?? "(none)"}`
2405
- );
2406
- return;
2407
- }
2408
- const session = ctx.config.get("session");
2409
- ctx.config.set("session", { ...session, systemPrompt: prompt });
2410
- ctx.renderer.printSuccess("System prompt updated.");
2411
- }
2412
- },
2413
- {
2414
- name: "context",
2415
- description: "Show or reload hierarchical context files",
2416
- usage: "/context [status|reload]",
2417
- execute(args, ctx) {
2418
- const sub = args[0];
2419
- if (sub === "reload") {
2420
- ctx.reloadContext();
2421
- const layers2 = ctx.getContextLayers();
2422
- if (layers2.length > 0) {
2423
- const summary = layers2.map((l) => `${l.level}:${l.charCount}`).join(", ");
2424
- ctx.renderer.printSuccess(`Context reloaded: ${layers2.length} layer(s) (${summary} chars)`);
2425
- } else {
2426
- ctx.renderer.printInfo("No context file found. Place AICLI.md, CLAUDE.md, or AGENTS.md in your project root or ~/.aicli/.");
2427
- }
2428
- return;
2429
- }
2430
- const layers = ctx.getContextLayers();
2431
- console.log();
2432
- if (layers.length > 0) {
2433
- console.log(" Context layers:");
2434
- const levelLabels = { global: "Global ", project: "Project", local: "Local " };
2435
- for (const l of layers) {
2436
- console.log(` ${levelLabels[l.level] ?? l.level} : ${l.displayPath ?? l.filePath} (${l.charCount} chars${l.truncated ? ", truncated" : ""})`);
2437
- }
2438
- const totalChars = layers.reduce((s, l) => s + l.charCount, 0);
2439
- console.log(` Total : ${totalChars} chars (${layers.length} layer${layers.length > 1 ? "s" : ""})`);
2440
- } else {
2441
- console.log(" No context files loaded.");
2442
- console.log(" Hierarchical context search order:");
2443
- console.log(" 1. Global : ~/.aicli/AICLI.override.md, AGENTS.override.md, AICLI.md, CLAUDE.md, or AGENTS.md");
2444
- console.log(" 2. Project : <git-root>/same candidate order");
2445
- console.log(" 3. Local : <cwd>/same candidate order (when cwd \u2260 project root)");
2446
- }
2447
- console.log();
2448
- }
2449
- },
2450
- {
2451
- name: "status",
2452
- description: "Show current status",
2453
- usage: "/status",
2454
- execute(_args, ctx) {
2455
- const session = ctx.sessions.current;
2456
- const layers = ctx.getContextLayers();
2457
- const tokenUsage = ctx.getSessionTokenUsage();
2458
- const gitBranch = ctx.getGitBranch();
2459
- console.log();
2460
- const provider = ctx.providers.get(ctx.getCurrentProvider());
2461
- const currentModel = ctx.getCurrentModel();
2462
- const modelInfo = provider?.info.models.find((m) => m.id === currentModel);
2463
- const ctxStr = modelInfo ? `${fmtCtx(modelInfo.contextWindow)} tokens` : "unknown";
2464
- console.log(` Provider : ${ctx.getCurrentProvider()}`);
2465
- console.log(` Model : ${currentModel}`);
2466
- console.log(` Ctx Win : ${ctxStr}`);
2467
- console.log(" PermProf : " + (ctx.config.get("defaultPermissionProfile") ?? "legacy"));
2468
- console.log(" Network : " + (ctx.config.get("networkPolicy")?.enabled ?? false ? "on" : "off"));
2469
- console.log(" AutoMode : " + (ctx.getToolExecutor().sessionAutoMode ? "on" : "off"));
2470
- console.log(" Hooks : " + getPendingHookTrust(mergePluginHooks(ctx.config.get("hooks") ?? void 0, ctx.config.getConfigDir()), ctx.config.getConfigDir()).length + " pending trust");
2471
- {
2472
- const memAll = listMemoryEntries(ctx.config.getConfigDir(), { includeExpired: true, includeRejected: true });
2473
- const memActive = memAll.filter((m) => m.approved && !isMemoryExpired(m)).length;
2474
- const memPending = memAll.filter((m) => !m.approved && !isMemoryExpired(m)).length;
2475
- const pendingStr = memPending > 0 ? theme.warning(`${memPending} pending approval \u2014 /memory show --pending, then /memory approve <8-char-id>`) : "0 pending";
2476
- console.log(` Memory : ${memActive} active \xB7 ${pendingStr}`);
2477
- }
2478
- console.log(` Messages : ${session?.messages.length ?? 0}`);
2479
- console.log(` Session : ${session?.id.slice(0, 8) ?? "none"}`);
2480
- if (layers.length > 0) {
2481
- const summary = layers.map((l) => l.level).join("+");
2482
- const totalChars = layers.reduce((s, l) => s + l.charCount, 0);
2483
- console.log(` Context : ${layers.length} layer(s) [${summary}] (${totalChars} chars)`);
2484
- } else {
2485
- console.log(` Context : (none)`);
2486
- }
2487
- if (gitBranch) {
2488
- console.log(` Git : ${gitBranch}`);
2489
- }
2490
- const sys = ctx.config.get("session").systemPrompt;
2491
- if (sys) {
2492
- console.log(` System : ${sys.slice(0, 60)}...`);
2493
- }
2494
- const cacheRead = tokenUsage.cacheReadTokens ?? 0;
2495
- const cacheCreate = tokenUsage.cacheCreationTokens ?? 0;
2496
- const totalTokens = tokenUsage.inputTokens + tokenUsage.outputTokens + cacheRead + cacheCreate;
2497
- if (totalTokens > 0) {
2498
- const cacheSuffix = cacheRead > 0 || cacheCreate > 0 ? ` [cache: +${cacheCreate.toLocaleString()} / -${cacheRead.toLocaleString()}]` : "";
2499
- console.log(
2500
- ` Tokens : in ${tokenUsage.inputTokens.toLocaleString()} + out ${tokenUsage.outputTokens.toLocaleString()} = ${totalTokens.toLocaleString()}${cacheSuffix}`
2501
- );
2502
- const cost = computeCost(ctx.getCurrentProvider(), ctx.getCurrentModel(), tokenUsage);
2503
- if (cost != null) {
2504
- console.log(` Cost : ${formatCost(cost)} (session total)`);
2505
- }
2506
- }
2507
- const ctxWindowSize = ctx.getContextWindowSize();
2508
- if (ctxWindowSize > 0) {
2509
- const estimated = ctx.estimateConversationTokens();
2510
- const pct = Math.round(estimated / ctxWindowSize * 100);
2511
- const estStr = fmtCtx(estimated);
2512
- const winStr = fmtCtx(ctxWindowSize);
2513
- const pctColor = pct >= 80 ? theme.error : pct >= 60 ? theme.warning : theme.success;
2514
- const pctDisplay = pct > 100 ? `${pct}% \u26A0 over limit` : `${pct}%`;
2515
- console.log(` Context% : ~${estStr} / ${winStr} tokens (${pctColor(pctDisplay)})`);
2516
- }
2517
- console.log();
2518
- }
2519
- },
2520
- {
2521
- name: "index",
2522
- description: "Manage symbol + semantic index (status/rebuild/clear/semantic-rebuild/semantic-clear)",
2523
- usage: "/index [status | rebuild | clear | semantic-rebuild | semantic-clear]",
2524
- async execute(args, ctx) {
2525
- const sub = (args[0] ?? "status").toLowerCase();
2526
- const root = process.cwd();
2527
- const { loadIndex, clearIndex } = await import("./store-MWNHVGJT.js");
2528
- const { indexProject } = await import("./indexer-TNDWGFRI.js");
2529
- const { loadVectorStore, clearVectorStore } = await import("./vector-store-BBDXB5IQ.js");
2530
- if (sub === "status") {
2531
- const idx = loadIndex(root);
2532
- const vec = loadVectorStore(root);
2533
- if (!idx) {
2534
- console.log(theme.dim(` No index for ${root}.`));
2535
- console.log(theme.dim(" Run `/index rebuild` to build one."));
2536
- return;
2537
- }
2538
- console.log(theme.heading("\n Symbol index:\n"));
2539
- console.log(` Root: ${idx.root}`);
2540
- console.log(` Generated: ${idx.generated}`);
2541
- console.log(` Files: ${idx.fileCount}`);
2542
- console.log(` Symbols: ${idx.symbolCount}`);
2543
- if (vec) {
2544
- console.log(` Semantic: ${vec.count} vectors \xD7 ${vec.dim}-dim (run /index semantic-rebuild after major changes)`);
2545
- } else {
2546
- console.log(` Semantic: ${theme.dim("not built")} \u2014 run /index semantic-rebuild to enable search_code`);
2547
- }
2548
- console.log();
2549
- return;
2550
- }
2551
- if (sub === "rebuild") {
2552
- console.log(theme.dim(` Indexing ${root}\u2026`));
2553
- const { stats } = await indexProject(root, {
2554
- force: true,
2555
- onProgress: (done, total) => {
2556
- process.stdout.write(theme.dim(`\r ${done}/${total} files\u2026`));
2557
- }
2558
- });
2559
- process.stdout.write("\r\x1B[K");
2560
- console.log(theme.success(
2561
- ` \u2713 Indexed ${stats.filesParsed} files (${stats.symbols} symbols) in ${stats.durationMs}ms`
2562
- ));
2563
- if (stats.filesSkipped > 0) {
2564
- console.log(theme.dim(` (${stats.filesSkipped} skipped: oversized or unreadable)`));
2565
- }
2566
- return;
2567
- }
2568
- if (sub === "clear") {
2569
- clearIndex(root);
2570
- clearVectorStore(root);
2571
- console.log(theme.success(` \u2713 Cleared symbol + semantic index for ${root}`));
2572
- return;
2573
- }
2574
- if (sub === "semantic-rebuild") {
2575
- const idx = loadIndex(root);
2576
- if (!idx) {
2577
- ctx.renderer.renderError("No symbol index yet. Run `/index rebuild` first, then semantic-rebuild.");
2578
- return;
2579
- }
2580
- if (idx.symbolCount === 0) {
2581
- ctx.renderer.renderError(
2582
- "No code symbols to embed. Semantic search needs source files in TS/TSX/JS/JSX/Python \u2014 this project appears to have none (check `/index rebuild` output). Nothing to do; not downloading the embedding model."
2583
- );
2584
- return;
2585
- }
2586
- console.log(theme.dim(` Building semantic index for ${idx.symbolCount} symbols\u2026`));
2587
- console.log(theme.dim(" (First run downloads ~117 MB embedding model to ~/.aicli/models/)"));
2588
- const { rebuildSemanticIndex } = await import("./semantic-HLAE2O4F.js");
2589
- try {
2590
- const stats = await rebuildSemanticIndex(root, {
2591
- onProgress: (done, total) => {
2592
- const pct = total > 0 ? Math.floor(done / total * 100) : 0;
2593
- process.stdout.write(theme.dim(`\r ${done}/${total} (${pct}%)\u2026`));
2594
- }
2595
- });
2596
- process.stdout.write("\r\x1B[K");
2597
- const first = stats.modelFirstLoadMs ? ` (model load+first batch ${stats.modelFirstLoadMs}ms)` : "";
2598
- console.log(theme.success(
2599
- ` \u2713 Embedded ${stats.symbolsEmbedded} symbols in ${stats.durationMs}ms${first}`
2600
- ));
2601
- } catch (err) {
2602
- process.stdout.write("\r\x1B[K");
2603
- const msg = err instanceof Error ? err.message : String(err);
2604
- ctx.renderer.renderError(`Semantic rebuild failed: ${msg}`);
2605
- }
2606
- return;
2607
- }
2608
- if (sub === "semantic-clear") {
2609
- clearVectorStore(root);
2610
- console.log(theme.success(` \u2713 Cleared semantic index (symbol index preserved)`));
2611
- return;
2612
- }
2613
- ctx.renderer.renderError(
2614
- `Unknown subcommand: ${sub}. Use status/rebuild/clear/semantic-rebuild/semantic-clear.`
2615
- );
2616
- }
2617
- },
2618
- // ── /commands ─────────────────────────────────────────────────
2619
- {
2620
- name: "memory",
2621
- description: "Governed persistent memory + chat memory recall index",
2622
- usage: "/memory [show|add|edit|approve|reject|delete|expire|rebind|clear|backup|restore|export|path|rebuild|refresh|status|recall|index-clear]",
2623
- async execute(args, ctx) {
2624
- const configDir = ctx.config.getConfigDir();
2625
- const intent = parseMemoryCommand(args);
2626
- const sub = intent.kind === "manage" ? intent.action : intent.kind;
2627
- if (intent.kind === "usage") {
2628
- ctx.renderer.printInfo(intent.message);
2629
- return;
2630
- }
2631
- if (intent.kind === "rebuild") {
2632
- const full = intent.full;
2633
- ctx.renderer.printInfo(rebuildStartMessage(full));
2634
- try {
2635
- let lastStage = "";
2636
- const stats = await buildChatIndex({
2637
- full,
2638
- onProgress: (p) => {
2639
- if (p.stage !== lastStage) {
2640
- lastStage = p.stage;
2641
- if (p.stage === "embedding" && p.total) {
2642
- process.stdout.write(theme.dim(` embedding ${p.total} chunk(s)\u2026
2643
- `));
2644
- } else if (p.stage !== "done") {
2645
- process.stdout.write(theme.dim(` ${p.stage}\u2026
2646
- `));
2647
- }
2648
- }
2649
- }
2650
- });
2651
- ctx.renderer.printSuccess(rebuildDoneMessage(stats));
2652
- } catch (err) {
2653
- ctx.renderer.renderError(`Rebuild failed: ${err instanceof Error ? err.message : String(err)}`);
2654
- }
2655
- return;
2656
- }
2657
- if (intent.kind === "status") {
2658
- const memories = listMemoryEntries(configDir, { includeExpired: true, includeRejected: true });
2659
- const injected = formatMemoryForPrompt(configDir, process.cwd(), ctx.config.get("memory")?.maxChars);
2660
- const view = buildMemoryStatusView({
2661
- active: memories.filter((m) => m.approved && !isMemoryExpired(m)).length,
2662
- pending: memories.filter((m) => !m.approved && !isMemoryExpired(m)).length,
2663
- injected: injected ? {
2664
- entryCount: injected.entryCount,
2665
- totalCount: injected.totalCount,
2666
- chars: injected.content.length,
2667
- truncated: injected.truncated
2668
- } : null,
2669
- storePath: memoryStorePath(configDir),
2670
- chatIndex: getChatIndexStatus()
2671
- });
2672
- console.log(theme.heading("\n\u{1F9E0} Governed Memory"));
2673
- for (const row of view.governed) {
2674
- const line = ` ${row.label.padEnd(13)}: ${row.value}`;
2675
- console.log(row.warn ? theme.warning(line) : line);
2676
- }
2677
- console.log(theme.heading("\n\u{1F9E0} Chat Memory Index"));
2678
- if (view.chatIndexHint) {
2679
- console.log(theme.dim(` ${view.chatIndexHint}
2680
- `));
2681
- return;
2682
- }
2683
- for (const row of view.chatIndex) {
2684
- console.log(` ${row.label.padEnd(13)}: ${row.value}`);
2685
- }
2686
- console.log();
2687
- return;
2688
- }
2689
- if (intent.kind === "recall") {
2690
- const query = intent.query;
2691
- const persistentHits = searchPersistentMemories(configDir, query, process.cwd(), 3);
2692
- if (persistentHits.length > 0) {
2693
- touchMemoryReferences(configDir, persistentHits.map((h) => h.entry.id));
2694
- }
2695
- let chatHits = [];
2696
- try {
2697
- chatHits = await searchChatMemory(query, { topK: 5, minScore: 0.2 });
2698
- } catch (err) {
2699
- ctx.renderer.renderError(`Recall failed: ${err instanceof Error ? err.message : String(err)}`);
2700
- return;
2701
- }
2702
- const view = buildRecallView(
2703
- query,
2704
- persistentHits.map((h) => ({
2705
- id: h.entry.id,
2706
- scope: h.entry.scope,
2707
- score: h.score,
2708
- content: h.entry.content
2709
- })),
2710
- chatHits.map((h) => ({
2711
- timestamp: h.chunk.timestamp,
2712
- title: h.chunk.sessionTitle ?? h.chunk.sessionId.slice(0, 8),
2713
- score: h.score,
2714
- text: h.chunk.text
2715
- }))
2716
- );
2717
- if (view.persistentHeading) {
2718
- console.log(theme.heading(`
2719
- ${view.persistentHeading}
2720
- `));
2721
- for (const hit of view.persistent) {
2722
- console.log(theme.accent(` ${hit.label}`));
2723
- console.log(theme.dim(" ") + hit.body.replace(/\n/g, "\n "));
2724
- console.log();
2725
- }
2726
- }
2727
- if (view.chatHeading) {
2728
- console.log(theme.heading(`
2729
- ${view.chatHeading}
2730
- `));
2731
- for (const hit of view.chat) {
2732
- console.log(theme.accent(` ${hit.label}`));
2733
- console.log(theme.dim(" ") + hit.body.replace(/\n/g, "\n "));
2734
- console.log();
2735
- }
2736
- }
2737
- if (view.note) {
2738
- console.log(view.note.startsWith("No memories") ? theme.warning(` ${view.note}`) : theme.dim(` ${view.note}`));
2739
- console.log();
2740
- }
2741
- return;
2742
- }
2743
- if (intent.kind === "index-clear") {
2744
- clearChatIndex();
2745
- ctx.renderer.printSuccess("Chat memory index cleared (persistent memory untouched).");
2746
- return;
2747
- }
2748
- const parseFlags = (items) => {
2749
- const flags = {};
2750
- const text = [];
2751
- for (let i = 0; i < items.length; i++) {
2752
- const item = items[i];
2753
- if (item.startsWith("--")) {
2754
- const key = item.slice(2);
2755
- const next = items[i + 1];
2756
- if (next && !next.startsWith("--")) {
2757
- flags[key] = next;
2758
- i++;
2759
- } else {
2760
- flags[key] = true;
2761
- }
2762
- } else {
2763
- text.push(item);
2764
- }
2765
- }
2766
- return { flags, text: text.join(" ").trim() };
2767
- };
2768
- const asSensitivity = (value) => value === "low" || value === "medium" || value === "high" ? value : void 0;
2769
- if (intent.kind === "show") {
2770
- const showFilters = parseMemoryShowFilters(args.slice(1));
2771
- if (showFilters.invalidStale) {
2772
- ctx.renderer.renderError("Usage: /memory show --stale <days> (positive number)");
2773
- return;
2774
- }
2775
- const sourceEntries = showFilters.orphaned ? listOrphanedProjectMemories(configDir) : listMemoryEntries(configDir, { includeExpired: true, includeRejected: true });
2776
- const filtered = filterMemoryEntries(sourceEntries, showFilters);
2777
- const entries = filtered.entries;
2778
- const filters = filtered.applied;
2779
- if (entries.length === 0) {
2780
- ctx.renderer.printInfo(filters.length > 0 ? `No memory entries match [${filters.join(", ")}]` : "Memory is empty");
2781
- return;
2782
- }
2783
- const filterNote = filters.length > 0 ? theme.dim(` (filtered: ${filters.join(", ")})`) : "";
2784
- console.log(theme.heading("\n\u{1F4DD} Governed Persistent Memory:") + filterNote + "\n" + theme.dim("\u2500".repeat(72)));
2785
- for (const entry of entries) {
2786
- const status = isMemoryExpired(entry) ? "expired" : entry.approved ? "approved" : "pending";
2787
- const source = [
2788
- entry.source ?? "unknown",
2789
- entry.sourceSession ? `session ${entry.sourceSession.slice(0, 8)}` : "",
2790
- entry.sourceProject ? `project ${entry.sourceProject}` : ""
2791
- ].filter(Boolean).join(" \xB7 ");
2792
- console.log(theme.accent(` ${entry.id.slice(0, 8)} ${status} scope:${entry.scope} sensitivity:${entry.sensitivity}`));
2793
- const lastRef = entry.lastReferencedAt ? ` \xB7 last-referenced: ${entry.lastReferencedAt.slice(0, 10)}` : "";
2794
- console.log(theme.dim(` source: ${source} \xB7 created: ${entry.createdAt.replace("T", " ").slice(0, 19)}${lastRef}${entry.expiresAt ? ` \xB7 expires: ${entry.expiresAt}` : ""}`));
2795
- console.log(" " + entry.content.replace(/\n/g, "\n "));
2796
- console.log();
2797
- }
2798
- console.log(theme.dim("\u2500".repeat(72)));
2799
- console.log(theme.dim(` store: ${memoryStorePath(configDir)}
2800
- `));
2801
- } else if (sub === "add") {
2802
- const parsed = parseFlags(args.slice(1));
2803
- if (!parsed.text) {
2804
- ctx.renderer.printInfo("Usage: /memory add [--scope personal|project] [--sensitivity low|medium|high] [--expires-at ISO] <text>");
2805
- return;
2806
- }
2807
- try {
2808
- const similar = findSimilarMemory(loadMemoryEntries(configDir), parsed.text);
2809
- const entry = addMemoryEntry(configDir, parsed.text, {
2810
- scope: parseWritableScope(parsed.flags.scope) ?? "personal",
2811
- source: "manual",
2812
- sensitivity: asSensitivity(parsed.flags.sensitivity),
2813
- expiresAt: typeof parsed.flags["expires-at"] === "string" ? parsed.flags["expires-at"] : void 0,
2814
- approved: parsed.flags.pending === true ? false : void 0,
2815
- cwd: process.cwd(),
2816
- redact: true
2817
- });
2818
- const status = entry.approved ? "approved" : "pending approval";
2819
- ctx.renderer.printSuccess(`Added memory ${entry.id.slice(0, 8)} (${status}, scope:${entry.scope}, sensitivity:${entry.sensitivity})`);
2820
- if (similar) {
2821
- ctx.renderer.printInfo(`\u26A0 Similar entry already exists: ${similar.id.slice(0, 8)} \u2014 consider /memory edit ${similar.id.slice(0, 8)} <text> or /memory delete one of them.`);
2822
- }
2823
- } catch (err) {
2824
- ctx.renderer.renderError(`Failed to write memory: ${err instanceof Error ? err.message : String(err)}`);
2825
- }
2826
- } else if (sub === "edit") {
2827
- const id = args[1];
2828
- const newText = args.slice(2).join(" ").trim();
2829
- if (!id || !newText) {
2830
- ctx.renderer.printInfo("Usage: /memory edit <id> <new text>");
2831
- return;
2832
- }
2833
- try {
2834
- const entry = updateMemoryEntry(configDir, id, newText, { source: "manual", redact: true });
2835
- const status = entry.approved ? "approved" : "pending approval \u2014 run /memory approve " + entry.id.slice(0, 8);
2836
- ctx.renderer.printSuccess(`Updated memory ${entry.id.slice(0, 8)} (${status})`);
2837
- } catch (err) {
2838
- ctx.renderer.renderError(err instanceof Error ? err.message : String(err));
2839
- }
2840
- } else if (sub === "rebind") {
2841
- const id = args[1];
2842
- const newPath = args.slice(2).join(" ").trim();
2843
- if (!id || !newPath) {
2844
- ctx.renderer.printInfo("Usage: /memory rebind <id> <new project path> (find orphans with /memory show --orphaned)");
2845
- return;
2846
- }
2847
- try {
2848
- const entry = rebindMemoryProject(configDir, id, newPath);
2849
- ctx.renderer.printSuccess(`Rebound memory ${entry.id.slice(0, 8)} to project ${entry.sourceProject}`);
2850
- } catch (err) {
2851
- ctx.renderer.renderError(err instanceof Error ? err.message : String(err));
2852
- }
2853
- } else if (sub === "approve" || sub === "reject") {
2854
- const id = args[1];
2855
- if (!id) {
2856
- ctx.renderer.printInfo(`Usage: /memory ${sub} <id>`);
2857
- return;
2858
- }
2859
- try {
2860
- const entry = updateMemoryApproval(configDir, id, sub === "approve");
2861
- ctx.renderer.printSuccess(`${sub === "approve" ? "Approved" : "Rejected"} memory ${entry.id.slice(0, 8)}`);
2862
- } catch (err) {
2863
- ctx.renderer.renderError(err instanceof Error ? err.message : String(err));
2864
- }
2865
- } else if (sub === "delete") {
2866
- const id = args[1];
2867
- if (!id) {
2868
- ctx.renderer.printInfo("Usage: /memory delete <id>");
2869
- return;
2870
- }
2871
- try {
2872
- const entry = deleteMemoryEntry(configDir, id);
2873
- ctx.renderer.printSuccess(`Deleted memory ${entry.id.slice(0, 8)}`);
2874
- } catch (err) {
2875
- ctx.renderer.renderError(err instanceof Error ? err.message : String(err));
2876
- }
2877
- } else if (sub === "expire") {
2878
- const id = args[1];
2879
- if (!id) {
2880
- ctx.renderer.printInfo("Usage: /memory expire <id> [ISO timestamp]");
2881
- return;
2882
- }
2883
- try {
2884
- const entry = expireMemoryEntry(configDir, id, args[2]);
2885
- ctx.renderer.printSuccess(`Expired memory ${entry.id.slice(0, 8)}`);
2886
- } catch (err) {
2887
- ctx.renderer.renderError(err instanceof Error ? err.message : String(err));
2888
- }
2889
- } else if (sub === "export") {
2890
- const parsed = parseFlags(args.slice(1));
2891
- const format = parsed.flags.format === "md" ? "md" : "json";
2892
- if (parsed.flags.format && parsed.flags.format !== "md" && parsed.flags.format !== "json") {
2893
- ctx.renderer.renderError(`Unknown format: ${parsed.flags.format}. Use --format json|md.`);
2894
- return;
2895
- }
2896
- const output = exportMemoryEntries(configDir, process.cwd(), format);
2897
- const outPath = typeof parsed.flags.out === "string" ? parsed.flags.out : void 0;
2898
- if (outPath) {
2899
- try {
2900
- atomicWriteFileSync(outPath, output);
2901
- const label = format === "md" ? "active entries (markdown)" : "all entries (json)";
2902
- ctx.renderer.printSuccess(`Exported ${label} to ${outPath}`);
2903
- } catch (err) {
2904
- ctx.renderer.renderError(`Export failed: ${err instanceof Error ? err.message : String(err)}`);
2905
- }
2906
- } else {
2907
- console.log(output);
2908
- }
2909
- } else if (sub === "clear") {
2910
- const all = listMemoryEntries(configDir, { includeExpired: true, includeRejected: true });
2911
- if (all.length === 0) {
2912
- ctx.renderer.printInfo("Memory is already empty.");
2913
- return;
2914
- }
2915
- console.log();
2916
- console.log(theme.warning(` About to permanently clear ALL persistent memory (${all.length} entr${all.length > 1 ? "ies" : "y"}):`));
2917
- for (const entry of all.slice(0, 5)) {
2918
- const first = entry.content.split("\n")[0];
2919
- console.log(theme.dim(` ${entry.id.slice(0, 8)} ${first.length > 60 ? first.slice(0, 60) + "\u2026" : first}`));
2920
- }
2921
- if (all.length > 5) console.log(theme.dim(` \u2026 and ${all.length - 5} more`));
2922
- console.log();
2923
- const choice = await ctx.select("Clear all persistent memory?", [
2924
- { label: "No \u2014 keep it", value: "no" },
2925
- { label: "Yes \u2014 clear all", value: "yes", hint: "a timestamped .bak backup is kept next to memory.jsonl" }
2926
- ]);
2927
- if (choice !== "yes") {
2928
- ctx.renderer.printInfo("Cancelled \u2014 memory kept.");
2929
- return;
2930
- }
2931
- const result = clearMemoryEntries(configDir);
2932
- ctx.renderer.printSuccess(`Cleared ${result.cleared} memory entr${result.cleared > 1 ? "ies" : "y"}.`);
2933
- if (result.backupPath) {
2934
- ctx.renderer.printInfo(`Backup saved: ${result.backupPath}`);
2935
- }
2936
- } else if (sub === "backup") {
2937
- const parsed = parseFlags(args.slice(1));
2938
- try {
2939
- const result = backupMemoryStore(configDir, typeof parsed.flags.out === "string" ? parsed.flags.out : void 0);
2940
- ctx.renderer.printSuccess(`Backed up ${result.count} entr${result.count > 1 ? "ies" : "y"} to ${result.backupPath}`);
2941
- } catch (err) {
2942
- ctx.renderer.renderError(err instanceof Error ? err.message : String(err));
2943
- }
2944
- } else if (sub === "restore") {
2945
- const file = args[1];
2946
- if (!file) {
2947
- ctx.renderer.printInfo("Usage: /memory restore <file> (accepts .jsonl backups or /memory export JSON)");
2948
- return;
2949
- }
2950
- try {
2951
- const result = restoreMemoryEntries(configDir, file);
2952
- ctx.renderer.printSuccess(`Restored ${result.imported} entr${result.imported === 1 ? "y" : "ies"} (${result.skipped} duplicate${result.skipped === 1 ? "" : "s"} skipped)`);
2953
- if (result.backupPath) ctx.renderer.printInfo(`Pre-restore backup: ${result.backupPath}`);
2954
- const pending = countPendingMemories(configDir);
2955
- if (pending > 0) ctx.renderer.printInfo(`${pending} entr${pending > 1 ? "ies" : "y"} pending approval \u2014 /memory show --pending`);
2956
- } catch (err) {
2957
- ctx.renderer.renderError(`Restore failed: ${err instanceof Error ? err.message : String(err)}`);
2958
- }
2959
- } else if (sub === "path") {
2960
- console.log(theme.accent(memoryStorePath(configDir)));
2961
- console.log(theme.dim(memoryMarkdownPath(configDir)));
2962
- } else {
2963
- ctx.renderer.printInfo(MEMORY_USAGE);
2964
- }
2965
- }
2966
- },
2967
- // ── /doctor ────────────────────────────────────────────────────────────────
2968
- {
2969
- name: "add-dir",
2970
- description: "Add (or remove) a directory from AI context",
2971
- usage: "/add-dir [path] [remove <path>]",
2972
- execute(args, ctx) {
2973
- if (args.length === 0) {
2974
- const dirs = ctx.listContextDirs();
2975
- if (dirs.length === 0) {
2976
- console.log(theme.dim(" No extra directories in context. Use /add-dir <path> to add one."));
2977
- } else {
2978
- console.log(theme.heading("\n Extra context directories:"));
2979
- for (const d of dirs) {
2980
- console.log(theme.accent(` \u2022 ${d.dir}`) + theme.dim(` (${d.charCount} chars)`));
2981
- }
2982
- console.log(theme.dim("\n Use /add-dir remove <path> to remove a directory"));
2983
- }
2984
- return;
2985
- }
2986
- if (args[0] === "remove" && args[1]) {
2987
- const target = args.slice(1).join(" ");
2988
- const removed = ctx.removeContextDir(target);
2989
- if (removed) {
2990
- ctx.renderer.printSuccess(`Removed from context: ${target}`);
2991
- } else {
2992
- ctx.renderer.printInfo(`Directory not found in context: ${target}`);
2993
- }
2994
- return;
2995
- }
2996
- const dirPath = args.join(" ");
2997
- const spinner = ctx.renderer.showSpinner(`Scanning ${dirPath}...`);
2998
- const result = ctx.addContextDir(dirPath);
2999
- spinner.stop();
3000
- if (!result.success) {
3001
- ctx.renderer.renderError(result.error ?? "Failed to add directory");
3002
- return;
3003
- }
3004
- if (!result.added) {
3005
- ctx.renderer.printInfo(`Already in context: ${dirPath} (${result.charCount} chars)`);
3006
- } else {
3007
- ctx.renderer.printSuccess(`Added to context: ${dirPath} (${result.charCount} chars \u2014 AI can now see this directory)`);
3008
- }
3009
- }
3010
- }
3011
- // ── /profile ───────────────────────────────────────────────────────────────
3012
- ];
3013
- }
3014
-
3015
- // src/repl/commands/session-commands.ts
3016
- import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
3017
- import { resolve, dirname as dirname2 } from "path";
3018
- import chalk3 from "chalk";
3019
- function createSessionCommands() {
3020
- return [
3021
- {
3022
- name: "clear",
3023
- description: "Start a new session (previous conversation is kept)",
3024
- usage: "/clear",
3025
- async execute(_args, ctx) {
3026
- const prior = ctx.sessions.current;
3027
- const priorId = prior?.id ?? null;
3028
- const priorCount = prior?.messages.length ?? 0;
3029
- if (priorCount > 0) await ctx.sessions.save();
3030
- ctx.sessions.createSession(ctx.getCurrentProvider(), ctx.getCurrentModel());
3031
- ctx.resetSessionTokenUsage();
3032
- ctx.clearDevState();
3033
- ctx.renderer.printInfo(clearedSessionMessage(priorId, priorCount));
3034
- }
3035
- },
3036
- {
3037
- name: "session",
3038
- description: "Session management",
3039
- usage: "/session new|list [--limit N|--all] [--filter provider=|model=|messages=N] [--search kw] [--days N|--stale N] [--sort f[:o]] [--json] [--verbose]|load <id>|show <id> [page]|page <n>|rename <id> <title>|remove <id>|clean --stale N [--dry-run]",
3040
- async execute(args, ctx) {
3041
- const sub = args[0];
3042
- if (sub === "new") {
3043
- const provider = ctx.getCurrentProvider();
3044
- const model = ctx.getCurrentModel();
3045
- ctx.sessions.createSession(provider, model);
3046
- ctx.resetSessionTokenUsage();
3047
- ctx.clearDevState();
3048
- ctx.renderer.printSuccess("New session started.");
3049
- } else if (sub === "list") {
3050
- const opts = parseSessionListArgs(args);
3051
- for (const w of opts.warnings) ctx.renderer.printInfo(`\u26A0 ${w}`);
3052
- const all = ctx.sessions.listSessions({ includeFileSize: opts.verbose || opts.sort.field === "size" });
3053
- if (all.length === 0) {
3054
- ctx.renderer.printInfo("No saved sessions.");
3055
- return;
3056
- }
3057
- const view = selectSessions(all, opts);
3058
- if (view.totalCount === 0) {
3059
- ctx.renderer.printInfo("No sessions match the filter criteria.");
3060
- return;
3061
- }
3062
- if (opts.json) {
3063
- console.log(JSON.stringify(view.shown.map((s) => sessionListJsonRow(s, view.needsFileSize)), null, 2));
3064
- if (view.shown.length < view.totalCount) {
3065
- console.error(
3066
- `
3067
- (truncated: ${view.shown.length} of ${view.totalCount} sessions shown; use --all for full list)`
3068
- );
3069
- }
3070
- return;
3071
- }
3072
- const headers = ["ID (short)", "Provider", "Model", "Messages"];
3073
- if (opts.verbose) headers.push("Size", "Br");
3074
- headers.push("Date", "Title");
3075
- const rows = view.shown.map((s) => {
3076
- const isStale = view.staleCutoff !== null && s.updated.getTime() < view.staleCutoff;
3077
- const dim = isStale ? theme.dim : (x) => String(x);
3078
- const row = [dim(s.id.slice(0, 8)), dim(s.provider), dim(s.model), s.messageCount];
3079
- if (opts.verbose) {
3080
- row.push(s.fileSize !== void 0 ? formatFileSize(s.fileSize) : "\u2014", s.branchCount ?? 1);
3081
- }
3082
- row.push(formatSessionDate(s.updated), dim(truncateTitle(s.title ?? "(untitled)", MAX_TITLE_WIDTH)));
3083
- return row;
3084
- });
3085
- console.log();
3086
- ctx.renderer.printTable(headers, rows);
3087
- console.log(theme.dim(` \u2500\u2500 ${sessionListSummary(view)}`));
3088
- if (view.shown.length < view.totalCount) {
3089
- console.log(theme.dim(` Use --all to show all, or --limit N to change count.`));
3090
- }
3091
- } else if (sub === "load" || sub === "show") {
3092
- const id = args[1];
3093
- if (!id) {
3094
- ctx.renderer.renderError(`Usage: /session ${sub} <id>${sub === "show" ? " [page]" : ""}`);
3095
- return;
3096
- }
3097
- const ref = resolveSessionRef(ctx.sessions.listSessions(), id, "most-recent");
3098
- if (ref.status !== "ok") {
3099
- ctx.renderer.renderError(ref.message);
3100
- return;
3101
- }
3102
- if (ref.message) {
3103
- console.log(theme.warning(` ${ref.message}`));
3104
- for (const m of ref.ambiguousWith.slice(0, 5)) {
3105
- console.log(theme.dim(` ${m.id.slice(0, 12)} ${m.title ?? "(untitled)"}`));
3106
- }
3107
- }
3108
- const match = ref.match;
3109
- ctx.sessions.loadSession(match.id);
3110
- ctx.setProvider(match.provider, match.model);
3111
- ctx.resetSessionTokenUsage();
3112
- ctx.renderer.printSuccess(
3113
- `Loaded session ${match.id.slice(0, 8)}: ${match.title ?? "(untitled)"}`
3114
- );
3115
- if (sub === "show") {
3116
- const pageArg = args[2] !== void 0 ? Number.parseInt(args[2], 10) : void 0;
3117
- const cur = ctx.sessions.current;
3118
- if (cur) renderTranscriptPage(cur.messages, ctx.renderer, pageArg);
3119
- }
3120
- } else if (sub === "page") {
3121
- const cur = ctx.sessions.current;
3122
- if (!cur) {
3123
- ctx.renderer.renderError("No active session. Use /session show <id> first.");
3124
- return;
3125
- }
3126
- const n = Number.parseInt(args[1] ?? "", 10);
3127
- if (Number.isNaN(n)) {
3128
- ctx.renderer.renderError("Usage: /session page <n>");
3129
- return;
3130
- }
3131
- renderTranscriptPage(cur.messages, ctx.renderer, n);
3132
- } else if (sub === "rename") {
3133
- const id = args[1];
3134
- const newTitle = args.slice(2).join(" ").trim();
3135
- if (!id || !newTitle) {
3136
- ctx.renderer.renderError("Usage: /session rename <id> <new title>");
3137
- return;
3138
- }
3139
- const ref = resolveSessionRef(ctx.sessions.listSessions(), id, "strict");
3140
- if (ref.status !== "ok") {
3141
- ctx.renderer.renderError(ref.message);
3142
- if (ref.status === "ambiguous") {
3143
- for (const m of ref.matches.slice(0, 10)) console.log(theme.dim(` ${formatSessionSummaryLine(m)}`));
3144
- }
3145
- return;
3146
- }
3147
- const priorId = ctx.sessions.current?.id ?? null;
3148
- const isCurrent = priorId === ref.match.id;
3149
- const session = isCurrent ? ctx.sessions.current : ctx.sessions.loadSession(ref.match.id);
3150
- session.title = newTitle;
3151
- await ctx.sessions.save();
3152
- if (!isCurrent && priorId) {
3153
- ctx.sessions.loadSession(priorId);
3154
- }
3155
- ctx.renderer.printSuccess(`Renamed session ${ref.match.id.slice(0, 8)}: "${newTitle}"`);
3156
- } else if (sub === "remove" || sub === "delete") {
3157
- if (args[1] === "--filter") {
3158
- const opts = parseBatchDeleteArgs(args);
3159
- if (opts.error) {
3160
- ctx.renderer.renderError(opts.error);
3161
- return;
3162
- }
3163
- const matches = ctx.sessions.listSessions().filter((s) => s.messageCount === opts.messages);
3164
- if (matches.length === 0) {
3165
- ctx.renderer.printInfo(`No sessions with ${opts.messages} message(s) found.`);
3166
- return;
3167
- }
3168
- console.log();
3169
- console.log(theme.warning(` Found ${matches.length} session(s) with ${opts.messages} message(s):`));
3170
- for (const s of matches.slice(0, 20)) console.log(theme.dim(` ${formatSessionSummaryLine(s)}`));
3171
- if (matches.length > 20) console.log(theme.dim(` ... and ${matches.length - 20} more`));
3172
- if (opts.dryRun) {
3173
- console.log(theme.info(`
3174
- --dry-run: no sessions were deleted. Remove --dry-run to actually delete.`));
3175
- return;
3176
- }
3177
- console.log();
3178
- const choice2 = await ctx.select(
3179
- `Delete all ${matches.length} matching sessions?`,
3180
- [
3181
- { label: "No \u2014 keep them", value: "no" },
3182
- { label: "Yes \u2014 delete permanently", value: "yes", hint: "unlinks all matching history files" }
3183
- ]
3184
- );
3185
- if (choice2 !== "yes") {
3186
- ctx.renderer.printInfo("Cancelled \u2014 no sessions deleted.");
3187
- return;
3188
- }
3189
- let deleted = 0;
3190
- let failed = 0;
3191
- for (const s of matches) {
3192
- if (ctx.sessions.deleteSession(s.id)) deleted++;
3193
- else failed++;
3194
- }
3195
- const currentStillExists = ctx.sessions.current !== null;
3196
- ctx.renderer.printSuccess(`Removed: ${deleted} deleted${failed > 0 ? `, ${failed} failed` : ""}.`);
3197
- if (!currentStillExists) {
3198
- ctx.sessions.createSession(ctx.getCurrentProvider(), ctx.getCurrentModel());
3199
- ctx.resetSessionTokenUsage();
3200
- ctx.clearDevState();
3201
- ctx.renderer.printInfo("Current session was among deleted \u2014 started a fresh session.");
3202
- }
3203
- return;
3204
- }
3205
- const id = args[1];
3206
- if (!id) {
3207
- ctx.renderer.renderError("Usage: /session remove <id> | /session remove --filter messages=N [--dry-run]");
3208
- return;
3209
- }
3210
- const ref = resolveSessionRef(ctx.sessions.listSessions(), id, "strict");
3211
- if (ref.status !== "ok") {
3212
- ctx.renderer.renderError(ref.message);
3213
- if (ref.status === "ambiguous") {
3214
- for (const m of ref.matches.slice(0, 10)) console.log(theme.dim(` ${formatSessionSummaryLine(m)}`));
3215
- }
3216
- return;
3217
- }
3218
- const match = ref.match;
3219
- const isCurrent = ctx.sessions.current?.id === match.id;
3220
- console.log();
3221
- console.log(theme.warning(" About to permanently delete session (no undo):"));
3222
- console.log(` ${theme.info(match.id.slice(0, 12))} ${match.provider}/${match.model} ${match.messageCount} messages ${match.updated.toLocaleDateString()}`);
3223
- console.log(` ${match.title ?? "(untitled)"}${isCurrent ? theme.warning(" \u2190 current session") : ""}`);
3224
- console.log();
3225
- const choice = await ctx.select("Delete this session?", [
3226
- { label: "No \u2014 keep it", value: "no" },
3227
- { label: "Yes \u2014 delete permanently", value: "yes", hint: "unlinks the history file" }
3228
- ]);
3229
- if (choice !== "yes") {
3230
- ctx.renderer.printInfo("Cancelled \u2014 session kept.");
3231
- return;
3232
- }
3233
- if (!ctx.sessions.deleteSession(match.id)) {
3234
- ctx.renderer.renderError("Delete failed \u2014 session file not found on disk.");
3235
- return;
3236
- }
3237
- ctx.renderer.printSuccess(`Deleted session ${match.id.slice(0, 8)}: ${match.title ?? "(untitled)"}`);
3238
- if (isCurrent) {
3239
- ctx.sessions.createSession(ctx.getCurrentProvider(), ctx.getCurrentModel());
3240
- ctx.resetSessionTokenUsage();
3241
- ctx.clearDevState();
3242
- ctx.renderer.printInfo("Current session was deleted \u2014 started a fresh session.");
3243
- }
3244
- } else if (sub === "clean") {
3245
- const opts = parseCleanArgs(args);
3246
- const stale = selectStaleSessions(ctx.sessions.listSessions(), opts.staleDays);
3247
- if (stale.length === 0) {
3248
- ctx.renderer.printInfo(`No sessions older than ${opts.staleDays} days found.`);
3249
- return;
3250
- }
3251
- console.log();
3252
- console.log(theme.warning(` Found ${stale.length} session(s) not updated in ${opts.staleDays}+ days:`));
3253
- for (const s of stale.slice(0, 20)) console.log(theme.dim(` ${formatSessionSummaryLine(s)}`));
3254
- if (stale.length > 20) console.log(theme.dim(` ... and ${stale.length - 20} more`));
3255
- if (opts.dryRun) {
3256
- console.log(theme.info(`
3257
- --dry-run: no sessions were deleted. Remove --dry-run to actually delete.`));
3258
- return;
3259
- }
3260
- console.log();
3261
- const choice = await ctx.select(
3262
- `Delete all ${stale.length} stale sessions?`,
3263
- [
3264
- { label: "No \u2014 keep them", value: "no" },
3265
- { label: "Yes \u2014 delete permanently", value: "yes", hint: "unlinks all stale history files" }
3266
- ]
3267
- );
3268
- if (choice !== "yes") {
3269
- ctx.renderer.printInfo("Cancelled \u2014 no sessions deleted.");
3270
- return;
3271
- }
3272
- let deleted = 0;
3273
- let failed = 0;
3274
- for (const s of stale) {
3275
- if (ctx.sessions.deleteSession(s.id)) deleted++;
3276
- else failed++;
3277
- }
3278
- ctx.renderer.printSuccess(`Cleaned up: ${deleted} deleted${failed > 0 ? `, ${failed} failed` : ""}.`);
3279
- } else {
3280
- ctx.renderer.renderError("Usage: /session new|list|load <id>|show <id> [page]|page <n>|rename <id> <title>|remove <id>|remove --filter messages=N [--dry-run]|clean --stale N [--dry-run]");
3281
- }
3282
- }
3283
- },
3284
- {
3285
- name: "compact",
3286
- description: "Summarize and compress conversation history to save context space",
3287
- usage: "/compact [instruction]",
3288
- async execute(args, ctx) {
3289
- const instruction = args.join(" ").trim() || void 0;
3290
- await ctx.compactSession(instruction);
3291
- }
3292
- },
3293
- {
3294
- name: "export",
3295
- description: "Export current session to file or stdout",
3296
- usage: "/export [md|json] [filename]",
3297
- execute(args, ctx) {
3298
- const { format, outFile } = parseExportArgs(args);
3299
- const session = ctx.sessions.current;
3300
- if (!session || session.messages.length === 0) {
3301
- ctx.renderer.printInfo("No messages to export.");
3302
- return;
3303
- }
3304
- const exported = buildSessionExport(session, format, outFile);
3305
- const absPath = resolve(process.cwd(), exported.filename);
3306
- try {
3307
- mkdirSync2(dirname2(absPath), { recursive: true });
3308
- writeFileSync2(absPath, exported.content, "utf-8");
3309
- ctx.renderer.printSuccess(`Exported ${exported.messageCount} messages \u2192 ${absPath}`);
3310
- } catch (err) {
3311
- ctx.renderer.renderError(`Failed to write file: ${err instanceof Error ? err.message : String(err)}`);
3312
- }
3313
- }
3314
- },
3315
- {
3316
- name: "checkpoint",
3317
- description: "Manage session checkpoints (save/restore/list/delete)",
3318
- usage: "/checkpoint [save <name> | restore <name> | delete <name>]",
3319
- execute(args, ctx) {
3320
- const session = ctx.sessions.current;
3321
- if (!session) {
3322
- ctx.renderer.renderError("No active session.");
3323
- return;
3324
- }
3325
- const intent = parseCheckpointCommand(args);
3326
- if (intent.kind === "list") {
3327
- const lines = formatCheckpointList(ctx.listCheckpoints());
3328
- console.log(theme.dim(` ${lines[0]}`));
3329
- for (const line of lines.slice(1)) {
3330
- const [name, rest] = [line.slice(0, 24), line.slice(24)];
3331
- console.log(theme.accent(` ${name}`) + theme.dim(rest));
3332
- }
3333
- console.log();
3334
- return;
3335
- }
3336
- if (intent.kind === "usage" || intent.kind === "unknown") {
3337
- ctx.renderer.renderError(intent.message);
3338
- return;
3339
- }
3340
- if (intent.kind === "save") {
3341
- ctx.createCheckpoint(intent.name);
3342
- console.log(theme.success(` ${checkpointSavedMessage(intent.name, session.messages.length)}`));
3343
- return;
3344
- }
3345
- if (intent.kind === "restore") {
3346
- if (!ctx.restoreCheckpoint(intent.name)) {
3347
- ctx.renderer.renderError(checkpointNotFoundMessage(intent.name));
3348
- return;
3349
- }
3350
- ctx.resetSessionTokenUsage();
3351
- console.log(theme.success(` ${checkpointRestoredMessage(intent.name, session.messages.length)}`));
3352
- return;
3353
- }
3354
- if (!ctx.deleteCheckpoint(intent.name)) {
3355
- ctx.renderer.renderError(checkpointNotFoundMessage(intent.name));
3356
- return;
3357
- }
3358
- console.log(theme.success(` ${checkpointDeletedMessage(intent.name)}`));
3359
- }
3360
- },
3361
- // ── /review ──────────────────────────────────────────────────
3362
- {
3363
- name: "cost",
3364
- description: "Show session token usage, prompt-cache hits, and USD cost",
3365
- usage: "/cost [reset | history]",
3366
- execute(args, ctx) {
3367
- const sub = args[0]?.toLowerCase();
3368
- if (sub === "reset") {
3369
- ctx.resetSessionTokenUsage();
3370
- ctx.renderer.printSuccess("Session token counters reset.");
3371
- return;
3372
- }
3373
- if (sub === "history" || sub === "h") {
3374
- const tracker = ctx.getCostTracker();
3375
- const budget = ctx.config.get("monthlyBudget");
3376
- console.log();
3377
- console.log(theme.heading(" \u{1F4B0} Cross-Session Cost Dashboard"));
3378
- console.log(theme.dim(" " + "\u2500".repeat(48)));
3379
- const summary = tracker.formatSummary(budget);
3380
- for (const line of summary.split("\n")) {
3381
- console.log(theme.dim(" ") + chalk3.white(line));
3382
- }
3383
- console.log(theme.dim(" " + "\u2500".repeat(48)));
3384
- const warning = tracker.checkBudget(budget);
3385
- if (warning) {
3386
- console.log(theme.warning(` ${warning}`));
3387
- } else if (budget && budget > 0) {
3388
- console.log(theme.success(" \u2713 Within monthly budget"));
3389
- } else {
3390
- console.log(theme.dim(' Tip: set "monthlyBudget" in config (e.g., 50 for $50/month)'));
3391
- }
3392
- console.log();
3393
- return;
3394
- }
3395
- const session = ctx.sessions.current;
3396
- const usage = session?.tokenUsage ?? {
3397
- inputTokens: 0,
3398
- outputTokens: 0,
3399
- cacheCreationTokens: 0,
3400
- cacheReadTokens: 0
3401
- };
3402
- const cacheCreate = usage.cacheCreationTokens ?? 0;
3403
- const cacheRead = usage.cacheReadTokens ?? 0;
3404
- const totalTokens = usage.inputTokens + usage.outputTokens + cacheCreate + cacheRead;
3405
- if (totalTokens === 0) {
3406
- ctx.renderer.printInfo("No token usage recorded this session.");
3407
- return;
3408
- }
3409
- const provider = ctx.getCurrentProvider();
3410
- const model = ctx.getCurrentModel();
3411
- const accrued = ctx.getSessionAccruedCost();
3412
- const report = buildSessionCostReport({
3413
- provider,
3414
- model,
3415
- usage: { ...usage, cacheCreationTokens: cacheCreate, cacheReadTokens: cacheRead },
3416
- accruedUsd: accrued.usd,
3417
- accrualIncomplete: accrued.incomplete
3418
- });
3419
- const cost = report.cost;
3420
- const savings = report.savings;
3421
- console.log();
3422
- console.log(theme.heading(" \u{1F4B0} Session Cost & Token Usage"));
3423
- console.log(theme.dim(" " + "\u2500".repeat(48)));
3424
- console.log(theme.dim(" Input (uncached) : ") + chalk3.white(usage.inputTokens.toLocaleString().padStart(12)));
3425
- console.log(theme.dim(" Output : ") + chalk3.white(usage.outputTokens.toLocaleString().padStart(12)));
3426
- if (cacheCreate > 0) {
3427
- console.log(theme.dim(" Cache write : ") + chalk3.yellow(cacheCreate.toLocaleString().padStart(12)));
3428
- }
3429
- if (cacheRead > 0) {
3430
- const pct = Math.round(cacheRead / (cacheRead + usage.inputTokens) * 100);
3431
- console.log(
3432
- theme.dim(" Cache read : ") + chalk3.green(cacheRead.toLocaleString().padStart(12)) + theme.dim(` (${pct}% hit rate)`)
3433
- );
3434
- }
3435
- console.log(theme.dim(" Total tokens : ") + chalk3.bold.white(totalTokens.toLocaleString().padStart(12)));
3436
- console.log(theme.dim(" " + "\u2500".repeat(48)));
3437
- if (cost != null) {
3438
- console.log(theme.dim(" Cost : ") + chalk3.bold.cyan(formatCost(cost).padStart(12)));
3439
- const note = costSourceNote(report.costSource);
3440
- if (note) console.log(theme.dim(` ${note}`));
3441
- if (savings != null && savings > 0) {
3442
- console.log(
3443
- theme.dim(" Cache savings : ") + chalk3.green(`-${formatCost(savings)}`.padStart(12)) + theme.dim(` (vs no cache)`)
3444
- );
3445
- }
3446
- } else {
3447
- console.log(theme.dim(" Cost : ") + theme.dim(" \u2014 (pricing unknown)"));
3448
- }
3449
- console.log(theme.dim(" " + "\u2500".repeat(48)));
3450
- if (session) {
3451
- console.log(theme.dim(" Provider : ") + theme.dim(provider));
3452
- console.log(theme.dim(" Model : ") + theme.dim(model));
3453
- console.log(theme.dim(" Messages : ") + theme.dim(String(session.messages.length)));
3454
- }
3455
- console.log();
3456
- }
3457
- },
3458
- // ── /checkpoint ────────────────────────────────────────────
3459
- {
3460
- name: "snapshot",
3461
- description: "Force-generate a dev-state handoff snapshot now (normally auto on /exit)",
3462
- usage: "/snapshot",
3463
- async execute(_args, ctx) {
3464
- const ok = await ctx.generateDevStateSnapshot();
3465
- if (!ok) {
3466
- ctx.renderer.printInfo("No meaningful conversation yet \u2014 nothing to snapshot.");
3467
- }
3468
- }
3469
- }
3470
- ];
3471
- }
3472
-
3473
- // src/repl/commands/misc-commands.ts
3474
- import { basename } from "path";
3475
- import { platform as platform3 } from "os";
3476
-
3477
- // src/repl/clipboard.ts
3478
- import { execSync as execSync2 } from "child_process";
3479
- import { existsSync as existsSync2, statSync, unlinkSync } from "fs";
3480
- import { tmpdir } from "os";
3481
- import { join as join2 } from "path";
3482
- var CLIPBOARD_TIMEOUT = 5e3;
3483
- function readClipboardImage() {
3484
- const outPath = join2(tmpdir(), `aicli-paste-${Date.now()}.png`);
3485
- try {
3486
- if (process.platform === "win32") {
3487
- _readWin32(outPath);
3488
- } else if (process.platform === "darwin") {
3489
- _readDarwin(outPath);
3490
- } else {
3491
- _readLinux(outPath);
3492
- }
3493
- } catch {
3494
- _cleanup(outPath);
3495
- return null;
3496
- }
3497
- if (!existsSync2(outPath) || statSync(outPath).size === 0) {
3498
- _cleanup(outPath);
3499
- return null;
3500
- }
3501
- return outPath;
3502
- }
3503
- function _readWin32(outPath) {
3504
- const escapedPath = outPath.replace(/'/g, "''");
3505
- const script = [
3506
- "Add-Type -AssemblyName System.Windows.Forms",
3507
- "Add-Type -AssemblyName System.Drawing",
3508
- "$img = [System.Windows.Forms.Clipboard]::GetImage()",
3509
- `if ($null -ne $img) { $img.Save('${escapedPath}', [System.Drawing.Imaging.ImageFormat]::Png); exit 0 } else { exit 1 }`
3510
- ].join("; ");
3511
- execSync2(`powershell -NoProfile -NonInteractive -Command "${script}"`, {
3512
- stdio: "pipe",
3513
- timeout: CLIPBOARD_TIMEOUT
3514
- });
3515
- }
3516
- function _readDarwin(outPath) {
3517
- try {
3518
- execSync2(`pngpaste "${outPath}"`, { stdio: "pipe", timeout: CLIPBOARD_TIMEOUT });
3519
- return;
3520
- } catch {
3521
- }
3522
- const script = [
3523
- "try",
3524
- " set d to (the clipboard as \xABclass PNGf\xBB)",
3525
- ` set fp to open for access POSIX file "${outPath}" with write permission`,
3526
- " write d to fp",
3527
- " close access fp",
3528
- "end try"
3529
- ].join("\n");
3530
- execSync2(`osascript -e '${script}'`, { stdio: "pipe", timeout: CLIPBOARD_TIMEOUT });
3531
- }
3532
- function _readLinux(outPath) {
3533
- try {
3534
- execSync2(`xclip -selection clipboard -t image/png -o > "${outPath}"`, {
3535
- shell: "/bin/sh",
3536
- stdio: "pipe",
3537
- timeout: CLIPBOARD_TIMEOUT
3538
- });
3539
- return;
3540
- } catch {
3541
- }
3542
- execSync2(`wl-paste --type image/png > "${outPath}"`, {
3543
- shell: "/bin/sh",
3544
- stdio: "pipe",
3545
- timeout: CLIPBOARD_TIMEOUT
3546
- });
3547
- }
3548
- function _cleanup(path) {
3549
- try {
3550
- if (existsSync2(path)) unlinkSync(path);
3551
- } catch {
3552
- }
3553
- }
3554
- function getClipboardHint() {
3555
- if (process.platform === "darwin") {
3556
- return "If pngpaste is not installed, run: brew install pngpaste";
3557
- }
3558
- if (process.platform === "linux") {
3559
- return "Please install xclip (X11): sudo apt install xclip\nor wl-paste (Wayland): sudo apt install wl-clipboard";
3560
- }
3561
- return "";
3562
- }
3563
-
3564
- // src/repl/commands/misc-commands.ts
3565
- function createMiscCommands() {
3566
- return [
3567
- {
3568
- name: "help",
3569
- description: "List all available commands (grouped by category)",
3570
- usage: "/help [search]",
3571
- execute(args, _ctx) {
3572
- const filter = args.join(" ").trim().toLowerCase();
3573
- const groups = [
3574
- {
3575
- title: "Chat & model",
3576
- rows: [
3577
- ["/provider <name>", "Switch AI provider"],
3578
- ["/model [name|refresh|cache]", "Switch model or refresh live model cache"],
3579
- ["/route [on|off|show|test <msg>]", "Smart model routing rules"],
3580
- ["/think [on|off|low|high|max|status]", "Control Extended Thinking and effort"],
3581
- ["/plan [execute|exit|status]", "Enter/manage Plan Mode (read-only)"],
3582
- ["/auto [on|off|status]", "Rule-based low-risk auto approval"],
3583
- ["/permissions recently-denied|clear-denied", "Inspect permission and Auto Mode decisions"],
3584
- ["/yolo [on|off]", "Toggle write auto-approve (destructive still confirms)"]
3585
- ]
3586
- },
3587
- {
3588
- title: "Session",
3589
- rows: [
3590
- ["/clear", "Clear conversation history"],
3591
- ["/compact [instruction]", "Summarize & compress history"],
3592
- ["/session new|list [--filter k=v]|load|show|page|remove", "Manage saved sessions (list: --filter provider=/model=/messages=)"],
3593
- ["/checkpoint [save|restore|delete] <name>", "Session checkpoints"],
3594
- ["/fork [checkpoint]", "Fork session from checkpoint or current"],
3595
- ["/branch [list|new|switch|delete|rename|diff|cherry-pick]", "Conversation branches"],
3596
- ["/rewind [list|<n>]", "Rewind to message N (restores files too)"],
3597
- ["/export [md|json] [file]", "Export session to file"],
3598
- ["/snapshot", "Save dev-state handoff snapshot now (auto-runs on /exit)"],
3599
- ["/cost [reset|history]", "Session token usage / cost dashboard"],
3600
- ["/status", "Show current status"]
3601
- ]
3602
- },
3603
- {
3604
- title: "Tools, context & memory",
3605
- rows: [
3606
- ["/tools", "List all AI tools available"],
3607
- ["/plugin [list|install|enable|disable|inspect|trust]", "Manage plugins (package + legacy JS)"],
3608
- ["/mcp [reconnect [serverId]|trust-project]", "Show MCP server connections and tools"],
3609
- ["/skill [name|off|list]", "Manage agent skills (reusable prompt packs)"],
3610
- ["/context", "Show or reload hierarchical context layers"],
3611
- ["/add-dir [path|remove]", "Add/remove a directory from AI context"],
3612
- ["/memory [show|add|clear|recall <q>|rebuild|...]", "Persistent memory + chat recall"],
3613
- ["/system <prompt>", "Set system prompt"],
3614
- ["/index [status|rebuild|clear|semantic-*]", "Symbol + semantic code index"],
3615
- ["/commands [reload]", "List/reload custom commands"]
3616
- ]
3617
- },
3618
- {
3619
- title: "Files & code",
3620
- rows: [
3621
- ["/init [--force]", "Generate AICLI.md by scanning project"],
3622
- ["/scaffold <description>", "Generate project scaffolding with AI"],
3623
- ["/review [--staged] [--detailed]", "AI code review from git diff"],
3624
- ["/security-review [--staged]", "Security vulnerability scan"],
3625
- ["/test [command|filter]", "Run project tests, structured report"],
3626
- ["/diff [--stats]", "All file modifications in this session"],
3627
- ["/undo [list|<n>]", "Undo file ops"],
3628
- ["/search <keyword>", "Search across all session histories"]
3629
- ]
3630
- },
3631
- {
3632
- title: "Clipboard & I/O",
3633
- rows: [
3634
- ["/copy", "Copy last AI response to clipboard"],
3635
- ["/paste [description]", "Read image from clipboard and send to AI"]
3636
- ]
3637
- },
3638
- {
3639
- title: "System",
3640
- rows: [
3641
- ["/config [set|get|show]", "Config wizard / get / set / show all"],
3642
- ["/profile [show|set|clear]", "View or edit your identity"],
3643
- ["/security [status|scan|on|off]", "Sensitive-data redaction"],
3644
- ["/hooks [list|trust|untrust]", "Lifecycle hooks and project hook trust"],
3645
- ["/doctor", "Health check (API keys, config, MCP, crashes, tool stats)"],
3646
- ["/bug [--copy]", "Generate bug report template"],
3647
- ["/about", "About ai-cli & author info"],
3648
- ["/help [search]", "This help \u2014 pass a term to filter"],
3649
- ["/exit", "Exit"]
3650
- ]
3651
- }
3652
- ];
3653
- const filterFn = (row) => !filter || row[0].toLowerCase().includes(filter) || row[1].toLowerCase().includes(filter);
3654
- const surviving = groups.flatMap((g) => g.rows.filter(filterFn));
3655
- if (surviving.length === 0) {
3656
- console.log(`
3657
- No commands match "${filter}".
3658
- `);
3659
- return;
3660
- }
3661
- const cmdWidth = Math.min(48, Math.max(...surviving.map((r) => r[0].length)) + 2);
3662
- console.log();
3663
- if (filter) console.log(theme.dim(` Filtering by "${filter}"
3664
- `));
3665
- for (const group of groups) {
3666
- const rows = group.rows.filter(filterFn);
3667
- if (rows.length === 0) continue;
3668
- console.log(theme.heading(` ${group.title}`));
3669
- for (const [cmd, desc] of rows) {
3670
- console.log(` ${theme.accent(cmd.padEnd(cmdWidth))} ${theme.dim(desc)}`);
3671
- }
3672
- console.log();
3673
- }
3674
- }
3675
- },
3676
- {
3677
- name: "about",
3678
- description: "Show information about ai-cli and its author",
3679
- usage: "/about",
3680
- execute(_args, ctx) {
3681
- const manager = ctx.getMcpManager();
3682
- const mcpInfo = manager ? { servers: manager.getConnectedCount(), tools: manager.getTotalToolCount() } : void 0;
3683
- const builtinTools = ctx.tools.listBuiltinTools().map((t) => ({ name: t.definition.name, description: t.definition.description }));
3684
- ctx.renderer.printAbout({
3685
- pluginCount: ctx.tools.listPluginTools().length,
3686
- mcpInfo,
3687
- builtinTools,
3688
- commandNames: ctx.getCommandNames()
3689
- });
3690
- }
3691
- },
3692
- {
3693
- name: "search",
3694
- description: "Search across all session histories",
3695
- usage: "/search <keyword>",
3696
- execute(args, ctx) {
3697
- const query = args.join(" ").trim();
3698
- if (!query) {
3699
- ctx.renderer.renderError("Usage: /search <keyword>");
3700
- return;
462
+ console.log();
463
+ console.log(theme.dim(" \u2726 ") + theme.accent("Full per-version history \u2192 CHANGELOG.md"));
464
+ console.log();
465
+ }
466
+ printPrompt(provider, _model) {
467
+ process.stdout.write(theme.prompt(`[${provider}]`) + chalk.white(" > "));
468
+ }
469
+ async renderStream(stream, options) {
470
+ process.stdout.write(theme.accent("Assistant: "));
471
+ let fileStream = null;
472
+ if (options?.saveToFile) {
473
+ mkdirSync(dirname(options.saveToFile), { recursive: true });
474
+ fileStream = createWriteStream(options.saveToFile, { encoding: "utf-8" });
475
+ }
476
+ let fullContent = "";
477
+ let usage;
478
+ const thinkFilter = new ThinkTagFilter();
479
+ const writeVisible = (visible) => {
480
+ if (!visible) return;
481
+ process.stdout.write(visible);
482
+ if (fileStream) fileStream.write(visible);
483
+ fullContent += visible;
484
+ };
485
+ const emitText = (raw) => writeVisible(thinkFilter.push(raw));
486
+ const flushTail = () => writeVisible(thinkFilter.flush());
487
+ let interrupted = false;
488
+ let streamErr = null;
489
+ try {
490
+ for await (const chunk of stream) {
491
+ if (options?.signal?.aborted) {
492
+ interrupted = true;
493
+ break;
3701
494
  }
3702
- const results = ctx.sessions.searchMessages(query);
3703
- if (results.length === 0) {
3704
- ctx.renderer.printInfo(`No sessions found containing "${query}".`);
3705
- return;
495
+ if (chunk.usage) {
496
+ usage = chunk.usage;
3706
497
  }
3707
- console.log(`
3708
- ${theme.heading(`Found ${results.length} session(s) containing "${query}"`)}
3709
- `);
3710
- for (const r of results) {
3711
- const { sessionMeta, matches } = r;
3712
- const dateStr = sessionMeta.updated.toLocaleDateString();
3713
- console.log(
3714
- ` ${theme.accent(sessionMeta.id.slice(0, 8))}` + theme.dim(` [${dateStr}] ${sessionMeta.provider} / ${sessionMeta.model}`) + (sessionMeta.title ? `
3715
- ${theme.dim(" " + sessionMeta.title)}` : "")
3716
- );
3717
- for (const m of matches) {
3718
- const icon = m.role === "user" ? "\u{1F464}" : "\u{1F916}";
3719
- console.log(` ${icon} ${theme.warning(m.snippet)}`);
3720
- }
3721
- console.log();
498
+ if (chunk.done) {
499
+ break;
3722
500
  }
3723
- console.log(theme.dim(" Use /session load <id> to resume a session.\n"));
501
+ if (!chunk.delta) continue;
502
+ emitText(chunk.delta);
3724
503
  }
3725
- },
3726
- {
3727
- name: "undo",
3728
- description: "Undo file operations (supports: /undo, /undo list, /undo <n>)",
3729
- usage: "/undo [list | <n>]",
3730
- execute(args, ctx) {
3731
- const sub = args.join(" ").trim();
3732
- if (sub === "list") {
3733
- const history = undoStack.getHistory();
3734
- if (history.length === 0) {
3735
- ctx.renderer.printInfo("Undo stack is empty.");
3736
- return;
3737
- }
3738
- console.log(theme.heading("\n Undo Stack (" + history.length + " entries, newest last):\n"));
3739
- history.forEach((entry, i) => {
3740
- const timeStr = entry.timestamp.toLocaleTimeString();
3741
- const typeTag = entry.isDirectory ? theme.accent("[dir]") : entry.previousContent === null ? theme.accent("[new]") : theme.dim("[mod]");
3742
- console.log(` ${theme.dim(String(i + 1).padStart(3))} ${typeTag} ${entry.description} ${theme.dim(timeStr)}`);
3743
- });
3744
- console.log("");
3745
- return;
3746
- }
3747
- const n = sub ? parseInt(sub, 10) : 1;
3748
- if (isNaN(n) || n < 1) {
3749
- ctx.renderer.printInfo("Usage: /undo [list | <n>] \u2014 n must be a positive integer.");
3750
- return;
3751
- }
3752
- if (undoStack.depth === 0) {
3753
- ctx.renderer.printInfo("Nothing to undo.");
3754
- return;
3755
- }
3756
- const count = Math.min(n, undoStack.depth);
3757
- const results = [];
3758
- for (let i = 0; i < count; i++) {
3759
- const top = undoStack.peek();
3760
- if (!top) break;
3761
- const timeStr = top.timestamp.toLocaleTimeString();
3762
- ctx.renderer.printInfo(`Undoing [${i + 1}/${count}]: ${top.description} (${timeStr})`);
3763
- const undoResult = undoStack.undo();
3764
- if (undoResult) {
3765
- results.push(undoResult.result);
3766
- ctx.renderer.printSuccess(" " + undoResult.result);
3767
- }
3768
- }
3769
- if (results.length === 0) {
3770
- ctx.renderer.printInfo("Nothing to undo.");
3771
- } else if (results.length > 1) {
3772
- console.log(theme.success(`
3773
- \u2713 ${results.length} operations undone.
3774
- `));
3775
- }
504
+ } catch (err) {
505
+ if (err?.name === "AbortError") {
506
+ interrupted = true;
507
+ } else {
508
+ streamErr = err;
3776
509
  }
3777
- },
3778
- {
3779
- name: "copy",
3780
- description: "Copy last AI response to system clipboard",
3781
- usage: "/copy",
3782
- execute(_args, ctx) {
3783
- const content = ctx.getLastResponse();
3784
- if (!content) {
3785
- ctx.renderer.printInfo("No AI response to copy.");
3786
- return;
3787
- }
3788
- try {
3789
- copyToClipboard(content);
3790
- const chars = content.length;
3791
- const lines = content.split("\n").length;
3792
- ctx.renderer.printSuccess(`Copied to clipboard (${lines} lines, ${chars} chars)`);
3793
- } catch (err) {
3794
- const msg = err instanceof Error ? err.message : String(err);
3795
- if (platform3() === "linux" && msg.includes("not found")) {
3796
- ctx.renderer.renderError("Clipboard tool not found. Install xclip or xsel: sudo apt install xclip");
3797
- } else {
3798
- ctx.renderer.renderError(`Failed to copy: ${msg}`);
3799
- }
510
+ }
511
+ flushTail();
512
+ if (interrupted) {
513
+ process.stdout.write(theme.dim(" [interrupted]\n"));
514
+ }
515
+ let tokensShown = false;
516
+ if (!streamErr) {
517
+ if (options?.showTokens) {
518
+ process.stdout.write("\n");
519
+ if (usage) {
520
+ const sessionTotal = options.sessionTotal;
521
+ const updatedTotal = sessionTotal ? { inputTokens: sessionTotal.inputTokens + usage.inputTokens, outputTokens: sessionTotal.outputTokens + usage.outputTokens } : void 0;
522
+ this.renderUsage(usage, updatedTotal);
523
+ tokensShown = true;
524
+ } else if (fullContent.length > 0) {
525
+ const est = Math.ceil(fullContent.length / 2.5);
526
+ process.stdout.write(theme.dim(`\u{1F4CA} ~${est.toLocaleString()} output tokens (estimated)
527
+
528
+ `));
529
+ tokensShown = true;
530
+ } else {
531
+ process.stdout.write("\n");
3800
532
  }
533
+ } else {
534
+ process.stdout.write("\n\n");
3801
535
  }
3802
- },
3803
- {
3804
- name: "paste",
3805
- description: "Read image from clipboard and send with optional description",
3806
- usage: "/paste [description]",
3807
- async execute(args, ctx) {
3808
- const imgPath = readClipboardImage();
3809
- if (!imgPath) {
3810
- const hint = getClipboardHint();
3811
- ctx.renderer.renderError(
3812
- "No image found in clipboard. Please copy an image in another application first, then run /paste." + (hint ? `
3813
- ${hint}` : "")
3814
- );
3815
- return;
3816
- }
3817
- const description = args.join(" ").trim() || "Please describe this image";
3818
- ctx.renderer.printSuccess(`\u{1F4CB} Image loaded: ${basename(imgPath)}`);
3819
- await ctx.sendAsChat(`@${imgPath} ${description}`);
536
+ } else {
537
+ process.stdout.write("\n");
538
+ }
539
+ if (fileStream) {
540
+ try {
541
+ await new Promise((resolve3) => {
542
+ fileStream.end(() => resolve3());
543
+ });
544
+ } catch {
3820
545
  }
3821
- },
3822
- {
3823
- name: "exit",
3824
- description: "Exit the REPL",
3825
- usage: "/exit",
3826
- execute(_args, ctx) {
3827
- ctx.exit();
546
+ if (!streamErr) {
547
+ const kb = (Buffer.byteLength(fullContent, "utf-8") / 1024).toFixed(1);
548
+ process.stdout.write(theme.success(` \u2705 Saved: ${options.saveToFile} (${kb} KB)
549
+
550
+ `));
3828
551
  }
3829
552
  }
3830
- ];
3831
- }
3832
-
3833
- // src/repl/commands/agent-commands.ts
3834
- function createAgentCommands() {
3835
- return [
3836
- {
3837
- name: "auto",
3838
- description: "Toggle rule-based Auto Mode for low-risk actions",
3839
- usage: "/auto [on|off|status]",
3840
- execute(args, ctx) {
3841
- const sub = args[0]?.toLowerCase() ?? "status";
3842
- const executor = ctx.getToolExecutor();
3843
- if (sub === "on") {
3844
- executor.sessionAutoMode = true;
3845
- console.log(theme.warning(" \u26A1 Auto Mode ON \u2014 low-risk actions may auto-run; high-risk actions still confirm or deny."));
3846
- console.log(theme.dim(" Auto Mode is session-scoped and is not enabled by project config."));
3847
- } else if (sub === "off") {
3848
- executor.sessionAutoMode = false;
3849
- console.log(theme.info(" Auto Mode disabled."));
3850
- } else if (sub === "status") {
3851
- console.log(` Auto Mode : ${executor.sessionAutoMode ? theme.warning("on") : theme.dim("off")}`);
3852
- console.log(theme.dim(" Scope : current session only"));
3853
- console.log(theme.dim(" Denied : /permissions recently-denied"));
3854
- } else {
3855
- console.log(theme.warning(" Usage: /auto [on|off|status]"));
3856
- }
3857
- console.log();
553
+ if (streamErr) throw streamErr;
554
+ return { content: fullContent, usage, tokensShown };
555
+ }
556
+ renderResponse(content) {
557
+ process.stdout.write(theme.accent("Assistant: "));
558
+ let displayed = content;
559
+ if (content.includes("<think>")) {
560
+ displayed = content.replace(/<think>[\s\S]*?<\/think>/g, "").trimStart();
561
+ }
562
+ const finalOutput = this.wrapWidth > 0 ? wrapText(displayed, this.wrapWidth) : displayed;
563
+ process.stdout.write(finalOutput);
564
+ process.stdout.write("\n\n");
565
+ }
566
+ /**
567
+ * 将已有 content 以模拟流式效果逐块输出(打字机体验),零额外 API 调用。
568
+ * 支持 AbortSignal 中断。
569
+ */
570
+ async renderContentAsStream(content, options) {
571
+ process.stdout.write(theme.accent("Assistant: "));
572
+ let displayed = content;
573
+ if (content.includes("<think>")) {
574
+ displayed = content.replace(/<think>[\s\S]*?<\/think>/g, "").trimStart();
575
+ }
576
+ if (this.wrapWidth > 0) {
577
+ displayed = wrapText(displayed, this.wrapWidth);
578
+ }
579
+ const CHUNK_SIZE = 12;
580
+ const DELAY_MS = 8;
581
+ let pos = 0;
582
+ while (pos < displayed.length) {
583
+ if (options?.signal?.aborted) {
584
+ process.stdout.write(theme.dim("\n[interrupted]"));
585
+ break;
3858
586
  }
3859
- },
3860
- {
3861
- name: "permissions",
3862
- description: "Inspect permission and Auto Mode decisions",
3863
- usage: "/permissions recently-denied|clear-denied",
3864
- execute(args) {
3865
- const sub = args[0]?.toLowerCase() ?? "recently-denied";
3866
- if (sub === "recently-denied") {
3867
- const rows = getRecentlyDeniedAutoActions();
3868
- if (rows.length === 0) {
3869
- console.log(theme.success(" No recently denied Auto Mode actions."));
3870
- } else {
3871
- console.log(theme.warning(` Recently denied Auto Mode actions (${rows.length})`));
3872
- for (const r of rows.slice(0, 20)) {
3873
- console.log(` - ${r.timestamp} ${theme.error(r.tool)} ${theme.dim(r.ruleId)}`);
3874
- console.log(` ${r.reason}`);
3875
- console.log(theme.dim(` ${r.argsPreview}`));
3876
- }
3877
- }
3878
- console.log();
3879
- return;
3880
- }
3881
- if (sub === "clear-denied") {
3882
- clearRecentlyDeniedAutoActions();
3883
- console.log(theme.info(" Cleared recently denied Auto Mode actions."));
3884
- console.log();
3885
- return;
3886
- }
3887
- console.log(theme.warning(" Usage: /permissions recently-denied|clear-denied"));
3888
- console.log();
587
+ const end = Math.min(pos + CHUNK_SIZE, displayed.length);
588
+ process.stdout.write(displayed.slice(pos, end));
589
+ pos = end;
590
+ if (pos < displayed.length) {
591
+ await new Promise((resolve3) => setTimeout(resolve3, DELAY_MS));
3889
592
  }
3890
- },
3891
- {
3892
- name: "yolo",
3893
- description: "Toggle session auto-approve for write tools (/yolo)",
3894
- usage: "/yolo [on|off|status]",
3895
- execute(args, ctx) {
3896
- const sub = args[0]?.toLowerCase();
3897
- const executor = ctx.getToolExecutor();
3898
- if (sub === "status") {
3899
- console.log(theme.info(` YOLO mode: ${executor.sessionAutoApprove ? "ON (write tools auto-approved)" : "OFF"}`));
3900
- console.log();
3901
- return;
3902
- }
3903
- if (sub !== void 0 && sub !== "on" && sub !== "off") {
3904
- console.log(theme.warning(` Unknown /yolo option "${sub}" \u2014 expected on | off | status`));
3905
- console.log();
3906
- return;
3907
- }
3908
- if (sub === "off") {
3909
- executor.sessionAutoApprove = false;
3910
- console.log(theme.info(" \u{1F512} Auto-approve disabled \u2014 confirmations restored for this session."));
3911
- } else {
3912
- executor.sessionAutoApprove = true;
3913
- console.log(theme.warning(" \u26A1 YOLO mode ON \u2014 write tools auto-approved; destructive tools still require confirmation."));
3914
- console.log(theme.dim(" Use /yolo off to re-enable confirmations."));
3915
- }
3916
- console.log();
593
+ }
594
+ process.stdout.write("\n\n");
595
+ }
596
+ renderError(err) {
597
+ const message = err instanceof Error ? err.message : String(err);
598
+ const lines = [message];
599
+ if (err instanceof Error && err.cause) {
600
+ let cause = err.cause;
601
+ let depth = 0;
602
+ while (cause && depth < 3) {
603
+ const causeMsg = cause instanceof Error ? cause.message : String(cause);
604
+ lines.push(` Caused by: ${causeMsg}`);
605
+ cause = cause instanceof Error ? cause.cause : void 0;
606
+ depth++;
3917
607
  }
3918
608
  }
3919
- ];
3920
- }
3921
-
3922
- // src/repl/commands/types.ts
3923
- var CommandRegistry = class {
3924
- commands = /* @__PURE__ */ new Map();
3925
- register(command) {
3926
- this.commands.set(command.name, command);
609
+ const typeName = err instanceof Error && err.name !== "Error" ? ` [${err.name}]` : "";
610
+ console.error(theme.error(`
611
+ Error${typeName}: ${lines.join("\n")}
612
+ `));
3927
613
  }
3928
- get(name) {
3929
- return this.commands.get(name);
614
+ /**
615
+ * 在 AI 回复后显示 token 用量。
616
+ * @param usage 本次调用的 input/output tokens
617
+ * @param sessionTotal 本次会话累计 tokens(可选)
618
+ */
619
+ renderUsage(usage, sessionTotal) {
620
+ const total = usage.inputTokens + usage.outputTokens;
621
+ let line = theme.dim("\u{1F4CA} ") + theme.dim(`in ${usage.inputTokens.toLocaleString()}`) + theme.dim(" + ") + theme.dim(`out ${usage.outputTokens.toLocaleString()}`) + theme.dim(` = ${total.toLocaleString()} tokens`);
622
+ const cacheRead = usage.cacheReadTokens ?? 0;
623
+ if (cacheRead > 0) {
624
+ line += theme.dim(` \u2502 cache: ${cacheRead.toLocaleString()}`);
625
+ }
626
+ if (sessionTotal) {
627
+ const sessionSum = sessionTotal.inputTokens + sessionTotal.outputTokens + (sessionTotal.cacheCreationTokens ?? 0) + (sessionTotal.cacheReadTokens ?? 0);
628
+ line += theme.dim(` \u2502 session total: ${sessionSum.toLocaleString()}`);
629
+ }
630
+ process.stdout.write(line + "\n\n");
3930
631
  }
3931
- listAll() {
3932
- return [...this.commands.values()];
632
+ printInfo(message) {
633
+ console.log(theme.warning(` ${message}`));
634
+ }
635
+ printSuccess(message) {
636
+ console.log(theme.success(` ${message}`));
637
+ }
638
+ showSpinner(text) {
639
+ return createSpinner(text);
640
+ }
641
+ printTable(headers, rows) {
642
+ const colWidths = headers.map((h, i) => {
643
+ const maxRow = Math.max(...rows.map((r) => stripAnsi(String(r[i] ?? "")).length));
644
+ return Math.max(stripAnsi(h).length, maxRow);
645
+ });
646
+ const separator = colWidths.map((w) => "-".repeat(w + 2)).join("+");
647
+ const headerLine = headers.map((h, i) => ` ${h.padEnd(colWidths[i])} `).join("|");
648
+ console.log(theme.dim(separator));
649
+ console.log(theme.heading(headerLine));
650
+ console.log(theme.dim(separator));
651
+ for (const row of rows) {
652
+ const line = row.map((cell, i) => {
653
+ const val = String(cell ?? "");
654
+ const colored = cell === true ? theme.success(val) : cell === false ? theme.error(val) : val;
655
+ const visLen = stripAnsi(colored).length;
656
+ const pad = Math.max(0, colWidths[i] - visLen);
657
+ return ` ${colored}${" ".repeat(pad)} `;
658
+ }).join("|");
659
+ console.log(line);
660
+ }
661
+ console.log(theme.dim(separator));
3933
662
  }
3934
663
  };
3935
664
 
3936
- // src/repl/commands/index.ts
3937
- function createDefaultCommands() {
3938
- return [
3939
- ...createMiscCommands(),
3940
- ...createAgentCommands(),
3941
- ...createProviderCommands(),
3942
- ...createToolingCommands(),
3943
- ...createSecurityCommands(),
3944
- ...createReviewCommands(),
3945
- ...createMemoryCommands(),
3946
- ...createSessionCommands()
3947
- ];
3948
- }
3949
-
3950
665
  // src/repl/select-list.ts
3951
- import chalk4 from "chalk";
666
+ import chalk2 from "chalk";
3952
667
  var CLEAR_LINE = "\r\x1B[2K";
3953
668
  var MOVE_UP = (n) => n > 0 ? `\x1B[${n}A` : "";
3954
669
  var IGNORE_ENTER_MS = 80;
3955
670
  function selectFromList(prompt, items, initialIndex = 0) {
3956
671
  if (items.length === 0) return Promise.resolve(null);
3957
672
  const PAGE = 12;
3958
- return new Promise((resolve4) => {
673
+ return new Promise((resolve3) => {
3959
674
  let selected = Math.max(0, Math.min(initialIndex, items.length - 1));
3960
675
  let windowStart = Math.max(0, selected - Math.floor(PAGE / 2));
3961
676
  let lastRenderedLines = 0;
@@ -3973,10 +688,10 @@ function selectFromList(prompt, items, initialIndex = 0) {
3973
688
  handleSequence(seq);
3974
689
  };
3975
690
  const renderLine = (item, active, absIdx) => {
3976
- const cursor = active ? chalk4.cyan("\u276F ") : " ";
3977
- const label = active ? chalk4.cyan(item.label) : item.label;
3978
- const hint = item.hint ? chalk4.dim(" " + item.hint) : "";
3979
- const num = chalk4.dim(String(absIdx + 1).padStart(2) + " ");
691
+ const cursor = active ? chalk2.cyan("\u276F ") : " ";
692
+ const label = active ? chalk2.cyan(item.label) : item.label;
693
+ const hint = item.hint ? chalk2.dim(" " + item.hint) : "";
694
+ const num = chalk2.dim(String(absIdx + 1).padStart(2) + " ");
3980
695
  return cursor + num + label + hint;
3981
696
  };
3982
697
  const render = () => {
@@ -3985,16 +700,16 @@ function selectFromList(prompt, items, initialIndex = 0) {
3985
700
  const windowEnd = Math.min(windowStart + PAGE, items.length);
3986
701
  const visible = items.slice(windowStart, windowEnd);
3987
702
  const lines = [];
3988
- lines.push(chalk4.bold(prompt));
703
+ lines.push(chalk2.bold(prompt));
3989
704
  if (windowStart > 0)
3990
- lines.push(chalk4.dim(` \u2191 ${windowStart} more above`));
705
+ lines.push(chalk2.dim(` \u2191 ${windowStart} more above`));
3991
706
  visible.forEach(
3992
707
  (item, i) => lines.push(renderLine(item, windowStart + i === selected, windowStart + i))
3993
708
  );
3994
709
  const below = items.length - windowEnd;
3995
710
  if (below > 0)
3996
- lines.push(chalk4.dim(` \u2193 ${below} more below`));
3997
- lines.push(chalk4.dim(" \u2191\u2193 move \xB7 Enter select \xB7 Esc cancel"));
711
+ lines.push(chalk2.dim(` \u2193 ${below} more below`));
712
+ lines.push(chalk2.dim(" \u2191\u2193 move \xB7 Enter select \xB7 Esc cancel"));
3998
713
  if (lastRenderedLines > 0) {
3999
714
  process.stdout.write(MOVE_UP(lastRenderedLines) + CLEAR_LINE);
4000
715
  for (let i = 1; i < lastRenderedLines; i++)
@@ -4027,10 +742,10 @@ function selectFromList(prompt, items, initialIndex = 0) {
4027
742
  process.stdout.write(MOVE_UP(lastRenderedLines - 1));
4028
743
  }
4029
744
  if (result !== null) {
4030
- process.stdout.write(chalk4.dim(` \u2714 ${result}
745
+ process.stdout.write(chalk2.dim(` \u2714 ${result}
4031
746
  `));
4032
747
  }
4033
- resolve4(result);
748
+ resolve3(result);
4034
749
  };
4035
750
  const handleSequence = (seq) => {
4036
751
  if (seq === "\x1B") {
@@ -4094,13 +809,13 @@ function selectFromList(prompt, items, initialIndex = 0) {
4094
809
  }
4095
810
  };
4096
811
  if (!process.stdin.isTTY) {
4097
- resolve4(items[0]?.value ?? null);
812
+ resolve3(items[0]?.value ?? null);
4098
813
  return;
4099
814
  }
4100
815
  try {
4101
816
  process.stdin.setRawMode(true);
4102
817
  } catch {
4103
- resolve4(items[0]?.value ?? null);
818
+ resolve3(items[0]?.value ?? null);
4104
819
  return;
4105
820
  }
4106
821
  savedDataListeners = process.stdin.rawListeners("data");
@@ -4121,12 +836,12 @@ var streamToFileContext = {
4121
836
  };
4122
837
 
4123
838
  // src/repl/custom-commands.ts
4124
- import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync, mkdirSync as mkdirSync3 } from "fs";
4125
- import { join as join3, extname } from "path";
839
+ import { existsSync, readFileSync, readdirSync, mkdirSync as mkdirSync2 } from "fs";
840
+ import { join, extname } from "path";
4126
841
  function parseCommandFile(filePath) {
4127
842
  let content;
4128
843
  try {
4129
- content = readFileSync2(filePath, "utf-8");
844
+ content = readFileSync(filePath, "utf-8");
4130
845
  } catch {
4131
846
  return null;
4132
847
  }
@@ -4150,7 +865,7 @@ function expandTemplate(template, args) {
4150
865
  result = result.replace(/\{\{input\}\}/g, args.join(" "));
4151
866
  result = result.replace(/\{\{file:([^}]+)\}\}/g, (_m, p) => {
4152
867
  try {
4153
- return readFileSync2(p.trim(), "utf-8");
868
+ return readFileSync(p.trim(), "utf-8");
4154
869
  } catch {
4155
870
  return `[Error: cannot read ${p.trim()}]`;
4156
871
  }
@@ -4181,13 +896,13 @@ var CustomCommandManager = class {
4181
896
  return this.commands.size;
4182
897
  }
4183
898
  loadCommandDir(dir, createIfMissing) {
4184
- if (!existsSync3(dir)) {
4185
- if (createIfMissing) mkdirSync3(dir, { recursive: true });
899
+ if (!existsSync(dir)) {
900
+ if (createIfMissing) mkdirSync2(dir, { recursive: true });
4186
901
  return;
4187
902
  }
4188
903
  for (const file of readdirSync(dir)) {
4189
904
  if (extname(file) !== ".md") continue;
4190
- const cmd = parseCommandFile(join3(dir, file));
905
+ const cmd = parseCommandFile(join(dir, file));
4191
906
  if (cmd && !this.commands.has(cmd.meta.name)) this.commands.set(cmd.meta.name, cmd);
4192
907
  }
4193
908
  }
@@ -4200,8 +915,8 @@ var CustomCommandManager = class {
4200
915
  };
4201
916
 
4202
917
  // src/repl/context-dir-scanner.ts
4203
- import { readdirSync as readdirSync2, statSync as statSync2, readFileSync as readFileSync3 } from "fs";
4204
- import { join as join4, extname as extname2 } from "path";
918
+ import { readdirSync as readdirSync2, statSync, readFileSync as readFileSync2 } from "fs";
919
+ import { join as join2, extname as extname2 } from "path";
4205
920
  var MAX_FILE_CHARS = 4e3;
4206
921
  var MAX_TOTAL_CHARS = 4e4;
4207
922
  var MAX_TREE_ENTRIES = 60;
@@ -4276,12 +991,12 @@ function scanDirContent(absPath) {
4276
991
  const filtered = entries.filter((e) => !SKIP_DIRS.has(e));
4277
992
  for (let i = 0; i < filtered.length && entryCount < MAX_TREE_ENTRIES; i++) {
4278
993
  const name = filtered[i];
4279
- const fullPath = join4(dir, name);
994
+ const fullPath = join2(dir, name);
4280
995
  const isLast = i === filtered.length - 1;
4281
996
  const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
4282
997
  let isDir;
4283
998
  try {
4284
- isDir = statSync2(fullPath).isDirectory();
999
+ isDir = statSync(fullPath).isDirectory();
4285
1000
  } catch {
4286
1001
  continue;
4287
1002
  }
@@ -4310,10 +1025,10 @@ ${treeLines.join("\n")}`
4310
1025
  for (const name of entries) {
4311
1026
  if (totalChars >= MAX_TOTAL_CHARS) break;
4312
1027
  if (SKIP_DIRS.has(name)) continue;
4313
- const fullPath = join4(dir, name);
1028
+ const fullPath = join2(dir, name);
4314
1029
  let st;
4315
1030
  try {
4316
- st = statSync2(fullPath);
1031
+ st = statSync(fullPath);
4317
1032
  } catch {
4318
1033
  continue;
4319
1034
  }
@@ -4325,7 +1040,7 @@ ${treeLines.join("\n")}`
4325
1040
  if (!TEXT_EXTS.has(ext) && !isSpecial) continue;
4326
1041
  if (st.size > MAX_FILE_CHARS * 3) continue;
4327
1042
  try {
4328
- let content = readFileSync3(fullPath, "utf-8");
1043
+ let content = readFileSync2(fullPath, "utf-8");
4329
1044
  if (content.length > MAX_FILE_CHARS) {
4330
1045
  content = content.slice(0, MAX_FILE_CHARS) + `
4331
1046
  ... (truncated, ${content.length} chars total)`;
@@ -4374,8 +1089,8 @@ function getVisionModelHint(model, provider) {
4374
1089
  }
4375
1090
 
4376
1091
  // src/repl/at-references.ts
4377
- import { existsSync as existsSync4, readFileSync as readFileSync4, statSync as statSync3 } from "fs";
4378
- import { extname as extname3, resolve as resolve2 } from "path";
1092
+ import { existsSync as existsSync2, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
1093
+ import { extname as extname3, resolve } from "path";
4379
1094
  var IMAGE_MIME = {
4380
1095
  ".png": "image/png",
4381
1096
  ".jpg": "image/jpeg",
@@ -4387,7 +1102,7 @@ var MAX_IMAGE_BYTES = 10 * 1024 * 1024;
4387
1102
  var MAX_PATH_SPACE_TOKENS = 8;
4388
1103
  function expandUnquotedPath(input, pathStart, rawPath, cwd) {
4389
1104
  const plain = { raw: rawPath, consumed: rawPath.length };
4390
- if (existsSync4(resolve2(cwd, rawPath))) return plain;
1105
+ if (existsSync2(resolve(cwd, rawPath))) return plain;
4391
1106
  let acc = rawPath;
4392
1107
  let consumed = rawPath.length;
4393
1108
  let rest = input.slice(pathStart + rawPath.length);
@@ -4397,7 +1112,7 @@ function expandUnquotedPath(input, pathStart, rawPath, cwd) {
4397
1112
  acc += next[1] + next[2];
4398
1113
  consumed += next[0].length;
4399
1114
  rest = rest.slice(next[0].length);
4400
- if (existsSync4(resolve2(cwd, acc))) return { raw: acc, consumed };
1115
+ if (existsSync2(resolve(cwd, acc))) return { raw: acc, consumed };
4401
1116
  }
4402
1117
  return plain;
4403
1118
  }
@@ -4419,20 +1134,20 @@ function parseAtReferences(input, cwd) {
4419
1134
  atPattern.lastIndex = pathStart + expanded.consumed;
4420
1135
  }
4421
1136
  }
4422
- const absPath = resolve2(cwd, rawPath);
1137
+ const absPath = resolve(cwd, rawPath);
4423
1138
  const ext = extname3(rawPath).toLowerCase();
4424
1139
  const mime = IMAGE_MIME[ext];
4425
- if (!existsSync4(absPath)) {
1140
+ if (!existsSync2(absPath)) {
4426
1141
  refs.push({ path: rawPath, type: "notfound" });
4427
1142
  continue;
4428
1143
  }
4429
1144
  if (mime) {
4430
- const fileSize = statSync3(absPath).size;
1145
+ const fileSize = statSync2(absPath).size;
4431
1146
  if (fileSize > MAX_IMAGE_BYTES) {
4432
1147
  refs.push({ path: rawPath, type: "toolarge" });
4433
1148
  continue;
4434
1149
  }
4435
- const data = readFileSync4(absPath).toString("base64");
1150
+ const data = readFileSync3(absPath).toString("base64");
4436
1151
  imageParts.push({
4437
1152
  type: "image_url",
4438
1153
  image_url: { url: `data:${mime};base64,${data}` }
@@ -4440,7 +1155,7 @@ function parseAtReferences(input, cwd) {
4440
1155
  refs.push({ path: rawPath, type: "image" });
4441
1156
  textBody = textBody.replace(matchedText, "").trim();
4442
1157
  } else {
4443
- const content = readFileSync4(absPath, "utf-8");
1158
+ const content = readFileSync3(absPath, "utf-8");
4444
1159
  const inlined = `
4445
1160
 
4446
1161
  [File: ${rawPath}]
@@ -4650,7 +1365,7 @@ function shortErrorTag(err) {
4650
1365
 
4651
1366
  // src/repl/notify.ts
4652
1367
  import { spawn } from "child_process";
4653
- import { platform as platform4 } from "os";
1368
+ import { platform } from "os";
4654
1369
  function safeSpawn(cmd, args) {
4655
1370
  let child;
4656
1371
  try {
@@ -4663,7 +1378,7 @@ function safeSpawn(cmd, args) {
4663
1378
  child.unref();
4664
1379
  }
4665
1380
  function sendNotification(title, body) {
4666
- const plat = platform4();
1381
+ const plat = platform();
4667
1382
  try {
4668
1383
  if (plat === "darwin") {
4669
1384
  const safeTitle = title.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
@@ -4876,13 +1591,13 @@ var Repl = class {
4876
1591
  * 已添加时返回 added=false(无重复添加)。
4877
1592
  */
4878
1593
  addExtraContextDir(dirPath) {
4879
- const absPath = resolve3(dirPath);
4880
- if (!existsSync5(absPath)) {
1594
+ const absPath = resolve2(dirPath);
1595
+ if (!existsSync3(absPath)) {
4881
1596
  return { success: false, charCount: 0, added: false, error: `Directory not found: ${dirPath}` };
4882
1597
  }
4883
1598
  let isDir;
4884
1599
  try {
4885
- isDir = statSync4(absPath).isDirectory();
1600
+ isDir = statSync3(absPath).isDirectory();
4886
1601
  } catch {
4887
1602
  return { success: false, charCount: 0, added: false, error: `Cannot access: ${dirPath}` };
4888
1603
  }
@@ -4899,7 +1614,7 @@ var Repl = class {
4899
1614
  }
4900
1615
  /** 从额外上下文中移除目录。返回 true 表示成功移除,false 表示未找到。 */
4901
1616
  removeExtraContextDir(dirPath) {
4902
- const absPath = resolve3(dirPath);
1617
+ const absPath = resolve2(dirPath);
4903
1618
  const idx = this.extraContextDirs.findIndex((d) => d.dir === absPath);
4904
1619
  if (idx === -1) return false;
4905
1620
  this.extraContextDirs.splice(idx, 1);
@@ -4914,10 +1629,10 @@ var Repl = class {
4914
1629
  const cwd = process.cwd();
4915
1630
  const gitRoot = getGitRoot(cwd);
4916
1631
  const projectRoot = gitRoot ?? cwd;
4917
- const mcpPath = join5(projectRoot, MCP_PROJECT_CONFIG_NAME);
4918
- if (!existsSync5(mcpPath)) return null;
1632
+ const mcpPath = join3(projectRoot, MCP_PROJECT_CONFIG_NAME);
1633
+ if (!existsSync3(mcpPath)) return null;
4919
1634
  try {
4920
- const raw = JSON.parse(readFileSync5(mcpPath, "utf-8"));
1635
+ const raw = JSON.parse(readFileSync4(mcpPath, "utf-8"));
4921
1636
  const servers = raw?.mcpServers;
4922
1637
  if (!servers || typeof servers !== "object") {
4923
1638
  process.stderr.write(
@@ -5251,7 +1966,7 @@ ${ctxLine}` : "")
5251
1966
  const modelParams = this.getModelParams();
5252
1967
  const showsEffort = this.providers.get(this.currentProvider)?.supportsReasoningEffort ?? false;
5253
1968
  const thinkTag = modelParams.thinking ? theme.accent(showsEffort ? `[THINK:${modelParams.thinkingEffort ?? "high"}]` : "[THINK]") : "";
5254
- const promptStr = theme.success(`[${this.currentProvider}]`) + skillTag + planTag + thinkTag + chalk5.white(" > ");
1969
+ const promptStr = theme.success(`[${this.currentProvider}]`) + skillTag + planTag + thinkTag + chalk3.white(" > ");
5255
1970
  this.rl.setPrompt(promptStr);
5256
1971
  }
5257
1972
  notice(line) {
@@ -5389,7 +2104,7 @@ Session '${this.resumeSessionId}' not found.
5389
2104
  process.stdout.write(theme.dim(` \u{1F50C} Plugins loaded: ${pluginCount} tool(s) from plugins/
5390
2105
  `));
5391
2106
  }
5392
- const skillsDir = join5(this.config.getConfigDir(), SKILLS_DIR_NAME);
2107
+ const skillsDir = join3(this.config.getConfigDir(), SKILLS_DIR_NAME);
5393
2108
  this.skillManager = new SkillManager(skillsDir, this.config.get("ui").skillSizeWarn);
5394
2109
  const pluginAssets = getActivePluginAssets(this.config.getConfigDir());
5395
2110
  const skillCount = this.skillManager.loadSkills(pluginAssets.skillDirs);
@@ -5397,7 +2112,7 @@ Session '${this.resumeSessionId}' not found.
5397
2112
  process.stdout.write(theme.dim(` \u{1F3AF} Skills: ${skillCount} available (use /skill to manage)
5398
2113
  `));
5399
2114
  }
5400
- const commandsDir = join5(this.config.getConfigDir(), CUSTOM_COMMANDS_DIR_NAME);
2115
+ const commandsDir = join3(this.config.getConfigDir(), CUSTOM_COMMANDS_DIR_NAME);
5401
2116
  this.customCommandManager = new CustomCommandManager(commandsDir);
5402
2117
  const customCmdCount = this.customCommandManager.loadCommands(pluginAssets.commandDirs);
5403
2118
  if (customCmdCount > 0) {
@@ -5427,8 +2142,8 @@ Session '${this.resumeSessionId}' not found.
5427
2142
  })();
5428
2143
  void (async () => {
5429
2144
  try {
5430
- const { getChatIndexStatus: getChatIndexStatus2, buildChatIndex: buildChatIndex2 } = await import("./chat-index-2D5GK2DB.js");
5431
- const initial = getChatIndexStatus2();
2145
+ const { getChatIndexStatus, buildChatIndex } = await import("./chat-index-2D5GK2DB.js");
2146
+ const initial = getChatIndexStatus();
5432
2147
  this.chatMemoryStatus = {
5433
2148
  exists: initial.exists,
5434
2149
  chunks: initial.chunks,
@@ -5447,8 +2162,8 @@ Session '${this.resumeSessionId}' not found.
5447
2162
  }
5448
2163
  return;
5449
2164
  }
5450
- const stats = await buildChatIndex2({ full: false });
5451
- const after = getChatIndexStatus2();
2165
+ const stats = await buildChatIndex({ full: false });
2166
+ const after = getChatIndexStatus();
5452
2167
  this.chatMemoryStatus = {
5453
2168
  exists: after.exists,
5454
2169
  chunks: after.chunks,
@@ -5531,8 +2246,8 @@ Session '${this.resumeSessionId}' not found.
5531
2246
  `);
5532
2247
  });
5533
2248
  const FAST_MCP_DEADLINE_MS = 1500;
5534
- const deadlinePromise = new Promise((resolve4) => {
5535
- setTimeout(() => resolve4("timeout"), FAST_MCP_DEADLINE_MS).unref?.();
2249
+ const deadlinePromise = new Promise((resolve3) => {
2250
+ setTimeout(() => resolve3("timeout"), FAST_MCP_DEADLINE_MS).unref?.();
5536
2251
  });
5537
2252
  const winner = await Promise.race([
5538
2253
  mcpReadyPromise.then(() => "ready"),
@@ -5574,7 +2289,7 @@ Session '${this.resumeSessionId}' not found.
5574
2289
  });
5575
2290
  this.showPrompt();
5576
2291
  process.stdin.setMaxListeners(20);
5577
- await new Promise((resolve4) => {
2292
+ await new Promise((resolve3) => {
5578
2293
  let processing = false;
5579
2294
  let pasteTimer = null;
5580
2295
  const PASTE_DEBOUNCE_MS = 50;
@@ -5607,7 +2322,7 @@ Session '${this.resumeSessionId}' not found.
5607
2322
  process.stdin.resume();
5608
2323
  this.showPrompt();
5609
2324
  } else {
5610
- resolve4();
2325
+ resolve3();
5611
2326
  }
5612
2327
  }
5613
2328
  };
@@ -5641,7 +2356,7 @@ Session '${this.resumeSessionId}' not found.
5641
2356
  pasteTimer = null;
5642
2357
  }
5643
2358
  if (!processing) {
5644
- resolve4();
2359
+ resolve3();
5645
2360
  }
5646
2361
  });
5647
2362
  });
@@ -6027,18 +2742,18 @@ Session '${this.resumeSessionId}' not found.
6027
2742
  completeFilePath(partial) {
6028
2743
  try {
6029
2744
  const normalized = partial.replace(/\\/g, "/");
6030
- const dir = normalized.includes("/") ? dirname3(normalized) : ".";
6031
- const prefix = normalized.includes("/") ? basename2(normalized) : normalized;
6032
- const absDir = resolve3(process.cwd(), dir);
6033
- if (!existsSync5(absDir)) return [];
2745
+ const dir = normalized.includes("/") ? dirname2(normalized) : ".";
2746
+ const prefix = normalized.includes("/") ? basename(normalized) : normalized;
2747
+ const absDir = resolve2(process.cwd(), dir);
2748
+ if (!existsSync3(absDir)) return [];
6034
2749
  const entries = readdirSync3(absDir);
6035
2750
  const results = [];
6036
2751
  for (const entry of entries) {
6037
2752
  if (entry.startsWith(".")) continue;
6038
2753
  if (!entry.toLowerCase().startsWith(prefix.toLowerCase())) continue;
6039
2754
  try {
6040
- const fullPath = join5(absDir, entry);
6041
- const stat = statSync4(fullPath);
2755
+ const fullPath = join3(absDir, entry);
2756
+ const stat = statSync3(fullPath);
6042
2757
  const rel = dir === "." ? entry : `${dir}/${entry}`;
6043
2758
  results.push(stat.isDirectory() ? `${rel}/` : rel);
6044
2759
  } catch {
@@ -6612,7 +3327,7 @@ ${decision.displayMessage}
6612
3327
  });
6613
3328
  if (decision.kind === "save") {
6614
3329
  try {
6615
- mkdirSync4(dirname3(target), { recursive: true });
3330
+ mkdirSync3(dirname2(target), { recursive: true });
6616
3331
  atomicWriteFileSync(target, decision.body);
6617
3332
  undoStack.push(target, `save_last_response (deferred): ${target}`);
6618
3333
  const lines = decision.body.split("\n").length;
@@ -6729,13 +3444,13 @@ ${decision.displayMessage}
6729
3444
  process.stdout.write(theme.dim(" Press ") + theme.info("y") + theme.dim(" to continue, or ") + theme.info("type a message") + theme.dim(" to redirect AI:\n"));
6730
3445
  process.stdout.write(theme.dim(" (Press ") + theme.info("n") + theme.dim(" or ") + theme.info("Esc") + theme.dim(" to stop)\n"));
6731
3446
  this.teardownInterjectionListener();
6732
- const pauseResponse = await new Promise((resolve4) => {
3447
+ const pauseResponse = await new Promise((resolve3) => {
6733
3448
  const rlWithOutput = rlInternal(this.rl);
6734
3449
  const savedOutput = rlWithOutput.output;
6735
3450
  rlWithOutput.output = process.stdout;
6736
3451
  this.rl.question(theme.warning(" \u25B8 "), (answer) => {
6737
3452
  rlWithOutput.output = savedOutput;
6738
- resolve4(answer.trim());
3453
+ resolve3(answer.trim());
6739
3454
  });
6740
3455
  });
6741
3456
  this.setupInterjectionListener();
@@ -7147,7 +3862,7 @@ Tip: You can continue the conversation by asking the AI to proceed.`
7147
3862
  try {
7148
3863
  await Promise.race([
7149
3864
  this.generateDevStateSnapshot(),
7150
- new Promise((resolve4) => setTimeout(resolve4, 1e4).unref())
3865
+ new Promise((resolve3) => setTimeout(resolve3, 1e4).unref())
7151
3866
  ]);
7152
3867
  } catch {
7153
3868
  }
@@ -7229,8 +3944,8 @@ program.command("config [action] [path] [value...]").description("Configure API
7229
3944
  console.log("Configuration saved.");
7230
3945
  return;
7231
3946
  }
7232
- const { applyConfigCommand: applyConfigCommand2 } = await import("./config-command-E5DHADEV.js");
7233
- const outcome = applyConfigCommand2([action, ...path ? [path] : [], ...valueParts ?? []], config);
3947
+ const { applyConfigCommand } = await import("./config-command-E5DHADEV.js");
3948
+ const outcome = applyConfigCommand([action, ...path ? [path] : [], ...valueParts ?? []], config);
7234
3949
  if (outcome.action === "show") {
7235
3950
  console.log(outcome.json);
7236
3951
  return;
@@ -7275,7 +3990,7 @@ program.command("web").description("Start Web UI server with browser-based chat
7275
3990
  console.error("Error: Invalid port number. Must be between 1 and 65535.");
7276
3991
  process.exit(1);
7277
3992
  }
7278
- const { startWebServer } = await import("./server-W2VIUKTQ.js");
3993
+ const { startWebServer } = await import("./server-A3OX52UC.js");
7279
3994
  await startWebServer({ port, host: options.host });
7280
3995
  });
7281
3996
  program.command("user [action] [username]").description("Manage Web UI users (list | create <name> | delete <name> | reset-password <name> | logout-all <name> | migrate <name>)").action(async (action, username) => {
@@ -7390,7 +4105,7 @@ ${users.length} user(s) registered (auth enabled):
7390
4105
  process.exit(1);
7391
4106
  });
7392
4107
  program.command("sessions").description("List recent conversation sessions").option("-n, --last <n>", "Show only the N most recent sessions", "20").option("--provider <id>", "Filter by provider (e.g. claude, openai)").action(async (options) => {
7393
- const chalk6 = (await import("chalk")).default;
4108
+ const chalk4 = (await import("chalk")).default;
7394
4109
  const config = new ConfigManager();
7395
4110
  const sessions = new SessionManager(config);
7396
4111
  let list = sessions.listSessions();
@@ -7401,7 +4116,7 @@ program.command("sessions").description("List recent conversation sessions").opt
7401
4116
  const limit = Math.max(1, parseInt(options.last ?? "20", 10) || 20);
7402
4117
  list = list.slice(0, limit);
7403
4118
  if (list.length === 0) {
7404
- console.log("\n" + chalk6.dim("No saved sessions.") + "\n");
4119
+ console.log("\n" + chalk4.dim("No saved sessions.") + "\n");
7405
4120
  return;
7406
4121
  }
7407
4122
  const relDate = (d) => {
@@ -7417,36 +4132,36 @@ program.command("sessions").description("List recent conversation sessions").opt
7417
4132
  };
7418
4133
  const truncate = (s, n) => s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
7419
4134
  console.log("");
7420
- const hId = chalk6.bold("ID ");
7421
- const hProvider = chalk6.bold("Provider ");
7422
- const hModel = chalk6.bold("Model ");
7423
- const hMsgs = chalk6.bold("Msgs");
7424
- const hDate = chalk6.bold(" When ");
7425
- const hTitle = chalk6.bold("Title");
4135
+ const hId = chalk4.bold("ID ");
4136
+ const hProvider = chalk4.bold("Provider ");
4137
+ const hModel = chalk4.bold("Model ");
4138
+ const hMsgs = chalk4.bold("Msgs");
4139
+ const hDate = chalk4.bold(" When ");
4140
+ const hTitle = chalk4.bold("Title");
7426
4141
  console.log(` ${hId} ${hProvider} ${hModel} ${hMsgs} ${hDate} ${hTitle}`);
7427
- console.log(" " + chalk6.dim("\u2500".repeat(88)));
4142
+ console.log(" " + chalk4.dim("\u2500".repeat(88)));
7428
4143
  for (const s of list) {
7429
- const id = chalk6.cyan(s.id.slice(0, 8));
7430
- const provider = chalk6.yellow(truncate(s.provider, 11).padEnd(11));
7431
- const model = chalk6.dim(truncate(s.model, 28).padEnd(28));
4144
+ const id = chalk4.cyan(s.id.slice(0, 8));
4145
+ const provider = chalk4.yellow(truncate(s.provider, 11).padEnd(11));
4146
+ const model = chalk4.dim(truncate(s.model, 28).padEnd(28));
7432
4147
  const msgs = String(s.messageCount).padStart(4);
7433
- const when = chalk6.dim(relDate(s.updated).padEnd(12));
7434
- const title = s.title ? s.title : chalk6.dim("(untitled)");
4148
+ const when = chalk4.dim(relDate(s.updated).padEnd(12));
4149
+ const title = s.title ? s.title : chalk4.dim("(untitled)");
7435
4150
  console.log(` ${id} ${provider} ${model} ${msgs} ${when} ${truncate(title, 40)}`);
7436
4151
  }
7437
4152
  const total = sessions.listSessions().length;
7438
4153
  const shown = list.length;
7439
- const footer = shown < total ? chalk6.dim(`
7440
- Showing ${shown} of ${total} sessions \xB7 use -n <count> or --provider <id> to filter`) : chalk6.dim(`
4154
+ const footer = shown < total ? chalk4.dim(`
4155
+ Showing ${shown} of ${total} sessions \xB7 use -n <count> or --provider <id> to filter`) : chalk4.dim(`
7441
4156
  ${total} session${total !== 1 ? "s" : ""} total`);
7442
4157
  console.log(footer + "\n");
7443
4158
  });
7444
4159
  program.command("usage").description("Show token + cost usage grouped by provider/model (cross-session)").option("--days <n>", "Only the last N days (inclusive of today)").option("--month <ym>", "Only a specific month, format YYYY-MM (e.g. 2026-06)").option("--json", "Output as JSON (for scripting)").action(async (options) => {
7445
- const { runUsageCli } = await import("./usage-TXPZY7EB.js");
4160
+ const { runUsageCli } = await import("./usage-ZXNYCAZB.js");
7446
4161
  await runUsageCli(options);
7447
4162
  });
7448
4163
  program.command("doctor").description("Health check: API keys, config, MCP, recent crashes, tool usage, disk usage").option("--json", "Output as JSON (for scripting)").option("--reset-stats", "Reset accumulated tool usage statistics").action(async (options) => {
7449
- const { runDoctorCli } = await import("./doctor-cli-ISFDY7LZ.js");
4164
+ const { runDoctorCli } = await import("./doctor-cli-VLFE46PX.js");
7450
4165
  const argv = process.argv.slice(2);
7451
4166
  await runDoctorCli({
7452
4167
  json: !!options.json || argv.includes("--json"),
@@ -7455,7 +4170,7 @@ program.command("doctor").description("Health check: API keys, config, MCP, rece
7455
4170
  });
7456
4171
  program.command("batch <action> [arg] [arg2]").description("Anthropic Message Batches: submit | list | status <id> | results <id> [out] | cancel <id>").option("--dry-run", "Parse and validate input without submitting (submit only)").action(async (action, arg, arg2, options) => {
7457
4172
  try {
7458
- const batch = await import("./batch-DA7VA35X.js");
4173
+ const batch = await import("./batch-GY3I53DE.js");
7459
4174
  switch (action) {
7460
4175
  case "submit":
7461
4176
  if (!arg) {
@@ -7498,7 +4213,7 @@ program.command("batch <action> [arg] [arg2]").description("Anthropic Message Ba
7498
4213
  }
7499
4214
  });
7500
4215
  program.command("mcp-serve").description("Start an MCP server over STDIO, exposing aicli's built-in tools to Claude Desktop / Cursor / other MCP clients").option("--allow-destructive", "Allow bash / run_interactive / task_create (always destructive in MCP mode)").option("--allow-outside-cwd", "Allow tool path arguments to escape the sandbox root \u2014 disabled by default").option("--tools <list>", "Comma-separated whitelist of tools to expose (default: all eligible tools)").option("--cwd <path>", "Working directory AND sandbox root (default: current directory)").action(async (options) => {
7501
- const { startMcpServer } = await import("./server-KK7DIV3J.js");
4216
+ const { startMcpServer } = await import("./server-7VSLW446.js");
7502
4217
  await startMcpServer({
7503
4218
  allowDestructive: !!options.allowDestructive,
7504
4219
  allowOutsideCwd: !!options.allowOutsideCwd,
@@ -7511,7 +4226,7 @@ program.command("pr <action> [target]").description("PR review helpers: review |
7511
4226
  console.error(`Unknown pr action: ${action}. Use review | security-review | summarize.`);
7512
4227
  process.exit(1);
7513
4228
  }
7514
- const { runPrCli } = await import("./pr-BREZ3DJX.js");
4229
+ const { runPrCli } = await import("./pr-2YUV4KII.js");
7515
4230
  const result = await runPrCli({
7516
4231
  action,
7517
4232
  target,
@@ -7531,8 +4246,8 @@ program.command("pr <action> [target]").description("PR review helpers: review |
7531
4246
  process.exit(result.exitCode);
7532
4247
  });
7533
4248
  program.command("ci").description("Headless PR review (code + security) \u2014 reads git/gh diff, optionally posts to PR. Designed for GitHub Actions.").option("--pr <num>", "PR number or URL; diff fetched via gh pr diff <value>", (v) => /^#?\d+$/.test(v) ? parseInt(v.replace(/^#/, ""), 10) : v).option("--base <ref>", "Base ref for git diff <ref>...HEAD (ignored when --pr set)").option("--post", "Post review as a PR comment (requires gh CLI + GH_TOKEN, needs --pr)").option("--no-update", "Always create a new comment instead of updating the previous aicli review").option("--skip-code", "Skip the code review section").option("--skip-security", "Skip the security review section").option("--detailed", "Use the detailed code-review prompt").option("--max-diff <n>", "Max diff chars sent to the model (default 30000)", (v) => parseInt(v, 10)).option("--provider <id>", "Override provider (default: config.defaultProvider)").option("--model <id>", "Override model").option("--dry-run", "Print result to stdout instead of posting (overrides --post)").option("--json", "Output a structured JSON report").option("--markdown", "Output Markdown report (default)").option("--sarif", "Output SARIF 2.1.0 for code scanning artifacts").option("--fail-on-security-high", "Fail when the security review contains HIGH/CRITICAL findings (default)").option("--no-fail-on-security-high", "Do not fail on security HIGH/CRITICAL findings").option("--fail-on-test-failure", "Fail when the report mentions test failures (default)").option("--no-fail-on-test-failure", "Do not fail on test failure findings").option("--fail-on-lint-failure", "Fail when the report mentions lint/typecheck failures (default)").option("--no-fail-on-lint-failure", "Do not fail on lint/typecheck failure findings").action(async (options) => {
7534
- const { runCi } = await import("./ci-HG2TPYVY.js");
7535
- const { formatCiResult } = await import("./ci-format-UB7K63UZ.js");
4249
+ const { runCi } = await import("./ci-JBUIVM7P.js");
4250
+ const { formatCiResult } = await import("./ci-format-B4AHV5SI.js");
7536
4251
  const formats = [options.json, options.markdown, options.sarif].filter(Boolean).length;
7537
4252
  if (formats > 1) {
7538
4253
  console.error("Choose only one output format: --json, --markdown, or --sarif.");
@@ -7690,7 +4405,7 @@ program.command("hub [topic]").description("Start multi-agent hub (discuss / bra
7690
4405
  config.get("customProviders"),
7691
4406
  config.getConfigDir()
7692
4407
  );
7693
- const { startHub } = await import("./hub-MP2CNDMZ.js");
4408
+ const { startHub } = await import("./hub-CNORVRYW.js");
7694
4409
  await startHub(
7695
4410
  {
7696
4411
  topic: topic ?? "",
@@ -7743,10 +4458,10 @@ program.command("join").description("Join a running hub as a remote AI agent").o
7743
4458
  program.parse();
7744
4459
  async function readStdin() {
7745
4460
  if (process.stdin.isTTY) return "";
7746
- return new Promise((resolve4, reject) => {
4461
+ return new Promise((resolve3, reject) => {
7747
4462
  const chunks = [];
7748
4463
  process.stdin.on("data", (chunk) => chunks.push(chunk));
7749
- process.stdin.on("end", () => resolve4(Buffer.concat(chunks).toString("utf-8").trimEnd()));
4464
+ process.stdin.on("end", () => resolve3(Buffer.concat(chunks).toString("utf-8").trimEnd()));
7750
4465
  process.stdin.on("error", reject);
7751
4466
  });
7752
4467
  }