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