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