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