jinzd-ai-cli 0.4.222 → 0.4.224
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-QOOO7HRW.js → batch-EQER63CQ.js} +2 -2
- package/dist/{chat-index-FDSGKWQV.js → chat-index-O56HDGFI.js} +2 -2
- package/dist/{chat-index-PS274XM7.js → chat-index-WSI7ULRQ.js} +1 -1
- package/dist/{chunk-OJL3PY36.js → chunk-2X2DOHA6.js} +3 -2
- package/dist/{chunk-VWTUYDZG.js → chunk-34RV4ZKA.js} +1 -1
- package/dist/{chunk-UWUTLIOH.js → chunk-35OPVVEK.js} +1 -1
- package/dist/{chunk-NZ4X6GUC.js → chunk-3ARPAVNV.js} +2 -2
- package/dist/{chunk-FBK7NSIK.js → chunk-4CZZJQXJ.js} +2 -2
- package/dist/{chunk-5EE4GRGG.js → chunk-4RQPCOEE.js} +281 -41
- package/dist/{chunk-VBRCWH55.js → chunk-6FEZAWH5.js} +3 -3
- package/dist/{chunk-SISFAT6W.js → chunk-A2AVDSXJ.js} +1 -1
- package/dist/{chunk-SLSWPBK3.js → chunk-B5TYJO7V.js} +4 -1
- package/dist/{chunk-QT5FA2ZY.js → chunk-DEHVRQWW.js} +5 -5
- package/dist/{chunk-AV5OPK7Q.js → chunk-HLXQXV23.js} +1 -1
- package/dist/{chunk-TNW22OUY.js → chunk-JDMFVRFG.js} +3 -3
- package/dist/{chunk-S6L5R6SS.js → chunk-JU7OI4BN.js} +1 -1
- package/dist/{chunk-VHY6NVMQ.js → chunk-LF3OUJHK.js} +4 -1
- package/dist/{chunk-TB4W4Y4T.js → chunk-NVUCDUXE.js} +1 -1
- package/dist/{chunk-NNWWMGYK.js → chunk-QJUAQKH2.js} +1 -1
- package/dist/{chunk-5GTWZ4QF.js → chunk-QYQCCIBT.js} +1 -1
- package/dist/{chunk-Q3ZUDA6S.js → chunk-VT6NRDD2.js} +1 -1
- package/dist/{ci-IB7NWHZN.js → ci-UAQFOAMS.js} +4 -4
- package/dist/{ci-format-YLGZN3D4.js → ci-format-MNUHYUTO.js} +2 -2
- package/dist/{constants-BOMLZEXC.js → constants-PGDTYD2R.js} +1 -1
- package/dist/{doctor-cli-2ND4LONW.js → doctor-cli-F6A3GOVY.js} +4 -4
- package/dist/electron-server.js +2017 -1786
- package/dist/{hub-6WADFJNC.js → hub-EDTZKS7E.js} +6 -6
- package/dist/{hub-server-LJ2JSKZ2.js → hub-server-XX36DX7O.js} +2 -2
- package/dist/index.js +26 -22
- package/dist/{persist-GTBEKVFL.js → persist-XUXIPYUU.js} +3 -3
- package/dist/{pr-F7WXUH27.js → pr-YC7DMVC5.js} +4 -4
- package/dist/{run-tests-5NEMM6EF.js → run-tests-AUEKHJCC.js} +2 -2
- package/dist/{run-tests-NIY24YDP.js → run-tests-S36TE2W2.js} +1 -1
- package/dist/{server-ACFX2J66.js → server-347YIBHS.js} +6 -6
- package/dist/{server-QGZYDDZW.js → server-LQFHSFN4.js} +1278 -1271
- package/dist/{task-orchestrator-4GOBFXPN.js → task-orchestrator-FIR5MCND.js} +17 -9
- package/dist/{usage-JXT2YVA6.js → usage-3MXPFFBU.js} +2 -2
- package/dist/web/client/app.js +3 -2
- package/dist/web/client/util.js +42 -0
- package/package.json +1 -1
|
@@ -3,11 +3,11 @@ import {
|
|
|
3
3
|
DiscussionOrchestrator,
|
|
4
4
|
getPreset,
|
|
5
5
|
resolveRoleProviders
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-VT6NRDD2.js";
|
|
7
|
+
import "./chunk-3ARPAVNV.js";
|
|
8
8
|
import {
|
|
9
9
|
persistDiscussion
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-DEHVRQWW.js";
|
|
11
11
|
import {
|
|
12
12
|
AuthManager,
|
|
13
13
|
TOKEN_EXPIRY_MS
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
scanDirTree,
|
|
24
24
|
scanProject,
|
|
25
25
|
setupProxy
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-4CZZJQXJ.js";
|
|
27
27
|
import {
|
|
28
28
|
buildReviewPrompt,
|
|
29
29
|
buildSecurityReviewPrompt
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
checkNetworkPolicy,
|
|
37
37
|
checkPermissionWithProfile,
|
|
38
38
|
cleanupRejectedTeeFile,
|
|
39
|
+
clearBashSession,
|
|
39
40
|
clearRecentlyDeniedAutoActions,
|
|
40
41
|
defaultActionClassifier,
|
|
41
42
|
deleteMemoryEntry,
|
|
@@ -66,7 +67,7 @@ import {
|
|
|
66
67
|
truncateOutput,
|
|
67
68
|
undoStack,
|
|
68
69
|
updateMemoryApproval
|
|
69
|
-
} from "./chunk-
|
|
70
|
+
} from "./chunk-4RQPCOEE.js";
|
|
70
71
|
import "./chunk-K3CF65QH.js";
|
|
71
72
|
import "./chunk-UUSRWSSX.js";
|
|
72
73
|
import "./chunk-CKH4KQ4E.js";
|
|
@@ -74,7 +75,7 @@ import "./chunk-BXP6YZ2P.js";
|
|
|
74
75
|
import {
|
|
75
76
|
SessionManager,
|
|
76
77
|
getContentText
|
|
77
|
-
} from "./chunk-
|
|
78
|
+
} from "./chunk-QYQCCIBT.js";
|
|
78
79
|
import {
|
|
79
80
|
computeCost,
|
|
80
81
|
formatCost
|
|
@@ -83,7 +84,7 @@ import {
|
|
|
83
84
|
buildDoctorReport,
|
|
84
85
|
formatDoctorReport,
|
|
85
86
|
loadContextFiles
|
|
86
|
-
} from "./chunk-
|
|
87
|
+
} from "./chunk-6FEZAWH5.js";
|
|
87
88
|
import {
|
|
88
89
|
ProviderRegistry
|
|
89
90
|
} from "./chunk-PUBCJF7E.js";
|
|
@@ -101,7 +102,7 @@ import {
|
|
|
101
102
|
runTool,
|
|
102
103
|
setPluginEnabled,
|
|
103
104
|
trustPlugin
|
|
104
|
-
} from "./chunk-
|
|
105
|
+
} from "./chunk-A2AVDSXJ.js";
|
|
105
106
|
import {
|
|
106
107
|
CONTENT_ONLY_STREAM_REMINDER,
|
|
107
108
|
TEE_FINAL_USER_NUDGE,
|
|
@@ -120,7 +121,7 @@ import {
|
|
|
120
121
|
} from "./chunk-XPBEJB27.js";
|
|
121
122
|
import {
|
|
122
123
|
ConfigManager
|
|
123
|
-
} from "./chunk-
|
|
124
|
+
} from "./chunk-35OPVVEK.js";
|
|
124
125
|
import "./chunk-5ULLIOVC.js";
|
|
125
126
|
import {
|
|
126
127
|
formatGitContextForPrompt,
|
|
@@ -128,7 +129,7 @@ import {
|
|
|
128
129
|
getGitRoot
|
|
129
130
|
} from "./chunk-HOSJZMQS.js";
|
|
130
131
|
import "./chunk-4BKXL7SM.js";
|
|
131
|
-
import "./chunk-
|
|
132
|
+
import "./chunk-JU7OI4BN.js";
|
|
132
133
|
import {
|
|
133
134
|
AGENTIC_BEHAVIOR_GUIDELINE,
|
|
134
135
|
AUTHOR,
|
|
@@ -145,12 +146,12 @@ import {
|
|
|
145
146
|
SKILLS_DIR_NAME,
|
|
146
147
|
VERSION,
|
|
147
148
|
buildUserIdentityPrompt
|
|
148
|
-
} from "./chunk-
|
|
149
|
-
import "./chunk-
|
|
149
|
+
} from "./chunk-QJUAQKH2.js";
|
|
150
|
+
import "./chunk-NVUCDUXE.js";
|
|
150
151
|
import "./chunk-KHYD3WXE.js";
|
|
151
152
|
import {
|
|
152
153
|
scanString
|
|
153
|
-
} from "./chunk-
|
|
154
|
+
} from "./chunk-B5TYJO7V.js";
|
|
154
155
|
import {
|
|
155
156
|
atomicWriteFileSync
|
|
156
157
|
} from "./chunk-IW3Q7AE5.js";
|
|
@@ -159,8 +160,8 @@ import {
|
|
|
159
160
|
import express from "express";
|
|
160
161
|
import { createServer } from "http";
|
|
161
162
|
import { WebSocketServer } from "ws";
|
|
162
|
-
import { join as
|
|
163
|
-
import { existsSync as
|
|
163
|
+
import { join as join4, dirname as dirname4, resolve as resolve4, relative, sep } from "path";
|
|
164
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync4, statSync as statSync4, realpathSync } from "fs";
|
|
164
165
|
import { networkInterfaces } from "os";
|
|
165
166
|
|
|
166
167
|
// src/web/tool-executor-web.ts
|
|
@@ -236,33 +237,33 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
|
236
237
|
}
|
|
237
238
|
/** Resolve a pending confirm from client response */
|
|
238
239
|
resolveConfirm(requestId, approved) {
|
|
239
|
-
const
|
|
240
|
-
if (
|
|
240
|
+
const resolve5 = this.pendingConfirms.get(requestId);
|
|
241
|
+
if (resolve5) {
|
|
241
242
|
this.clearPendingTimer(requestId);
|
|
242
243
|
this.pendingConfirms.delete(requestId);
|
|
243
244
|
this.confirming = false;
|
|
244
|
-
|
|
245
|
+
resolve5(approved);
|
|
245
246
|
}
|
|
246
247
|
}
|
|
247
248
|
/** Resolve a pending batch confirm from client response */
|
|
248
249
|
resolveBatchConfirm(requestId, decision) {
|
|
249
|
-
const
|
|
250
|
-
if (
|
|
250
|
+
const resolve5 = this.pendingBatchConfirms.get(requestId);
|
|
251
|
+
if (resolve5) {
|
|
251
252
|
this.clearPendingTimer(requestId);
|
|
252
253
|
this.pendingBatchConfirms.delete(requestId);
|
|
253
254
|
this.confirming = false;
|
|
254
255
|
if (decision === "all" || decision === "none") {
|
|
255
|
-
|
|
256
|
+
resolve5(decision);
|
|
256
257
|
} else {
|
|
257
|
-
|
|
258
|
+
resolve5(new Set(decision));
|
|
258
259
|
}
|
|
259
260
|
}
|
|
260
261
|
}
|
|
261
262
|
/** Cancel all pending confirms (e.g., on disconnect) */
|
|
262
263
|
cancelAll() {
|
|
263
|
-
for (const
|
|
264
|
+
for (const resolve5 of this.pendingConfirms.values()) resolve5(false);
|
|
264
265
|
this.pendingConfirms.clear();
|
|
265
|
-
for (const
|
|
266
|
+
for (const resolve5 of this.pendingBatchConfirms.values()) resolve5("none");
|
|
266
267
|
this.pendingBatchConfirms.clear();
|
|
267
268
|
this.confirming = false;
|
|
268
269
|
}
|
|
@@ -338,8 +339,8 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
|
338
339
|
diff: this.getDiffPreview(call)
|
|
339
340
|
};
|
|
340
341
|
this.send(msg);
|
|
341
|
-
return new Promise((
|
|
342
|
-
this.pendingConfirms.set(requestId,
|
|
342
|
+
return new Promise((resolve5) => {
|
|
343
|
+
this.pendingConfirms.set(requestId, resolve5);
|
|
343
344
|
this.pendingTimers.set(requestId, setTimeout(() => {
|
|
344
345
|
if (this.pendingConfirms.has(requestId)) {
|
|
345
346
|
this.resolveConfirm(requestId, false);
|
|
@@ -363,8 +364,8 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
|
363
364
|
files
|
|
364
365
|
};
|
|
365
366
|
this.send(msg);
|
|
366
|
-
return new Promise((
|
|
367
|
-
this.pendingBatchConfirms.set(requestId,
|
|
367
|
+
return new Promise((resolve5) => {
|
|
368
|
+
this.pendingBatchConfirms.set(requestId, resolve5);
|
|
368
369
|
this.pendingTimers.set(requestId, setTimeout(() => {
|
|
369
370
|
if (this.pendingBatchConfirms.has(requestId)) {
|
|
370
371
|
this.resolveBatchConfirm(requestId, "none");
|
|
@@ -667,103 +668,113 @@ function loadMemoryContent(configDir) {
|
|
|
667
668
|
}
|
|
668
669
|
|
|
669
670
|
// src/web/session-handler.ts
|
|
670
|
-
import { mkdirSync as
|
|
671
|
-
import { join as
|
|
671
|
+
import { mkdirSync as mkdirSync3, createWriteStream as createWriteStream3 } from "fs";
|
|
672
|
+
import { join as join3, dirname as dirname3 } from "path";
|
|
672
673
|
|
|
673
674
|
// src/web/commands/provider-commands.ts
|
|
674
675
|
async function handleProvider(args, ctx) {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
return;
|
|
680
|
-
}
|
|
681
|
-
const p = ctx.providers.get(id);
|
|
682
|
-
if (!p) {
|
|
683
|
-
ctx.send({ type: "error", message: `Provider "${id}" not available` });
|
|
684
|
-
return;
|
|
685
|
-
}
|
|
686
|
-
ctx.currentProvider = id;
|
|
687
|
-
ctx.currentModel = p.info.defaultModel;
|
|
688
|
-
ctx.updateContextWindow();
|
|
689
|
-
ctx.send({ type: "info", message: `Switched to provider: ${id} (${ctx.currentModel})` });
|
|
690
|
-
ctx.sendStatus();
|
|
676
|
+
const id = args[0];
|
|
677
|
+
if (!id) {
|
|
678
|
+
ctx.send({ type: "error", message: "Usage: /provider <id>" });
|
|
679
|
+
return;
|
|
691
680
|
}
|
|
681
|
+
const p = ctx.providers.get(id);
|
|
682
|
+
if (!p) {
|
|
683
|
+
ctx.send({ type: "error", message: `Provider "${id}" not available` });
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
ctx.currentProvider = id;
|
|
687
|
+
ctx.currentModel = p.info.defaultModel;
|
|
688
|
+
ctx.updateContextWindow();
|
|
689
|
+
ctx.send({ type: "info", message: `Switched to provider: ${id} (${ctx.currentModel})` });
|
|
690
|
+
ctx.sendStatus();
|
|
692
691
|
}
|
|
693
692
|
async function handleModel(args, ctx) {
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
ctx.send({ type: "error", message: `Model refresh failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
708
|
-
}
|
|
709
|
-
return;
|
|
710
|
-
}
|
|
711
|
-
if (sub === "cache") {
|
|
712
|
-
if ((args[1] ?? "").toLowerCase() === "clear") {
|
|
713
|
-
const providerId = args[2];
|
|
714
|
-
ctx.providers.clearModelCache(providerId);
|
|
715
|
-
ctx.send({ type: "info", message: providerId ? `Cleared model cache for ${providerId}.` : "Cleared model cache." });
|
|
716
|
-
return;
|
|
717
|
-
}
|
|
718
|
-
const rows = ctx.providers.getModelCacheStatus();
|
|
719
|
-
ctx.send({
|
|
720
|
-
type: "info",
|
|
721
|
-
message: rows.length === 0 ? "Model cache is empty. Use /model refresh to populate it." : rows.map((row) => `${row.providerId}: ${row.count} model(s), fetched ${row.fetchedAt}`).join("\n")
|
|
722
|
-
});
|
|
723
|
-
return;
|
|
693
|
+
const sub = args[0];
|
|
694
|
+
if (!sub) {
|
|
695
|
+
ctx.send({ type: "error", message: "Usage: /model <id>|refresh [provider]|cache [clear [provider]]" });
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
if (sub === "refresh") {
|
|
699
|
+
const providerId = args[1] ?? ctx.currentProvider;
|
|
700
|
+
try {
|
|
701
|
+
const models2 = await ctx.providers.refreshModels(providerId);
|
|
702
|
+
ctx.send({ type: "info", message: `Refreshed ${models2.length} model(s) for ${providerId}.` });
|
|
703
|
+
ctx.sendStatus();
|
|
704
|
+
} catch (err) {
|
|
705
|
+
ctx.send({ type: "error", message: `Model refresh failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
724
706
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
if (
|
|
729
|
-
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
if (sub === "cache") {
|
|
710
|
+
if ((args[1] ?? "").toLowerCase() === "clear") {
|
|
711
|
+
const providerId = args[2];
|
|
712
|
+
ctx.providers.clearModelCache(providerId);
|
|
713
|
+
ctx.send({ type: "info", message: providerId ? `Cleared model cache for ${providerId}.` : "Cleared model cache." });
|
|
730
714
|
return;
|
|
731
715
|
}
|
|
732
|
-
|
|
733
|
-
ctx.
|
|
734
|
-
|
|
735
|
-
|
|
716
|
+
const rows = ctx.providers.getModelCacheStatus();
|
|
717
|
+
ctx.send({
|
|
718
|
+
type: "info",
|
|
719
|
+
message: rows.length === 0 ? "Model cache is empty. Use /model refresh to populate it." : rows.map((row) => `${row.providerId}: ${row.count} model(s), fetched ${row.fetchedAt}`).join("\n")
|
|
720
|
+
});
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
const modelId = sub;
|
|
724
|
+
const models = await ctx.providers.listModels(ctx.currentProvider);
|
|
725
|
+
const found = models.find((m) => m.id === modelId);
|
|
726
|
+
if (!found) {
|
|
727
|
+
ctx.send({ type: "error", message: `Model "${modelId}" not found` });
|
|
728
|
+
return;
|
|
736
729
|
}
|
|
730
|
+
ctx.currentModel = modelId;
|
|
731
|
+
ctx.updateContextWindow();
|
|
732
|
+
ctx.send({ type: "info", message: `Switched to model: ${modelId}` });
|
|
733
|
+
ctx.sendStatus();
|
|
737
734
|
}
|
|
738
735
|
async function handleThink(args, ctx) {
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
ctx.sendStatus();
|
|
746
|
-
}
|
|
736
|
+
const sub = args[0];
|
|
737
|
+
if (sub === "on") ctx.runtimeThinking = true;
|
|
738
|
+
else if (sub === "off") ctx.runtimeThinking = false;
|
|
739
|
+
else ctx.runtimeThinking = !ctx.runtimeThinking;
|
|
740
|
+
ctx.send({ type: "info", message: `Thinking mode: ${ctx.runtimeThinking ? "ON" : "OFF"}` });
|
|
741
|
+
ctx.sendStatus();
|
|
747
742
|
}
|
|
748
743
|
async function handlePlan(args, ctx) {
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
744
|
+
const sub = args[0];
|
|
745
|
+
if (sub === "exit" || sub === "cancel") {
|
|
746
|
+
ctx.planMode = false;
|
|
747
|
+
ctx.send({ type: "info", message: "Plan mode OFF." });
|
|
748
|
+
} else if (sub === "execute") {
|
|
749
|
+
ctx.planMode = false;
|
|
750
|
+
ctx.send({ type: "info", message: "Plan mode OFF. Executing with all tools enabled." });
|
|
751
|
+
} else {
|
|
752
|
+
ctx.planMode = !ctx.planMode;
|
|
753
|
+
ctx.send({ type: "info", message: `Plan mode: ${ctx.planMode ? "ON (read-only tools only)" : "OFF"}` });
|
|
754
|
+
}
|
|
755
|
+
ctx.sendStatus();
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// src/web/commands/_shared.ts
|
|
759
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync, mkdirSync, readdirSync, statSync, createWriteStream } from "fs";
|
|
760
|
+
import { join, resolve, dirname } from "path";
|
|
761
|
+
function lastAssistantText(messages) {
|
|
762
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
763
|
+
const m = messages[i];
|
|
764
|
+
if (!m || m.role !== "assistant") continue;
|
|
765
|
+
const c = m.content;
|
|
766
|
+
if (typeof c === "string") return c;
|
|
767
|
+
if (Array.isArray(c)) {
|
|
768
|
+
const text = c.filter((p) => p && p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
|
|
769
|
+
if (text) return text;
|
|
760
770
|
}
|
|
761
|
-
|
|
771
|
+
return void 0;
|
|
762
772
|
}
|
|
773
|
+
return void 0;
|
|
763
774
|
}
|
|
764
775
|
|
|
765
776
|
// src/web/commands/session-commands.ts
|
|
766
|
-
async function handleClear(
|
|
777
|
+
async function handleClear(_args, ctx) {
|
|
767
778
|
ctx.saveIfNeeded();
|
|
768
779
|
ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
|
|
769
780
|
ctx.resetWebSessionUsage();
|
|
@@ -774,1207 +785,1215 @@ async function handleClear(args, ctx) {
|
|
|
774
785
|
async function handleCompact(args, ctx) {
|
|
775
786
|
await ctx.compactSession(args.join(" ") || void 0);
|
|
776
787
|
}
|
|
777
|
-
async function handleStatus(
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
const cacheLine = cacheRead > 0 || cacheCreate > 0 ? `
|
|
788
|
+
async function handleStatus(_args, ctx) {
|
|
789
|
+
const session = ctx.sessions.current;
|
|
790
|
+
const cacheRead = ctx.sessionTokenUsage.cacheReadTokens;
|
|
791
|
+
const cacheCreate = ctx.sessionTokenUsage.cacheCreationTokens;
|
|
792
|
+
const cost = computeCost(ctx.currentProvider, ctx.currentModel, ctx.sessionTokenUsage);
|
|
793
|
+
const cacheLine = cacheRead > 0 || cacheCreate > 0 ? `
|
|
784
794
|
Cache: write=${cacheCreate} read=${cacheRead}` : "";
|
|
785
|
-
|
|
795
|
+
const costLine = cost != null ? `
|
|
786
796
|
Cost: ${formatCost(cost)}` : "";
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
797
|
+
ctx.send({
|
|
798
|
+
type: "info",
|
|
799
|
+
message: `Provider: ${ctx.currentProvider}
|
|
790
800
|
Model: ${ctx.currentModel}
|
|
791
801
|
Session: ${session?.id ?? "none"} (${session?.messages.length ?? 0} messages)
|
|
792
802
|
Tokens: in=${ctx.sessionTokenUsage.inputTokens} out=${ctx.sessionTokenUsage.outputTokens}${cacheLine}${costLine}`
|
|
793
|
-
|
|
794
|
-
}
|
|
803
|
+
});
|
|
795
804
|
}
|
|
796
805
|
async function handleSession(args, ctx) {
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
806
|
+
const sub = args[0];
|
|
807
|
+
if (sub === "new") {
|
|
808
|
+
ctx.saveIfNeeded();
|
|
809
|
+
const created = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
|
|
810
|
+
ctx.unsavedSessions.set(created.id, created);
|
|
811
|
+
ctx.resetWebSessionUsage();
|
|
812
|
+
ctx.send({ type: "info", message: "New session created." });
|
|
813
|
+
ctx.sendStatus();
|
|
814
|
+
ctx.sendSessionList();
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
if (sub === "load" && args[1]) {
|
|
818
|
+
const targetId = args[1];
|
|
819
|
+
ctx.saveIfNeeded();
|
|
820
|
+
const cachedExact = ctx.unsavedSessions.get(targetId);
|
|
821
|
+
const cached = cachedExact ?? [...ctx.unsavedSessions.values()].find((s) => s.id.startsWith(targetId));
|
|
822
|
+
if (cached) {
|
|
823
|
+
ctx.sessions.setCurrent(cached);
|
|
803
824
|
ctx.resetWebSessionUsage();
|
|
804
|
-
ctx.send({
|
|
825
|
+
ctx.send({
|
|
826
|
+
type: "info",
|
|
827
|
+
message: `Loaded session: ${cached.id.slice(0, 8)} "${cached.title ?? ""}" (${cached.messages.length} messages)`
|
|
828
|
+
});
|
|
829
|
+
ctx.sendSessionMessages();
|
|
805
830
|
ctx.sendStatus();
|
|
806
831
|
ctx.sendSessionList();
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
const found = list.find((s) => s.id.startsWith(targetId));
|
|
825
|
-
if (found) {
|
|
826
|
-
ctx.sessions.loadSession(found.id);
|
|
827
|
-
ctx.resetWebSessionUsage();
|
|
828
|
-
ctx.send({ type: "info", message: `Loaded session: ${found.id.slice(0, 8)} "${found.title ?? ""}" (${found.messageCount} messages)` });
|
|
829
|
-
const loadedSession = ctx.sessions.current;
|
|
830
|
-
if (loadedSession && loadedSession.messages.length > 0) {
|
|
831
|
-
const lastMsg = loadedSession.messages[loadedSession.messages.length - 1];
|
|
832
|
-
const isIncomplete = lastMsg.role === "tool" || lastMsg.role === "assistant" && lastMsg.toolCalls && lastMsg.toolCalls.length > 0;
|
|
833
|
-
if (isIncomplete) {
|
|
834
|
-
ctx.send({
|
|
835
|
-
type: "info",
|
|
836
|
-
message: '\u26A0 This session appears to have been interrupted mid-task. The AI will see the tool history and can continue where it left off. Type "continue where you left off" to resume.'
|
|
837
|
-
});
|
|
838
|
-
}
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
const list = ctx.sessions.listSessions();
|
|
835
|
+
const found = list.find((s) => s.id.startsWith(targetId));
|
|
836
|
+
if (found) {
|
|
837
|
+
ctx.sessions.loadSession(found.id);
|
|
838
|
+
ctx.resetWebSessionUsage();
|
|
839
|
+
ctx.send({ type: "info", message: `Loaded session: ${found.id.slice(0, 8)} "${found.title ?? ""}" (${found.messageCount} messages)` });
|
|
840
|
+
const loadedSession = ctx.sessions.current;
|
|
841
|
+
if (loadedSession && loadedSession.messages.length > 0) {
|
|
842
|
+
const lastMsg = loadedSession.messages[loadedSession.messages.length - 1];
|
|
843
|
+
const isIncomplete = lastMsg.role === "tool" || lastMsg.role === "assistant" && lastMsg.toolCalls && lastMsg.toolCalls.length > 0;
|
|
844
|
+
if (isIncomplete) {
|
|
845
|
+
ctx.send({
|
|
846
|
+
type: "info",
|
|
847
|
+
message: '\u26A0 This session appears to have been interrupted mid-task. The AI will see the tool history and can continue where it left off. Type "continue where you left off" to resume.'
|
|
848
|
+
});
|
|
839
849
|
}
|
|
840
|
-
ctx.sendSessionMessages();
|
|
841
|
-
ctx.sendStatus();
|
|
842
|
-
ctx.sendSessionList();
|
|
843
|
-
} else {
|
|
844
|
-
const recreated = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
|
|
845
|
-
ctx.unsavedSessions.set(recreated.id, recreated);
|
|
846
|
-
ctx.resetWebSessionUsage();
|
|
847
|
-
ctx.send({
|
|
848
|
-
type: "info",
|
|
849
|
-
message: `Previous session (${targetId.slice(0, 8)}) is no longer available \u2014 started a new one.`
|
|
850
|
-
});
|
|
851
|
-
ctx.sendStatus();
|
|
852
|
-
ctx.sendSessionList();
|
|
853
850
|
}
|
|
854
|
-
|
|
851
|
+
ctx.sendSessionMessages();
|
|
852
|
+
ctx.sendStatus();
|
|
853
|
+
ctx.sendSessionList();
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
const recreated = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
|
|
857
|
+
ctx.unsavedSessions.set(recreated.id, recreated);
|
|
858
|
+
ctx.resetWebSessionUsage();
|
|
859
|
+
ctx.send({
|
|
860
|
+
type: "info",
|
|
861
|
+
message: `Previous session (${targetId.slice(0, 8)}) is no longer available \u2014 started a new one.`
|
|
862
|
+
});
|
|
863
|
+
ctx.sendStatus();
|
|
864
|
+
ctx.sendSessionList();
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
if (sub === "list") {
|
|
868
|
+
ctx.sendSessionList();
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
if (sub === "delete" && args[1]) {
|
|
872
|
+
const targetId = args[1];
|
|
873
|
+
const cachedKey = ctx.unsavedSessions.has(targetId) ? targetId : [...ctx.unsavedSessions.keys()].find((k) => k.startsWith(targetId));
|
|
874
|
+
if (cachedKey) {
|
|
875
|
+
ctx.unsavedSessions.delete(cachedKey);
|
|
876
|
+
if (ctx.sessions.current?.id === cachedKey) {
|
|
877
|
+
ctx.sessions.clearCurrent();
|
|
878
|
+
}
|
|
879
|
+
ctx.send({ type: "info", message: `Deleted session: ${cachedKey.slice(0, 8)}` });
|
|
855
880
|
ctx.sendSessionList();
|
|
856
|
-
|
|
857
|
-
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
const list = ctx.sessions.listSessions();
|
|
884
|
+
const found = list.find((s) => s.id.startsWith(targetId));
|
|
885
|
+
if (found) {
|
|
886
|
+
ctx.sessions.deleteSession(found.id);
|
|
887
|
+
ctx.send({ type: "info", message: `Deleted session: ${found.id.slice(0, 8)}` });
|
|
888
|
+
ctx.sendSessionList();
|
|
889
|
+
} else {
|
|
890
|
+
ctx.send({ type: "error", message: `Session not found: ${targetId}` });
|
|
891
|
+
}
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
if (sub === "batch-delete") {
|
|
895
|
+
const ids = args.slice(1).filter(Boolean);
|
|
896
|
+
if (ids.length === 0) {
|
|
897
|
+
ctx.send({ type: "error", message: "No session IDs provided." });
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
const list = ctx.sessions.listSessions();
|
|
901
|
+
let deleted = 0;
|
|
902
|
+
for (const targetId of ids) {
|
|
858
903
|
const cachedKey = ctx.unsavedSessions.has(targetId) ? targetId : [...ctx.unsavedSessions.keys()].find((k) => k.startsWith(targetId));
|
|
859
904
|
if (cachedKey) {
|
|
860
905
|
ctx.unsavedSessions.delete(cachedKey);
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
}
|
|
864
|
-
ctx.send({ type: "info", message: `Deleted session: ${cachedKey.slice(0, 8)}` });
|
|
865
|
-
ctx.sendSessionList();
|
|
906
|
+
deleted++;
|
|
907
|
+
continue;
|
|
866
908
|
}
|
|
867
|
-
const list = ctx.sessions.listSessions();
|
|
868
909
|
const found = list.find((s) => s.id.startsWith(targetId));
|
|
869
910
|
if (found) {
|
|
870
911
|
ctx.sessions.deleteSession(found.id);
|
|
871
|
-
|
|
872
|
-
ctx.sendSessionList();
|
|
873
|
-
} else {
|
|
874
|
-
ctx.send({ type: "error", message: `Session not found: ${targetId}` });
|
|
912
|
+
deleted++;
|
|
875
913
|
}
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
914
|
+
}
|
|
915
|
+
ctx.send({ type: "info", message: `Deleted ${deleted} session(s).` });
|
|
916
|
+
ctx.sendSessionList();
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
if (sub === "rename" && args[1] && args.length >= 3) {
|
|
920
|
+
const targetId = args[1];
|
|
921
|
+
const newTitle = args.slice(2).join(" ").trim();
|
|
922
|
+
if (!newTitle) {
|
|
923
|
+
ctx.send({ type: "error", message: "Title cannot be empty." });
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
const cachedKey = ctx.unsavedSessions.has(targetId) ? targetId : [...ctx.unsavedSessions.keys()].find((k) => k.startsWith(targetId));
|
|
927
|
+
if (cachedKey) {
|
|
928
|
+
const session = ctx.unsavedSessions.get(cachedKey);
|
|
929
|
+
session.title = newTitle;
|
|
930
|
+
ctx.send({ type: "info", message: `Renamed session: "${newTitle}"` });
|
|
931
|
+
ctx.sendSessionList();
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
const list = ctx.sessions.listSessions();
|
|
935
|
+
const found = list.find((s) => s.id.startsWith(targetId));
|
|
936
|
+
if (found) {
|
|
937
|
+
const session = ctx.sessions.loadSession(found.id);
|
|
938
|
+
session.title = newTitle;
|
|
939
|
+
ctx.sessions.save();
|
|
940
|
+
ctx.send({ type: "info", message: `Renamed session: "${newTitle}"` });
|
|
897
941
|
ctx.sendSessionList();
|
|
898
|
-
} else if (sub === "rename" && args[1] && args.length >= 3) {
|
|
899
|
-
const targetId = args[1];
|
|
900
|
-
const newTitle = args.slice(2).join(" ").trim();
|
|
901
|
-
if (!newTitle) {
|
|
902
|
-
ctx.send({ type: "error", message: "Title cannot be empty." });
|
|
903
|
-
}
|
|
904
|
-
const cachedKey = ctx.unsavedSessions.has(targetId) ? targetId : [...ctx.unsavedSessions.keys()].find((k) => k.startsWith(targetId));
|
|
905
|
-
if (cachedKey) {
|
|
906
|
-
const session = ctx.unsavedSessions.get(cachedKey);
|
|
907
|
-
session.title = newTitle;
|
|
908
|
-
ctx.send({ type: "info", message: `Renamed session: "${newTitle}"` });
|
|
909
|
-
ctx.sendSessionList();
|
|
910
|
-
}
|
|
911
|
-
const list = ctx.sessions.listSessions();
|
|
912
|
-
const found = list.find((s) => s.id.startsWith(targetId));
|
|
913
|
-
if (found) {
|
|
914
|
-
const session = ctx.sessions.loadSession(found.id);
|
|
915
|
-
session.title = newTitle;
|
|
916
|
-
ctx.sessions.save();
|
|
917
|
-
ctx.send({ type: "info", message: `Renamed session: "${newTitle}"` });
|
|
918
|
-
ctx.sendSessionList();
|
|
919
|
-
} else {
|
|
920
|
-
ctx.send({ type: "error", message: `Session not found: ${targetId}` });
|
|
921
|
-
}
|
|
922
942
|
} else {
|
|
923
|
-
ctx.send({ type: "
|
|
943
|
+
ctx.send({ type: "error", message: `Session not found: ${targetId}` });
|
|
924
944
|
}
|
|
945
|
+
return;
|
|
925
946
|
}
|
|
947
|
+
ctx.send({ type: "info", message: "Usage: /session new | list | load <id> | delete <id> | rename <id> <title>" });
|
|
926
948
|
}
|
|
927
949
|
async function handleExport(args, ctx) {
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
ctx.exportConversation(format);
|
|
931
|
-
}
|
|
950
|
+
const format = args[0] === "json" ? "json" : "md";
|
|
951
|
+
ctx.exportConversation(format);
|
|
932
952
|
}
|
|
933
|
-
async function handleCost(
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
lines.push(` Cache savings : -${formatCost(savings)} (vs no cache)`);
|
|
965
|
-
}
|
|
966
|
-
} else {
|
|
967
|
-
lines.push(` Cost : \u2014 (pricing unknown for this model)`);
|
|
953
|
+
async function handleCost(_args, ctx) {
|
|
954
|
+
const u = ctx.sessionTokenUsage;
|
|
955
|
+
const total = u.inputTokens + u.outputTokens + u.cacheCreationTokens + u.cacheReadTokens;
|
|
956
|
+
const cost = computeCost(ctx.currentProvider, ctx.currentModel, u);
|
|
957
|
+
let savings = null;
|
|
958
|
+
if (cost != null && u.cacheReadTokens > 0) {
|
|
959
|
+
const withoutCache = computeCost(ctx.currentProvider, ctx.currentModel, {
|
|
960
|
+
inputTokens: u.inputTokens + u.cacheReadTokens,
|
|
961
|
+
outputTokens: u.outputTokens,
|
|
962
|
+
cacheCreationTokens: u.cacheCreationTokens,
|
|
963
|
+
cacheReadTokens: 0
|
|
964
|
+
});
|
|
965
|
+
if (withoutCache != null) savings = withoutCache - cost;
|
|
966
|
+
}
|
|
967
|
+
const lines = [
|
|
968
|
+
"\u{1F4B0} Session Cost & Token Usage",
|
|
969
|
+
` Provider : ${ctx.currentProvider}`,
|
|
970
|
+
` Model : ${ctx.currentModel}`,
|
|
971
|
+
` Input (uncached): ${u.inputTokens.toLocaleString()}`,
|
|
972
|
+
` Output : ${u.outputTokens.toLocaleString()}`
|
|
973
|
+
];
|
|
974
|
+
if (u.cacheCreationTokens > 0) lines.push(` Cache write : ${u.cacheCreationTokens.toLocaleString()}`);
|
|
975
|
+
if (u.cacheReadTokens > 0) {
|
|
976
|
+
const pct = Math.round(u.cacheReadTokens / (u.cacheReadTokens + u.inputTokens) * 100);
|
|
977
|
+
lines.push(` Cache read : ${u.cacheReadTokens.toLocaleString()} (${pct}% hit rate)`);
|
|
978
|
+
}
|
|
979
|
+
lines.push(` Total tokens : ${total.toLocaleString()}`);
|
|
980
|
+
if (cost != null) {
|
|
981
|
+
lines.push(` Cost : ${formatCost(cost)}`);
|
|
982
|
+
if (savings != null && savings > 0) {
|
|
983
|
+
lines.push(` Cache savings : -${formatCost(savings)} (vs no cache)`);
|
|
968
984
|
}
|
|
969
|
-
|
|
985
|
+
} else {
|
|
986
|
+
lines.push(` Cost : \u2014 (pricing unknown for this model)`);
|
|
970
987
|
}
|
|
988
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
971
989
|
}
|
|
972
990
|
async function handleCheckpoint(args, ctx) {
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
}
|
|
989
|
-
ctx.send({ type: "info", message: lines.join("\n") });
|
|
991
|
+
const session = ctx.sessions.current;
|
|
992
|
+
if (!session) {
|
|
993
|
+
ctx.send({ type: "error", message: "No active session." });
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
const sub = args[0]?.toLowerCase();
|
|
997
|
+
if (!sub || sub === "list") {
|
|
998
|
+
const cps = session.checkpoints;
|
|
999
|
+
if (cps.length === 0) {
|
|
1000
|
+
ctx.send({ type: "info", message: "No checkpoints. Use /checkpoint save <name> to create one." });
|
|
1001
|
+
} else {
|
|
1002
|
+
const lines = [`\u{1F4CC} Checkpoints (${cps.length}):`, ""];
|
|
1003
|
+
for (const cp of cps) {
|
|
1004
|
+
const ts = cp.timestamp.toLocaleString();
|
|
1005
|
+
lines.push(` ${cp.name.padEnd(20)} msg #${cp.messageIndex} ${ts}`);
|
|
990
1006
|
}
|
|
1007
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
991
1008
|
}
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
if (!cpName) {
|
|
1013
|
-
ctx.send({ type: "error", message: "Usage: /checkpoint delete <name>" });
|
|
1014
|
-
}
|
|
1015
|
-
const ok = session.deleteCheckpoint(cpName);
|
|
1016
|
-
if (ok) {
|
|
1017
|
-
ctx.send({ type: "info", message: `\u2713 Deleted checkpoint "${cpName}"` });
|
|
1018
|
-
} else {
|
|
1019
|
-
ctx.send({ type: "error", message: `Checkpoint "${cpName}" not found.` });
|
|
1020
|
-
}
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
const cpName = args.slice(1).join(" ").trim();
|
|
1012
|
+
if (sub === "save") {
|
|
1013
|
+
if (!cpName) {
|
|
1014
|
+
ctx.send({ type: "error", message: "Usage: /checkpoint save <name>" });
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
session.createCheckpoint(cpName);
|
|
1018
|
+
ctx.send({ type: "info", message: `\u2713 Checkpoint "${cpName}" saved at message #${session.messages.length}` });
|
|
1019
|
+
} else if (sub === "restore") {
|
|
1020
|
+
if (!cpName) {
|
|
1021
|
+
ctx.send({ type: "error", message: "Usage: /checkpoint restore <name>" });
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
const ok = session.restoreCheckpoint(cpName);
|
|
1025
|
+
if (ok) {
|
|
1026
|
+
ctx.send({ type: "info", message: `\u2713 Restored to checkpoint "${cpName}" (${session.messages.length} messages)` });
|
|
1027
|
+
ctx.sendSessionMessages();
|
|
1028
|
+
ctx.sendStatus();
|
|
1021
1029
|
} else {
|
|
1022
|
-
ctx.send({ type: "error", message: `
|
|
1030
|
+
ctx.send({ type: "error", message: `Checkpoint "${cpName}" not found.` });
|
|
1023
1031
|
}
|
|
1032
|
+
} else if (sub === "delete") {
|
|
1033
|
+
if (!cpName) {
|
|
1034
|
+
ctx.send({ type: "error", message: "Usage: /checkpoint delete <name>" });
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
const ok = session.deleteCheckpoint(cpName);
|
|
1038
|
+
if (ok) {
|
|
1039
|
+
ctx.send({ type: "info", message: `\u2713 Deleted checkpoint "${cpName}"` });
|
|
1040
|
+
} else {
|
|
1041
|
+
ctx.send({ type: "error", message: `Checkpoint "${cpName}" not found.` });
|
|
1042
|
+
}
|
|
1043
|
+
} else {
|
|
1044
|
+
ctx.send({ type: "error", message: `Unknown subcommand: ${sub}. Use save/restore/list/delete.` });
|
|
1024
1045
|
}
|
|
1025
1046
|
}
|
|
1026
1047
|
async function handleBranch(args, ctx) {
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
}
|
|
1042
|
-
return null;
|
|
1043
|
-
};
|
|
1044
|
-
if (!sub || sub === "list") {
|
|
1045
|
-
const branches = session.listBranches();
|
|
1046
|
-
const lines = [`\u{1F33F} Branches (${branches.length}):`, ""];
|
|
1047
|
-
for (const b of branches) {
|
|
1048
|
-
const marker = b.id === session.activeBranchId ? "\u25CF " : " ";
|
|
1049
|
-
const count = b.id === session.activeBranchId ? session.messages.length : session.getBranchMessages(b.id)?.length ?? 0;
|
|
1050
|
-
const parent = b.parentBranchId ? ` \u2190 ${b.parentBranchId}@${b.parentMessageIndex}` : "";
|
|
1051
|
-
lines.push(` ${marker}${b.id.padEnd(10)} ${b.title.padEnd(20)} (${count} msgs)${parent}`);
|
|
1052
|
-
}
|
|
1053
|
-
lines.push("");
|
|
1054
|
-
lines.push("Usage: /branch new <msgIndex> [title] | switch <id|title> | delete <id|title> | rename <id|title> <new title> | diff <id|title> | cherry-pick <id|title> <msgIndex>");
|
|
1055
|
-
ctx.send({ type: "info", message: lines.join("\n") });
|
|
1048
|
+
const session = ctx.sessions.current;
|
|
1049
|
+
if (!session) {
|
|
1050
|
+
ctx.send({ type: "error", message: "No active session." });
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
const sub = args[0]?.toLowerCase();
|
|
1054
|
+
const resolve5 = (ref) => {
|
|
1055
|
+
const r = session.resolveBranchRef(ref);
|
|
1056
|
+
if (r.ok) return r.id;
|
|
1057
|
+
if (r.reason === "ambiguous") {
|
|
1058
|
+
const list = r.matches.map((m) => `${m.id} (${m.title})`).join(", ");
|
|
1059
|
+
ctx.send({ type: "error", message: `Ambiguous branch reference "${ref}". Matches: ${list}. Use the 6-char id.` });
|
|
1060
|
+
} else {
|
|
1061
|
+
ctx.send({ type: "error", message: `Branch "${ref}" not found. Use /branch list to see all branches.` });
|
|
1056
1062
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
try {
|
|
1068
|
-
const newId = session.createBranch(fromIndex, title);
|
|
1069
|
-
await ctx.sessions.save();
|
|
1070
|
-
ctx.send({ type: "info", message: `\u2713 Created branch "${newId}" from message #${fromIndex}. Now active (${session.messages.length} messages).` });
|
|
1071
|
-
ctx.sendSessionMessages();
|
|
1072
|
-
ctx.sendStatus();
|
|
1073
|
-
} catch (err) {
|
|
1074
|
-
ctx.send({ type: "error", message: err.message });
|
|
1075
|
-
}
|
|
1063
|
+
return null;
|
|
1064
|
+
};
|
|
1065
|
+
if (!sub || sub === "list") {
|
|
1066
|
+
const branches = session.listBranches();
|
|
1067
|
+
const lines = [`\u{1F33F} Branches (${branches.length}):`, ""];
|
|
1068
|
+
for (const b of branches) {
|
|
1069
|
+
const marker = b.id === session.activeBranchId ? "\u25CF " : " ";
|
|
1070
|
+
const count = b.id === session.activeBranchId ? session.messages.length : session.getBranchMessages(b.id)?.length ?? 0;
|
|
1071
|
+
const parent = b.parentBranchId ? ` \u2190 ${b.parentBranchId}@${b.parentMessageIndex}` : "";
|
|
1072
|
+
lines.push(` ${marker}${b.id.padEnd(10)} ${b.title.padEnd(20)} (${count} msgs)${parent}`);
|
|
1076
1073
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
await ctx.sessions.save();
|
|
1088
|
-
const b = session.getActiveBranch();
|
|
1089
|
-
ctx.send({ type: "info", message: `\u2713 Switched to branch "${b.id}" \u2014 ${b.title} (${session.messages.length} messages)` });
|
|
1090
|
-
ctx.sendSessionMessages();
|
|
1091
|
-
ctx.sendStatus();
|
|
1092
|
-
} else {
|
|
1093
|
-
ctx.send({ type: "error", message: `Cannot switch to "${id}" (already active).` });
|
|
1094
|
-
}
|
|
1074
|
+
lines.push("");
|
|
1075
|
+
lines.push("Usage: /branch new <msgIndex> [title] | switch <id|title> | delete <id|title> | rename <id|title> <new title> | diff <id|title> | cherry-pick <id|title> <msgIndex>");
|
|
1076
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
if (sub === "new") {
|
|
1080
|
+
const idxArg = args[1];
|
|
1081
|
+
if (!idxArg) {
|
|
1082
|
+
ctx.send({ type: "error", message: "Usage: /branch new <msgIndex> [title]" });
|
|
1083
|
+
return;
|
|
1095
1084
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
}
|
|
1101
|
-
const id = resolve6(ref);
|
|
1102
|
-
if (!id) {
|
|
1103
|
-
}
|
|
1104
|
-
const ok = session.deleteBranch(id);
|
|
1105
|
-
if (ok) {
|
|
1106
|
-
await ctx.sessions.save();
|
|
1107
|
-
ctx.send({ type: "info", message: `\u2713 Deleted branch "${id}"` });
|
|
1108
|
-
ctx.sendStatus();
|
|
1109
|
-
} else {
|
|
1110
|
-
ctx.send({ type: "error", message: `Cannot delete "${id}" (active, or last remaining branch).` });
|
|
1111
|
-
}
|
|
1085
|
+
const fromIndex = parseInt(idxArg, 10);
|
|
1086
|
+
if (isNaN(fromIndex) || fromIndex < 0 || fromIndex > session.messages.length) {
|
|
1087
|
+
ctx.send({ type: "error", message: `Invalid msgIndex: ${idxArg}. Range: 0-${session.messages.length}` });
|
|
1088
|
+
return;
|
|
1112
1089
|
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
const
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
}
|
|
1122
|
-
const ok = session.renameBranch(id, title);
|
|
1123
|
-
if (ok) {
|
|
1124
|
-
await ctx.sessions.save();
|
|
1125
|
-
ctx.send({ type: "info", message: `\u2713 Renamed branch "${id}" \u2192 "${title}"` });
|
|
1126
|
-
ctx.sendStatus();
|
|
1127
|
-
} else {
|
|
1128
|
-
ctx.send({ type: "error", message: `Branch "${id}" not found.` });
|
|
1129
|
-
}
|
|
1090
|
+
const title = args.slice(2).join(" ").trim() || void 0;
|
|
1091
|
+
try {
|
|
1092
|
+
const newId = session.createBranch(fromIndex, title);
|
|
1093
|
+
await ctx.sessions.save();
|
|
1094
|
+
ctx.send({ type: "info", message: `\u2713 Created branch "${newId}" from message #${fromIndex}. Now active (${session.messages.length} messages).` });
|
|
1095
|
+
ctx.sendSessionMessages();
|
|
1096
|
+
ctx.sendStatus();
|
|
1097
|
+
} catch (err) {
|
|
1098
|
+
ctx.send({ type: "error", message: err.message });
|
|
1130
1099
|
}
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
}
|
|
1139
|
-
const d = session.diffBranches(id);
|
|
1140
|
-
if (!d) {
|
|
1141
|
-
ctx.send({ type: "error", message: `Branch "${id}" not found.` });
|
|
1142
|
-
}
|
|
1143
|
-
const preview = (m) => {
|
|
1144
|
-
const text = typeof m.content === "string" ? m.content : "[complex content]";
|
|
1145
|
-
return text.replace(/\s+/g, " ").trim().slice(0, 70);
|
|
1146
|
-
};
|
|
1147
|
-
const lines = [
|
|
1148
|
-
`\u{1F500} diff ${id} (source) \u2194 ${session.activeBranchId} (active)`,
|
|
1149
|
-
`Common prefix: ${d.commonPrefix} message(s)`
|
|
1150
|
-
];
|
|
1151
|
-
if (d.sourceOnly.length === 0 && d.targetOnly.length === 0) {
|
|
1152
|
-
lines.push("Branches are identical.");
|
|
1153
|
-
} else {
|
|
1154
|
-
if (d.sourceOnly.length > 0) {
|
|
1155
|
-
lines.push("", `Only in ${id} (${d.sourceOnly.length}):`);
|
|
1156
|
-
d.sourceOnly.forEach(
|
|
1157
|
-
(m, i) => lines.push(` [${d.commonPrefix + i}] ${m.role}: ${preview(m)}`)
|
|
1158
|
-
);
|
|
1159
|
-
}
|
|
1160
|
-
if (d.targetOnly.length > 0) {
|
|
1161
|
-
lines.push("", `Only in ${session.activeBranchId} (active) (${d.targetOnly.length}):`);
|
|
1162
|
-
d.targetOnly.forEach(
|
|
1163
|
-
(m, i) => lines.push(` [${d.commonPrefix + i}] ${m.role}: ${preview(m)}`)
|
|
1164
|
-
);
|
|
1165
|
-
}
|
|
1166
|
-
lines.push("", `Use /branch cherry-pick ${id} <index> to copy a message into the active branch.`);
|
|
1167
|
-
}
|
|
1168
|
-
ctx.send({ type: "info", message: lines.join("\n") });
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
if (sub === "switch") {
|
|
1103
|
+
const ref = args[1];
|
|
1104
|
+
if (!ref) {
|
|
1105
|
+
ctx.send({ type: "error", message: "Usage: /branch switch <id|title>" });
|
|
1106
|
+
return;
|
|
1169
1107
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
ctx.send({ type: "error", message: "Usage: /branch cherry-pick <source-id|title> <msg-index>" });
|
|
1175
|
-
}
|
|
1176
|
-
const id = resolve6(ref);
|
|
1177
|
-
if (!id) {
|
|
1178
|
-
}
|
|
1179
|
-
const idx = parseInt(idxStr, 10);
|
|
1180
|
-
if (Number.isNaN(idx)) {
|
|
1181
|
-
ctx.send({ type: "error", message: `Invalid message index: "${idxStr}"` });
|
|
1182
|
-
}
|
|
1183
|
-
const picked = session.cherryPickMessage(id, idx);
|
|
1184
|
-
if (!picked) {
|
|
1185
|
-
ctx.send({ type: "error", message: `Cherry-pick failed \u2014 index ${idx} out of range for branch "${id}".` });
|
|
1186
|
-
}
|
|
1108
|
+
const id = resolve5(ref);
|
|
1109
|
+
if (!id) return;
|
|
1110
|
+
const ok = session.switchBranch(id);
|
|
1111
|
+
if (ok) {
|
|
1187
1112
|
await ctx.sessions.save();
|
|
1188
|
-
const
|
|
1189
|
-
ctx.send({ type: "info", message: `\u2713
|
|
1113
|
+
const b = session.getActiveBranch();
|
|
1114
|
+
ctx.send({ type: "info", message: `\u2713 Switched to branch "${b.id}" \u2014 ${b.title} (${session.messages.length} messages)` });
|
|
1190
1115
|
ctx.sendSessionMessages();
|
|
1191
1116
|
ctx.sendStatus();
|
|
1117
|
+
} else {
|
|
1118
|
+
ctx.send({ type: "error", message: `Cannot switch to "${id}" (already active).` });
|
|
1119
|
+
}
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
if (sub === "delete") {
|
|
1123
|
+
const ref = args[1];
|
|
1124
|
+
if (!ref) {
|
|
1125
|
+
ctx.send({ type: "error", message: "Usage: /branch delete <id|title>" });
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
const id = resolve5(ref);
|
|
1129
|
+
if (!id) return;
|
|
1130
|
+
const ok = session.deleteBranch(id);
|
|
1131
|
+
if (ok) {
|
|
1132
|
+
await ctx.sessions.save();
|
|
1133
|
+
ctx.send({ type: "info", message: `\u2713 Deleted branch "${id}"` });
|
|
1134
|
+
ctx.sendStatus();
|
|
1135
|
+
} else {
|
|
1136
|
+
ctx.send({ type: "error", message: `Cannot delete "${id}" (active, or last remaining branch).` });
|
|
1137
|
+
}
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
if (sub === "rename") {
|
|
1141
|
+
const ref = args[1];
|
|
1142
|
+
const title = args.slice(2).join(" ").trim();
|
|
1143
|
+
if (!ref || !title) {
|
|
1144
|
+
ctx.send({ type: "error", message: "Usage: /branch rename <id|title> <new title>" });
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
const id = resolve5(ref);
|
|
1148
|
+
if (!id) return;
|
|
1149
|
+
const ok = session.renameBranch(id, title);
|
|
1150
|
+
if (ok) {
|
|
1151
|
+
await ctx.sessions.save();
|
|
1152
|
+
ctx.send({ type: "info", message: `\u2713 Renamed branch "${id}" \u2192 "${title}"` });
|
|
1153
|
+
ctx.sendStatus();
|
|
1154
|
+
} else {
|
|
1155
|
+
ctx.send({ type: "error", message: `Branch "${id}" not found.` });
|
|
1156
|
+
}
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
if (sub === "diff") {
|
|
1160
|
+
const ref = args[1];
|
|
1161
|
+
if (!ref) {
|
|
1162
|
+
ctx.send({ type: "error", message: "Usage: /branch diff <id|title>" });
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
const id = resolve5(ref);
|
|
1166
|
+
if (!id) return;
|
|
1167
|
+
const d = session.diffBranches(id);
|
|
1168
|
+
if (!d) {
|
|
1169
|
+
ctx.send({ type: "error", message: `Branch "${id}" not found.` });
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
const preview = (m) => {
|
|
1173
|
+
const text = typeof m.content === "string" ? m.content : "[complex content]";
|
|
1174
|
+
return text.replace(/\s+/g, " ").trim().slice(0, 70);
|
|
1175
|
+
};
|
|
1176
|
+
const lines = [
|
|
1177
|
+
`\u{1F500} diff ${id} (source) \u2194 ${session.activeBranchId} (active)`,
|
|
1178
|
+
`Common prefix: ${d.commonPrefix} message(s)`
|
|
1179
|
+
];
|
|
1180
|
+
if (d.sourceOnly.length === 0 && d.targetOnly.length === 0) {
|
|
1181
|
+
lines.push("Branches are identical.");
|
|
1182
|
+
} else {
|
|
1183
|
+
if (d.sourceOnly.length > 0) {
|
|
1184
|
+
lines.push("", `Only in ${id} (${d.sourceOnly.length}):`);
|
|
1185
|
+
d.sourceOnly.forEach(
|
|
1186
|
+
(m, i) => lines.push(` [${d.commonPrefix + i}] ${m.role}: ${preview(m)}`)
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
if (d.targetOnly.length > 0) {
|
|
1190
|
+
lines.push("", `Only in ${session.activeBranchId} (active) (${d.targetOnly.length}):`);
|
|
1191
|
+
d.targetOnly.forEach(
|
|
1192
|
+
(m, i) => lines.push(` [${d.commonPrefix + i}] ${m.role}: ${preview(m)}`)
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
lines.push("", `Use /branch cherry-pick ${id} <index> to copy a message into the active branch.`);
|
|
1196
|
+
}
|
|
1197
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
if (sub === "cherry-pick") {
|
|
1201
|
+
const ref = args[1];
|
|
1202
|
+
const idxStr = args[2];
|
|
1203
|
+
if (!ref || !idxStr) {
|
|
1204
|
+
ctx.send({ type: "error", message: "Usage: /branch cherry-pick <source-id|title> <msg-index>" });
|
|
1205
|
+
return;
|
|
1192
1206
|
}
|
|
1193
|
-
|
|
1207
|
+
const id = resolve5(ref);
|
|
1208
|
+
if (!id) return;
|
|
1209
|
+
const idx = parseInt(idxStr, 10);
|
|
1210
|
+
if (Number.isNaN(idx)) {
|
|
1211
|
+
ctx.send({ type: "error", message: `Invalid message index: "${idxStr}"` });
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
const picked = session.cherryPickMessage(id, idx);
|
|
1215
|
+
if (!picked) {
|
|
1216
|
+
ctx.send({ type: "error", message: `Cherry-pick failed \u2014 index ${idx} out of range for branch "${id}".` });
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
await ctx.sessions.save();
|
|
1220
|
+
const pickPreview = typeof picked.content === "string" ? picked.content.replace(/\s+/g, " ").trim().slice(0, 60) : "[complex content]";
|
|
1221
|
+
ctx.send({ type: "info", message: `\u2713 Cherry-picked ${picked.role} message from "${id}"[${idx}] \u2192 active branch (${session.messages.length} msgs): ${pickPreview}` });
|
|
1222
|
+
ctx.sendSessionMessages();
|
|
1223
|
+
ctx.sendStatus();
|
|
1224
|
+
return;
|
|
1194
1225
|
}
|
|
1226
|
+
ctx.send({ type: "error", message: `Unknown subcommand: ${sub}. Use list/new/switch/delete/rename/diff/cherry-pick.` });
|
|
1195
1227
|
}
|
|
1196
1228
|
async function handleFork(args, ctx) {
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
messageCount = cp.messageIndex;
|
|
1212
|
-
fromLabel = `checkpoint "${cp.name}" (${cp.messageIndex} messages)`;
|
|
1229
|
+
const session = ctx.sessions.current;
|
|
1230
|
+
if (!session) {
|
|
1231
|
+
ctx.send({ type: "info", message: "No active session to fork." });
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
const sub = args.join(" ").trim();
|
|
1235
|
+
let messageCount = session.messages.length;
|
|
1236
|
+
let fromLabel = "current position";
|
|
1237
|
+
if (sub) {
|
|
1238
|
+
const cp = session.checkpoints.find((c) => c.name === sub);
|
|
1239
|
+
if (!cp) {
|
|
1240
|
+
const available = session.checkpoints.map((c) => c.name);
|
|
1241
|
+
ctx.send({ type: "error", message: available.length > 0 ? `Checkpoint "${sub}" not found. Available: ${available.join(", ")}` : `Checkpoint "${sub}" not found. No checkpoints saved.` });
|
|
1242
|
+
return;
|
|
1213
1243
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1244
|
+
messageCount = cp.messageIndex;
|
|
1245
|
+
fromLabel = `checkpoint "${cp.name}" (${cp.messageIndex} messages)`;
|
|
1246
|
+
}
|
|
1247
|
+
try {
|
|
1248
|
+
const originalId = session.id.slice(0, 8);
|
|
1249
|
+
const forked = await ctx.sessions.forkSession(messageCount);
|
|
1250
|
+
ctx.send({ type: "info", message: `\u{1F500} Session Forked
|
|
1218
1251
|
Original: ${originalId}
|
|
1219
1252
|
Forked: ${forked.id.slice(0, 8)} "${forked.title ?? "(untitled)"}"
|
|
1220
1253
|
From: ${fromLabel}
|
|
1221
1254
|
Messages: ${forked.messages.length} copied` });
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
}
|
|
1255
|
+
ctx.sendSessionMessages();
|
|
1256
|
+
ctx.sendStatus();
|
|
1257
|
+
ctx.sendSessionList();
|
|
1258
|
+
} catch (err) {
|
|
1259
|
+
ctx.send({ type: "error", message: `Fork failed: ${err.message}` });
|
|
1228
1260
|
}
|
|
1229
1261
|
}
|
|
1230
1262
|
async function handleRewind(args, ctx) {
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1263
|
+
const session = ctx.sessions.current;
|
|
1264
|
+
if (!session || session.messages.length === 0) {
|
|
1265
|
+
ctx.send({ type: "info", message: "No messages to rewind." });
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
const rewindSub = args[0];
|
|
1269
|
+
if (rewindSub === "list" || !rewindSub) {
|
|
1270
|
+
const lines = [`Conversation messages (${session.messages.length} total):
|
|
1239
1271
|
`];
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
const rewindN = parseInt(rewindSub, 10);
|
|
1251
|
-
if (isNaN(rewindN) || rewindN < 1 || rewindN > session.messages.length) {
|
|
1252
|
-
ctx.send({ type: "error", message: `Invalid message number: ${rewindSub}. Range: 1-${session.messages.length}` });
|
|
1253
|
-
}
|
|
1254
|
-
const { fileCheckpoints: fc } = await import("./file-checkpoint-NKBHGC7L.js");
|
|
1255
|
-
const rewindRemoved = session.messages.length - rewindN;
|
|
1256
|
-
const rewindResult = fc.restoreToMessageIndex(rewindN);
|
|
1257
|
-
session.messages = session.messages.slice(0, rewindN);
|
|
1258
|
-
session.checkpoints = session.checkpoints.filter((c) => c.messageIndex <= rewindN);
|
|
1259
|
-
session.updated = /* @__PURE__ */ new Date();
|
|
1260
|
-
ctx.sessions.save();
|
|
1261
|
-
const rewindLines = [`\u2713 Rewound to message ${rewindN}`, ` Messages removed: ${rewindRemoved}`];
|
|
1262
|
-
if (rewindResult.restored > 0 || rewindResult.deleted > 0) {
|
|
1263
|
-
rewindLines.push(` Files restored: ${rewindResult.restored}, deleted: ${rewindResult.deleted}`);
|
|
1264
|
-
for (const f of rewindResult.files) rewindLines.push(` ${f}`);
|
|
1265
|
-
} else {
|
|
1266
|
-
rewindLines.push(" No file changes to revert.");
|
|
1267
|
-
}
|
|
1268
|
-
ctx.send({ type: "info", message: rewindLines.join("\n") });
|
|
1269
|
-
ctx.sendSessionMessages();
|
|
1270
|
-
ctx.sendStatus();
|
|
1272
|
+
const cpIndices = (await import("./file-checkpoint-NKBHGC7L.js")).fileCheckpoints.getMessageIndices();
|
|
1273
|
+
for (let i = 0; i < session.messages.length; i++) {
|
|
1274
|
+
const m = session.messages[i];
|
|
1275
|
+
const text = getContentText(m.content).replace(/\n/g, " ").slice(0, 60);
|
|
1276
|
+
const pin = cpIndices.includes(i) ? " \u{1F4CC}" : "";
|
|
1277
|
+
lines.push(` [${i + 1}] ${m.role.padEnd(10)} ${text}${pin}`);
|
|
1278
|
+
}
|
|
1279
|
+
lines.push("", "Usage: /rewind <n> \u2014 rewind to message N", "\u{1F4CC} = file checkpoint");
|
|
1280
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
1281
|
+
return;
|
|
1271
1282
|
}
|
|
1283
|
+
const rewindN = parseInt(rewindSub, 10);
|
|
1284
|
+
if (isNaN(rewindN) || rewindN < 1 || rewindN > session.messages.length) {
|
|
1285
|
+
ctx.send({ type: "error", message: `Invalid message number: ${rewindSub}. Range: 1-${session.messages.length}` });
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
const { fileCheckpoints: fc } = await import("./file-checkpoint-NKBHGC7L.js");
|
|
1289
|
+
const rewindRemoved = session.messages.length - rewindN;
|
|
1290
|
+
const rewindResult = fc.restoreToMessageIndex(rewindN);
|
|
1291
|
+
session.messages = session.messages.slice(0, rewindN);
|
|
1292
|
+
session.checkpoints = session.checkpoints.filter((c) => c.messageIndex <= rewindN);
|
|
1293
|
+
session.updated = /* @__PURE__ */ new Date();
|
|
1294
|
+
ctx.sessions.save();
|
|
1295
|
+
const rewindLines = [`\u2713 Rewound to message ${rewindN}`, ` Messages removed: ${rewindRemoved}`];
|
|
1296
|
+
if (rewindResult.restored > 0 || rewindResult.deleted > 0) {
|
|
1297
|
+
rewindLines.push(` Files restored: ${rewindResult.restored}, deleted: ${rewindResult.deleted}`);
|
|
1298
|
+
for (const f of rewindResult.files) rewindLines.push(` ${f}`);
|
|
1299
|
+
} else {
|
|
1300
|
+
rewindLines.push(" No file changes to revert.");
|
|
1301
|
+
}
|
|
1302
|
+
ctx.send({ type: "info", message: rewindLines.join("\n") });
|
|
1303
|
+
ctx.sendSessionMessages();
|
|
1304
|
+
ctx.sendStatus();
|
|
1272
1305
|
}
|
|
1273
1306
|
|
|
1274
1307
|
// src/web/commands/tooling-commands.ts
|
|
1275
|
-
|
|
1276
|
-
import { join } from "path";
|
|
1277
|
-
async function handleTools(args, ctx) {
|
|
1308
|
+
async function handleTools(_args, ctx) {
|
|
1278
1309
|
ctx.sendToolsList();
|
|
1279
1310
|
}
|
|
1280
|
-
async function handlePlugins(
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
for (const t of pluginTools) lines.push(` \u2022 ${t.definition.name} \u2014 ${t.definition.description}`);
|
|
1299
|
-
}
|
|
1311
|
+
async function handlePlugins(_args, ctx) {
|
|
1312
|
+
const configDir = ctx.config.getConfigDir();
|
|
1313
|
+
const pluginsDir = join(configDir, PLUGINS_DIR_NAME);
|
|
1314
|
+
const pluginTools = ctx.toolRegistry.listPluginTools();
|
|
1315
|
+
const pluginPackages = listInstalledPlugins(configDir);
|
|
1316
|
+
const lines = [`\u{1F50C} **Plugins:**`, `Dir: ${pluginsDir}`, ""];
|
|
1317
|
+
if (pluginTools.length === 0 && pluginPackages.length === 0) {
|
|
1318
|
+
lines.push("No plugins loaded.");
|
|
1319
|
+
lines.push("Use /plugin install <path>, /plugin trust <name>, then /plugin enable <name>.");
|
|
1320
|
+
} else {
|
|
1321
|
+
if (pluginPackages.length > 0) {
|
|
1322
|
+
lines.push(`Package plugins (${pluginPackages.length}):`);
|
|
1323
|
+
for (const p of pluginPackages) lines.push(` \u2022 ${p.name}@${p.manifest.version} \u2014 ${p.enabled ? "enabled" : "disabled"} / ${p.trusted ? "trusted" : "untrusted"}`);
|
|
1324
|
+
lines.push("");
|
|
1325
|
+
}
|
|
1326
|
+
if (pluginTools.length > 0) {
|
|
1327
|
+
lines.push(`Legacy JS plugin tools (${pluginTools.length}):`);
|
|
1328
|
+
for (const t of pluginTools) lines.push(` \u2022 ${t.definition.name} \u2014 ${t.definition.description}`);
|
|
1300
1329
|
}
|
|
1301
|
-
ctx.send({ type: "info", message: lines.join("\n") });
|
|
1302
1330
|
}
|
|
1331
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
1303
1332
|
}
|
|
1304
1333
|
async function handlePlugin(args, ctx) {
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
}
|
|
1316
|
-
if (sub === "install") {
|
|
1317
|
-
const input = args[1];
|
|
1318
|
-
if (!input) throw new Error("Usage: /plugin install <path>");
|
|
1319
|
-
const plugin = installPlugin(configDir, input);
|
|
1320
|
-
ctx.send({ type: "info", message: `Installed plugin ${plugin.name}@${plugin.manifest.version}. Run /plugin trust ${plugin.name}, then /plugin enable ${plugin.name}.` });
|
|
1321
|
-
}
|
|
1322
|
-
if (sub === "trust") {
|
|
1323
|
-
const name = args[1];
|
|
1324
|
-
if (!name) throw new Error("Usage: /plugin trust <name>");
|
|
1325
|
-
const state = trustPlugin(configDir, name);
|
|
1326
|
-
ctx.send({ type: "info", message: `Trusted plugin ${state.name}.` });
|
|
1327
|
-
}
|
|
1328
|
-
if (sub === "enable" || sub === "disable") {
|
|
1329
|
-
const name = args[1];
|
|
1330
|
-
if (!name) throw new Error(`Usage: /plugin ${sub} <name>`);
|
|
1331
|
-
const state = setPluginEnabled(configDir, name, sub === "enable");
|
|
1332
|
-
ctx.send({ type: "info", message: `${sub === "enable" ? "Enabled" : "Disabled"} plugin ${state.name}. Restart Web UI for changes to take effect.` });
|
|
1333
|
-
}
|
|
1334
|
-
if (sub === "inspect") {
|
|
1335
|
-
const name = args[1];
|
|
1336
|
-
if (!name) throw new Error("Usage: /plugin inspect <name>");
|
|
1337
|
-
ctx.send({ type: "info", message: describePlugin(getInstalledPlugin(configDir, name)).join("\n") });
|
|
1338
|
-
}
|
|
1339
|
-
throw new Error("Usage: /plugin [list|install|enable|disable|inspect|trust]");
|
|
1340
|
-
} catch (err) {
|
|
1341
|
-
ctx.send({ type: "error", message: err instanceof Error ? err.message : String(err) });
|
|
1334
|
+
const configDir = ctx.config.getConfigDir();
|
|
1335
|
+
const sub = args[0] ?? "list";
|
|
1336
|
+
try {
|
|
1337
|
+
if (sub === "list") {
|
|
1338
|
+
const plugins = listInstalledPlugins(configDir);
|
|
1339
|
+
const lines = [`\u{1F50C} **Package Plugins**`, `Dir: ${pluginRoot(configDir)}`, ""];
|
|
1340
|
+
if (plugins.length === 0) lines.push("No package plugins installed.");
|
|
1341
|
+
for (const p of plugins) lines.push(` \u2022 ${p.name}@${p.manifest.version} \u2014 ${p.enabled ? "enabled" : "disabled"} / ${p.trusted ? "trusted" : "untrusted"}`);
|
|
1342
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
1343
|
+
return;
|
|
1342
1344
|
}
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
} catch (err) {
|
|
1357
|
-
ctx.send({ type: "error", message: `Reconnect failed: ${err.message}` });
|
|
1358
|
-
}
|
|
1359
|
-
ctx.sendToolsList();
|
|
1345
|
+
if (sub === "install") {
|
|
1346
|
+
const input = args[1];
|
|
1347
|
+
if (!input) throw new Error("Usage: /plugin install <path>");
|
|
1348
|
+
const plugin = installPlugin(configDir, input);
|
|
1349
|
+
ctx.send({ type: "info", message: `Installed plugin ${plugin.name}@${plugin.manifest.version}. Run /plugin trust ${plugin.name}, then /plugin enable ${plugin.name}.` });
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
if (sub === "trust") {
|
|
1353
|
+
const name = args[1];
|
|
1354
|
+
if (!name) throw new Error("Usage: /plugin trust <name>");
|
|
1355
|
+
const state = trustPlugin(configDir, name);
|
|
1356
|
+
ctx.send({ type: "info", message: `Trusted plugin ${state.name}.` });
|
|
1357
|
+
return;
|
|
1360
1358
|
}
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1359
|
+
if (sub === "enable" || sub === "disable") {
|
|
1360
|
+
const name = args[1];
|
|
1361
|
+
if (!name) throw new Error(`Usage: /plugin ${sub} <name>`);
|
|
1362
|
+
const state = setPluginEnabled(configDir, name, sub === "enable");
|
|
1363
|
+
ctx.send({ type: "info", message: `${sub === "enable" ? "Enabled" : "Disabled"} plugin ${state.name}. Restart Web UI for changes to take effect.` });
|
|
1364
|
+
return;
|
|
1364
1365
|
}
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1366
|
+
if (sub === "inspect") {
|
|
1367
|
+
const name = args[1];
|
|
1368
|
+
if (!name) throw new Error("Usage: /plugin inspect <name>");
|
|
1369
|
+
ctx.send({ type: "info", message: describePlugin(getInstalledPlugin(configDir, name)).join("\n") });
|
|
1370
|
+
return;
|
|
1369
1371
|
}
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
ctx.send({ type: "
|
|
1372
|
+
throw new Error("Usage: /plugin [list|install|enable|disable|inspect|trust]");
|
|
1373
|
+
} catch (err) {
|
|
1374
|
+
ctx.send({ type: "error", message: err instanceof Error ? err.message : String(err) });
|
|
1373
1375
|
}
|
|
1374
1376
|
}
|
|
1375
|
-
async function
|
|
1376
|
-
{
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1377
|
+
async function handleMcp(_args, ctx) {
|
|
1378
|
+
if (!ctx.mcpManager) {
|
|
1379
|
+
ctx.send({ type: "info", message: "MCP not configured. Add mcpServers to config.json." });
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
const sub = _args[0];
|
|
1383
|
+
if (sub === "reconnect") {
|
|
1384
|
+
ctx.send({ type: "info", message: "\u{1F504} Reconnecting MCP servers..." });
|
|
1385
|
+
try {
|
|
1386
|
+
await ctx.mcpManager.reconnectAll();
|
|
1387
|
+
ctx.send({ type: "info", message: "\u2713 MCP reconnect complete." });
|
|
1388
|
+
} catch (err) {
|
|
1389
|
+
ctx.send({ type: "error", message: `Reconnect failed: ${err.message}` });
|
|
1390
|
+
}
|
|
1391
|
+
ctx.sendToolsList();
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
const statuses = ctx.mcpManager.getStatus();
|
|
1395
|
+
if (statuses.length === 0) {
|
|
1396
|
+
ctx.send({ type: "info", message: "No MCP servers configured." });
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
const lines = [`\u{1F50C} **MCP Servers (${statuses.length}):**`, ""];
|
|
1400
|
+
for (const s of statuses) {
|
|
1401
|
+
const state = s.connected ? `\u2713 connected \xB7 ${s.serverName} \xB7 ${s.toolCount} tools` : `\u2717 disconnected${s.error ? ` \xB7 ${s.error}` : ""}`;
|
|
1402
|
+
lines.push(` ${s.serverId}: ${state}`);
|
|
1403
|
+
}
|
|
1404
|
+
lines.push("");
|
|
1405
|
+
lines.push("Use /mcp reconnect to reconnect.");
|
|
1406
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
1407
|
+
}
|
|
1408
|
+
async function handleSkill(args, ctx) {
|
|
1409
|
+
const sub = args[0];
|
|
1410
|
+
if (!ctx.skillManager) {
|
|
1411
|
+
ctx.send({ type: "error", message: "Skill system not available." });
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
if (!sub || sub === "list") {
|
|
1415
|
+
const skills = ctx.skillManager.listSkills();
|
|
1416
|
+
const active = ctx.skillManager.getActive();
|
|
1417
|
+
if (skills.length === 0) {
|
|
1418
|
+
ctx.send({ type: "info", message: "No skills available. Add .md files to ~/.aicli/skills/" });
|
|
1404
1419
|
} else {
|
|
1405
|
-
const
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
${
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
const available = ctx.skillManager.listSkills().map((s) => s.meta.name).join(", ");
|
|
1413
|
-
ctx.send({ type: "error", message: `Skill "${sub}" not found. Available: ${available}` });
|
|
1414
|
-
}
|
|
1420
|
+
const lines = skills.map((s) => {
|
|
1421
|
+
const isActive = active?.meta.name === s.meta.name;
|
|
1422
|
+
const marker = isActive ? " \u2705 (active)" : "";
|
|
1423
|
+
return ` ${s.meta.name}${marker} \u2014 ${s.meta.description || "(no description)"}`;
|
|
1424
|
+
});
|
|
1425
|
+
ctx.send({ type: "info", message: `\u{1F4DA} Skills:
|
|
1426
|
+
${lines.join("\n")}` });
|
|
1415
1427
|
}
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
if (sub === "off") {
|
|
1431
|
+
ctx.skillManager.deactivate();
|
|
1432
|
+
ctx.send({ type: "info", message: "\u{1F50C} Skill deactivated." });
|
|
1433
|
+
ctx.sendToolsList();
|
|
1434
|
+
ctx.sendStatus();
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
if (sub === "reload") {
|
|
1438
|
+
ctx.skillManager.loadSkills(getActivePluginAssets(ctx.config.getConfigDir()).skillDirs);
|
|
1439
|
+
ctx.send({ type: "info", message: `\u{1F504} Reloaded ${ctx.skillManager.listSkills().length} skill(s).` });
|
|
1440
|
+
ctx.sendToolsList();
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
const activated = ctx.skillManager.activate(sub);
|
|
1444
|
+
if (activated) {
|
|
1445
|
+
ctx.send({ type: "info", message: `\u2705 Skill activated: ${activated.meta.name}
|
|
1446
|
+
${activated.meta.description || ""}` });
|
|
1447
|
+
ctx.sendToolsList();
|
|
1448
|
+
ctx.sendStatus();
|
|
1449
|
+
} else {
|
|
1450
|
+
const available = ctx.skillManager.listSkills().map((s) => s.meta.name).join(", ");
|
|
1451
|
+
ctx.send({ type: "error", message: `Skill "${sub}" not found. Available: ${available}` });
|
|
1416
1452
|
}
|
|
1417
1453
|
}
|
|
1418
|
-
async function handleCommands(
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
ctx.send({ type: "info", message: `No custom commands directory.
|
|
1454
|
+
async function handleCommands(_args, ctx) {
|
|
1455
|
+
const configDir = ctx.config.getConfigDir();
|
|
1456
|
+
const commandsDir = join(configDir, CUSTOM_COMMANDS_DIR_NAME);
|
|
1457
|
+
if (!existsSync2(commandsDir)) {
|
|
1458
|
+
ctx.send({ type: "info", message: `No custom commands directory.
|
|
1424
1459
|
Create: ${commandsDir}/ with .md files.` });
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
try {
|
|
1463
|
+
const files = readdirSync(commandsDir).filter((f) => f.endsWith(".md"));
|
|
1464
|
+
if (files.length === 0) {
|
|
1465
|
+
ctx.send({ type: "info", message: `No custom commands found in ${commandsDir}
|
|
1430
1466
|
Add .md files to create commands.` });
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
}
|
|
1437
|
-
ctx.send({ type: "info", message: lines.join("\n") });
|
|
1467
|
+
} else {
|
|
1468
|
+
const lines = [`\u{1F4CB} **Custom Commands (${files.length}):**`, `Dir: ${commandsDir}`, ""];
|
|
1469
|
+
for (const f of files) {
|
|
1470
|
+
const name = f.replace(/\.md$/, "");
|
|
1471
|
+
lines.push(` /${name}`);
|
|
1438
1472
|
}
|
|
1439
|
-
|
|
1440
|
-
ctx.send({ type: "info", message: `Cannot read: ${commandsDir}` });
|
|
1473
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
1441
1474
|
}
|
|
1475
|
+
} catch {
|
|
1476
|
+
ctx.send({ type: "info", message: `Cannot read: ${commandsDir}` });
|
|
1442
1477
|
}
|
|
1443
1478
|
}
|
|
1444
1479
|
|
|
1445
1480
|
// src/web/commands/review-commands.ts
|
|
1446
|
-
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
1447
|
-
import { join as join2 } from "path";
|
|
1448
1481
|
async function handleReview(args, ctx) {
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
+
const gitCtx = getGitContext();
|
|
1483
|
+
if (!gitCtx) {
|
|
1484
|
+
ctx.send({ type: "error", message: "Not a git repository." });
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
const staged = args.includes("--staged");
|
|
1488
|
+
const detailed = args.includes("--detailed");
|
|
1489
|
+
let diff;
|
|
1490
|
+
try {
|
|
1491
|
+
diff = readGitDiff({ staged });
|
|
1492
|
+
} catch {
|
|
1493
|
+
ctx.send({ type: "error", message: "Failed to run git diff." });
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
if (!diff) {
|
|
1497
|
+
ctx.send({ type: "info", message: "No changes to review." + (staged ? "" : " Try --staged for staged changes.") });
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
const MAX_DIFF = 8e3;
|
|
1501
|
+
let truncated = false;
|
|
1502
|
+
if (diff.length > MAX_DIFF) {
|
|
1503
|
+
const head = diff.slice(0, Math.floor(MAX_DIFF * 0.7));
|
|
1504
|
+
const tail = diff.slice(diff.length - Math.floor(MAX_DIFF * 0.2));
|
|
1505
|
+
diff = head + "\n\n... [diff truncated, " + diff.length + " chars total] ...\n\n" + tail;
|
|
1506
|
+
truncated = true;
|
|
1507
|
+
}
|
|
1508
|
+
const reviewPrompt = buildReviewPrompt(diff, formatGitContextForPrompt(gitCtx), detailed);
|
|
1509
|
+
ctx.send({ type: "info", message: "\u{1F50D} Analyzing changes..." });
|
|
1510
|
+
try {
|
|
1511
|
+
const review = await ctx.chatOnce(reviewPrompt, { temperature: 0.3, maxTokens: 8192 });
|
|
1512
|
+
const msg = truncated ? review + "\n\n\u26A0 Diff was truncated. Consider reviewing smaller changesets." : review;
|
|
1513
|
+
ctx.send({ type: "info", message: msg });
|
|
1514
|
+
} catch (err) {
|
|
1515
|
+
ctx.send({ type: "error", message: `Review failed: ${err.message}` });
|
|
1482
1516
|
}
|
|
1483
1517
|
}
|
|
1484
1518
|
async function handleSecurityReview(args, ctx) {
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1519
|
+
const gitCtx = getGitContext();
|
|
1520
|
+
if (!gitCtx) {
|
|
1521
|
+
ctx.send({ type: "error", message: "Not a git repository." });
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
const secStaged = args.includes("--staged");
|
|
1525
|
+
let secDiff;
|
|
1526
|
+
try {
|
|
1527
|
+
secDiff = readGitDiff({ staged: secStaged });
|
|
1528
|
+
} catch {
|
|
1529
|
+
ctx.send({ type: "error", message: "Failed to run git diff." });
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
if (!secDiff) {
|
|
1533
|
+
ctx.send({ type: "info", message: "No changes to review." + (secStaged ? "" : " Try --staged for staged changes.") });
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
const SEC_MAX_DIFF = 8e3;
|
|
1537
|
+
let secTruncated = false;
|
|
1538
|
+
if (secDiff.length > SEC_MAX_DIFF) {
|
|
1539
|
+
const head = secDiff.slice(0, Math.floor(SEC_MAX_DIFF * 0.7));
|
|
1540
|
+
const tail = secDiff.slice(secDiff.length - Math.floor(SEC_MAX_DIFF * 0.2));
|
|
1541
|
+
secDiff = head + "\n\n... [diff truncated, " + secDiff.length + " chars total] ...\n\n" + tail;
|
|
1542
|
+
secTruncated = true;
|
|
1543
|
+
}
|
|
1544
|
+
const secPrompt = buildSecurityReviewPrompt(secDiff, formatGitContextForPrompt(gitCtx));
|
|
1545
|
+
ctx.send({ type: "info", message: "\u{1F512} Scanning for security vulnerabilities..." });
|
|
1546
|
+
try {
|
|
1547
|
+
const secReview = await ctx.chatOnce(secPrompt, { temperature: 0.2, maxTokens: 8192 });
|
|
1548
|
+
const secMsg = secTruncated ? secReview + "\n\n\u26A0 Diff was truncated. Consider reviewing smaller changesets." : secReview;
|
|
1549
|
+
ctx.send({ type: "info", message: secMsg });
|
|
1550
|
+
} catch (err) {
|
|
1551
|
+
ctx.send({ type: "error", message: `Security review failed: ${err.message}` });
|
|
1517
1552
|
}
|
|
1518
1553
|
}
|
|
1519
1554
|
async function handleTest(args, ctx) {
|
|
1520
|
-
{
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
ctx.send({ type: "error", message: `Test failed: ${err.message}` });
|
|
1534
|
-
}
|
|
1555
|
+
ctx.send({ type: "info", message: "\u{1F9EA} Running tests..." });
|
|
1556
|
+
try {
|
|
1557
|
+
const { executeTests } = await import("./run-tests-AUEKHJCC.js");
|
|
1558
|
+
const argStr = args.join(" ").trim();
|
|
1559
|
+
let testArgs = {};
|
|
1560
|
+
if (argStr) {
|
|
1561
|
+
const isCommand = argStr.includes(" ") || /^(mvn|gradle|npm|pytest|cargo|go)\b/.test(argStr);
|
|
1562
|
+
testArgs = isCommand ? { command: argStr } : { filter: argStr };
|
|
1563
|
+
}
|
|
1564
|
+
const report = await executeTests(testArgs);
|
|
1565
|
+
ctx.send({ type: "info", message: report });
|
|
1566
|
+
} catch (err) {
|
|
1567
|
+
ctx.send({ type: "error", message: `Test failed: ${err.message}` });
|
|
1535
1568
|
}
|
|
1536
1569
|
}
|
|
1537
1570
|
async function handleDiff(args, ctx) {
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1571
|
+
const history = undoStack.getHistory();
|
|
1572
|
+
if (history.length === 0) {
|
|
1573
|
+
ctx.send({ type: "info", message: "No file modifications in this session." });
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
const fileMap = /* @__PURE__ */ new Map();
|
|
1577
|
+
for (const entry of history) {
|
|
1578
|
+
if (!fileMap.has(entry.filePath)) {
|
|
1579
|
+
fileMap.set(entry.filePath, { earliest: entry.previousContent });
|
|
1542
1580
|
}
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1581
|
+
}
|
|
1582
|
+
const statsOnly = args[0]?.toLowerCase() === "--stats";
|
|
1583
|
+
let newFiles = 0;
|
|
1584
|
+
let modifiedFiles = 0;
|
|
1585
|
+
const diffLines = [];
|
|
1586
|
+
for (const [filePath, { earliest }] of fileMap) {
|
|
1587
|
+
const currentContent = existsSync2(filePath) ? (() => {
|
|
1588
|
+
try {
|
|
1589
|
+
return readFileSync2(filePath, "utf-8");
|
|
1590
|
+
} catch {
|
|
1591
|
+
return null;
|
|
1547
1592
|
}
|
|
1548
|
-
}
|
|
1549
|
-
const
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
try {
|
|
1556
|
-
return readFileSync3(filePath, "utf-8");
|
|
1557
|
-
} catch {
|
|
1558
|
-
return null;
|
|
1559
|
-
}
|
|
1560
|
-
})() : null;
|
|
1561
|
-
const isNew = earliest === null;
|
|
1562
|
-
if (isNew) newFiles++;
|
|
1563
|
-
else modifiedFiles++;
|
|
1564
|
-
if (statsOnly) continue;
|
|
1565
|
-
if (isNew) {
|
|
1566
|
-
diffLines.push(`
|
|
1593
|
+
})() : null;
|
|
1594
|
+
const isNew = earliest === null;
|
|
1595
|
+
if (isNew) newFiles++;
|
|
1596
|
+
else modifiedFiles++;
|
|
1597
|
+
if (statsOnly) continue;
|
|
1598
|
+
if (isNew) {
|
|
1599
|
+
diffLines.push(`
|
|
1567
1600
|
\u{1F4C4} [NEW] ${filePath}`);
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
} else {
|
|
1571
|
-
diffLines.push(" (file was deleted after creation)");
|
|
1572
|
-
}
|
|
1601
|
+
if (currentContent !== null) {
|
|
1602
|
+
diffLines.push(` ${currentContent.split("\n").length} lines`);
|
|
1573
1603
|
} else {
|
|
1574
|
-
diffLines.push(
|
|
1604
|
+
diffLines.push(" (file was deleted after creation)");
|
|
1605
|
+
}
|
|
1606
|
+
} else {
|
|
1607
|
+
diffLines.push(`
|
|
1575
1608
|
\u{1F4DD} [MODIFIED] ${filePath}`);
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
}
|
|
1609
|
+
const oldText = earliest ?? "";
|
|
1610
|
+
const newText = currentContent ?? "";
|
|
1611
|
+
if (oldText === newText) {
|
|
1612
|
+
diffLines.push(" (no net change)");
|
|
1613
|
+
} else {
|
|
1614
|
+
diffLines.push(renderDiff(oldText, newText, { filePath, maxLines: 80 }));
|
|
1583
1615
|
}
|
|
1584
1616
|
}
|
|
1585
|
-
diffLines.push(`
|
|
1586
|
-
**Summary:** ${fileMap.size} file(s) \u2014 ${newFiles} new, ${modifiedFiles} modified`);
|
|
1587
|
-
ctx.send({ type: "info", message: diffLines.join("\n") });
|
|
1588
1617
|
}
|
|
1618
|
+
diffLines.push(`
|
|
1619
|
+
**Summary:** ${fileMap.size} file(s) \u2014 ${newFiles} new, ${modifiedFiles} modified`);
|
|
1620
|
+
ctx.send({ type: "info", message: diffLines.join("\n") });
|
|
1589
1621
|
}
|
|
1590
1622
|
async function handleInit(args, ctx) {
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
ctx.send({ type: "info", message: `AICLI.md already exists at ${targetPath}
|
|
1623
|
+
const cwd = process.cwd();
|
|
1624
|
+
const targetPath = join(cwd, "AICLI.md");
|
|
1625
|
+
const force = args.includes("--force");
|
|
1626
|
+
if (existsSync2(targetPath) && !force) {
|
|
1627
|
+
ctx.send({ type: "info", message: `AICLI.md already exists at ${targetPath}
|
|
1597
1628
|
Use /init --force to overwrite.` });
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
ctx.send({ type: "info", message: "\u{1F4DD} Scanning project structure..." });
|
|
1632
|
+
try {
|
|
1633
|
+
const projectInfo = scanProject(cwd);
|
|
1634
|
+
const prompt = buildInitPrompt(projectInfo, cwd);
|
|
1635
|
+
const content = await ctx.chatOnce(prompt, { temperature: 0.3, maxTokens: 4096 });
|
|
1636
|
+
writeFileSync(targetPath, content, "utf-8");
|
|
1637
|
+
ctx.send({ type: "info", message: `\u2713 Generated: ${targetPath} (${content.length} chars)
|
|
1606
1638
|
Use /context reload to load it.` });
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
}
|
|
1639
|
+
} catch (err) {
|
|
1640
|
+
ctx.send({ type: "error", message: `Failed to generate AICLI.md: ${err.message}` });
|
|
1610
1641
|
}
|
|
1611
1642
|
}
|
|
1612
1643
|
async function handleScaffold(args, ctx) {
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1644
|
+
const description = args.join(" ").trim();
|
|
1645
|
+
if (!description) {
|
|
1646
|
+
ctx.send({ type: "error", message: "Usage: /scaffold <project description>" });
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
ctx.send({ type: "info", message: "\u{1F3D7}\uFE0F Generating project scaffold..." });
|
|
1650
|
+
const scaffoldPrompt = `Please scaffold a project based on this description: ${description}
|
|
1620
1651
|
|
|
1621
1652
|
Create the necessary files and directory structure. Use write_file and bash tools to set up the project.`;
|
|
1622
|
-
|
|
1623
|
-
}
|
|
1653
|
+
await ctx.handleChat(scaffoldPrompt);
|
|
1624
1654
|
}
|
|
1625
1655
|
|
|
1626
1656
|
// src/web/commands/memory-commands.ts
|
|
1627
|
-
import { existsSync as existsSync4, statSync as statSync3 } from "fs";
|
|
1628
|
-
import { resolve as resolve3 } from "path";
|
|
1629
1657
|
async function handleSystem(args, ctx) {
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
ctx.send({ type: "info", message: `\u{1F4CB} Current system prompt (${current.length} chars):
|
|
1658
|
+
const text = args.join(" ").trim();
|
|
1659
|
+
if (!text) {
|
|
1660
|
+
const current = ctx.activeSystemPrompt;
|
|
1661
|
+
if (current) {
|
|
1662
|
+
const preview = current.length > 500 ? current.slice(0, 500) + "..." : current;
|
|
1663
|
+
ctx.send({ type: "info", message: `\u{1F4CB} Current system prompt (${current.length} chars):
|
|
1637
1664
|
|
|
1638
1665
|
${preview}` });
|
|
1639
|
-
} else {
|
|
1640
|
-
ctx.send({ type: "info", message: "No custom system prompt set. Using default.\nUsage: /system <prompt>" });
|
|
1641
|
-
}
|
|
1642
|
-
}
|
|
1643
|
-
if (text === "clear" || text === "reset") {
|
|
1644
|
-
ctx.activeSystemPrompt = ctx.loadContextFiles();
|
|
1645
|
-
ctx.send({ type: "info", message: "\u2713 System prompt reset to default (context files)." });
|
|
1646
1666
|
} else {
|
|
1647
|
-
ctx.
|
|
1648
|
-
ctx.send({ type: "info", message: `\u2713 System prompt set (${text.length} chars).` });
|
|
1667
|
+
ctx.send({ type: "info", message: "No custom system prompt set. Using default.\nUsage: /system <prompt>" });
|
|
1649
1668
|
}
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
if (text === "clear" || text === "reset") {
|
|
1672
|
+
ctx.activeSystemPrompt = ctx.loadContextFiles();
|
|
1673
|
+
ctx.send({ type: "info", message: "\u2713 System prompt reset to default (context files)." });
|
|
1674
|
+
} else {
|
|
1675
|
+
ctx.activeSystemPrompt = text;
|
|
1676
|
+
ctx.send({ type: "info", message: `\u2713 System prompt set (${text.length} chars).` });
|
|
1650
1677
|
}
|
|
1651
1678
|
}
|
|
1652
|
-
async function handleContext(
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
layers.push("");
|
|
1673
|
-
layers.push("Skipped:");
|
|
1674
|
-
for (const skipped of result.skipped.slice(0, 5)) {
|
|
1675
|
-
layers.push(` - ${skipped.displayPath}: ${skipped.reason}${skipped.message ? ` (${skipped.message})` : ""}`);
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1679
|
+
async function handleContext(_args, ctx) {
|
|
1680
|
+
const sub = _args[0];
|
|
1681
|
+
if (sub === "reload") {
|
|
1682
|
+
ctx.activeSystemPrompt = ctx.loadContextFiles();
|
|
1683
|
+
ctx.send({ type: "info", message: ctx.activeSystemPrompt ? `\u2713 Context reloaded (${ctx.activeSystemPrompt.length} chars).` : "\u2713 No context files found." });
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
const result = ctx.contextLoadResult;
|
|
1687
|
+
const layers = ["\u{1F4DA} **Context Layers:**", ""];
|
|
1688
|
+
if (result && result.layers.length > 0) {
|
|
1689
|
+
const labels = { global: "Global ", project: "Project", plugin: "Plugin ", local: "Local " };
|
|
1690
|
+
for (const layer of result.layers) {
|
|
1691
|
+
layers.push(` \u2713 ${labels[layer.level] ?? layer.level}: ${layer.displayPath} (${layer.charCount} chars${layer.truncated ? ", truncated" : ""})`);
|
|
1692
|
+
}
|
|
1693
|
+
layers.push(` Total: ${result.totalChars} chars (${result.layers.length} layer${result.layers.length > 1 ? "s" : ""})`);
|
|
1694
|
+
} else {
|
|
1695
|
+
layers.push(" No context files loaded.");
|
|
1696
|
+
layers.push(" Search order per layer: AICLI.override.md \u2192 AGENTS.override.md \u2192 AICLI.md \u2192 CLAUDE.md \u2192 AGENTS.md");
|
|
1697
|
+
}
|
|
1698
|
+
if (result && result.skipped.length > 0) {
|
|
1678
1699
|
layers.push("");
|
|
1679
|
-
layers.push(
|
|
1680
|
-
|
|
1681
|
-
|
|
1700
|
+
layers.push("Skipped:");
|
|
1701
|
+
for (const skipped of result.skipped.slice(0, 5)) {
|
|
1702
|
+
layers.push(` - ${skipped.displayPath}: ${skipped.reason}${skipped.message ? ` (${skipped.message})` : ""}`);
|
|
1703
|
+
}
|
|
1682
1704
|
}
|
|
1705
|
+
layers.push("");
|
|
1706
|
+
layers.push(`Total prompt length: ${ctx.activeSystemPrompt?.length ?? 0} chars`);
|
|
1707
|
+
layers.push("Use /context reload to refresh.");
|
|
1708
|
+
ctx.send({ type: "info", message: layers.join("\n") });
|
|
1683
1709
|
}
|
|
1684
1710
|
async function handleIndex(args, ctx) {
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1711
|
+
const sub = (args[0] ?? "status").toLowerCase();
|
|
1712
|
+
const root = process.cwd();
|
|
1713
|
+
const { loadIndex, clearIndex } = await import("./store-MWNHVGJT.js");
|
|
1714
|
+
const { indexProject } = await import("./indexer-2AG4G6B5.js");
|
|
1715
|
+
const { loadVectorStore, clearVectorStore } = await import("./vector-store-BBDXB5IQ.js");
|
|
1716
|
+
if (sub === "status") {
|
|
1717
|
+
const idx = loadIndex(root);
|
|
1718
|
+
const vec = loadVectorStore(root);
|
|
1719
|
+
if (!idx) {
|
|
1720
|
+
ctx.send({ type: "info", message: `No symbol index for ${root}. Run /index rebuild.` });
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
const semantic = vec ? `${vec.count} vectors \xD7 ${vec.dim}-dim` : "not built \u2014 run /index semantic-rebuild to enable search_code";
|
|
1724
|
+
ctx.send({
|
|
1725
|
+
type: "info",
|
|
1726
|
+
message: [
|
|
1727
|
+
"\u{1F50E} Symbol index:",
|
|
1728
|
+
` Root: ${idx.root}`,
|
|
1729
|
+
` Generated: ${idx.generated}`,
|
|
1730
|
+
` Files: ${idx.fileCount}`,
|
|
1731
|
+
` Symbols: ${idx.symbolCount}`,
|
|
1732
|
+
` Semantic: ${semantic}`
|
|
1733
|
+
].join("\n")
|
|
1734
|
+
});
|
|
1735
|
+
} else if (sub === "rebuild") {
|
|
1736
|
+
ctx.send({ type: "info", message: `Indexing ${root}\u2026` });
|
|
1737
|
+
const { stats } = await indexProject(root, { force: true });
|
|
1738
|
+
ctx.send({
|
|
1739
|
+
type: "info",
|
|
1740
|
+
message: `\u2713 Indexed ${stats.filesParsed} files (${stats.symbols} symbols) in ${stats.durationMs}ms`
|
|
1741
|
+
});
|
|
1742
|
+
} else if (sub === "clear") {
|
|
1743
|
+
clearIndex(root);
|
|
1744
|
+
clearVectorStore(root);
|
|
1745
|
+
ctx.send({ type: "info", message: `\u2713 Cleared symbol + semantic index for ${root}` });
|
|
1746
|
+
} else if (sub === "semantic-rebuild") {
|
|
1747
|
+
const idx = loadIndex(root);
|
|
1748
|
+
if (!idx) {
|
|
1749
|
+
ctx.send({ type: "error", message: "No symbol index yet. Run /index rebuild first." });
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
if (idx.symbolCount === 0) {
|
|
1713
1753
|
ctx.send({
|
|
1714
|
-
type: "
|
|
1715
|
-
message:
|
|
1754
|
+
type: "error",
|
|
1755
|
+
message: "No code symbols to embed. Semantic search needs TS/TSX/JS/JSX/Python source files \u2014 this project appears to have none. Not downloading the embedding model."
|
|
1716
1756
|
});
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
ctx.send({
|
|
1728
|
-
type: "error",
|
|
1729
|
-
message: "No code symbols to embed. Semantic search needs TS/TSX/JS/JSX/Python source files \u2014 this project appears to have none. Not downloading the embedding model."
|
|
1730
|
-
});
|
|
1731
|
-
}
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
ctx.send({
|
|
1760
|
+
type: "info",
|
|
1761
|
+
message: `Building semantic index for ${idx.symbolCount} symbols\u2026 (first run downloads ~117 MB model)`
|
|
1762
|
+
});
|
|
1763
|
+
try {
|
|
1764
|
+
const { rebuildSemanticIndex } = await import("./semantic-HLAE2O4F.js");
|
|
1765
|
+
const stats = await rebuildSemanticIndex(root);
|
|
1766
|
+
const first = stats.modelFirstLoadMs ? ` (model load+first batch ${stats.modelFirstLoadMs}ms)` : "";
|
|
1732
1767
|
ctx.send({
|
|
1733
1768
|
type: "info",
|
|
1734
|
-
message:
|
|
1735
|
-
});
|
|
1736
|
-
try {
|
|
1737
|
-
const { rebuildSemanticIndex } = await import("./semantic-HLAE2O4F.js");
|
|
1738
|
-
const stats = await rebuildSemanticIndex(root);
|
|
1739
|
-
const first = stats.modelFirstLoadMs ? ` (model load+first batch ${stats.modelFirstLoadMs}ms)` : "";
|
|
1740
|
-
ctx.send({
|
|
1741
|
-
type: "info",
|
|
1742
|
-
message: `\u2713 Embedded ${stats.symbolsEmbedded} symbols in ${stats.durationMs}ms${first}`
|
|
1743
|
-
});
|
|
1744
|
-
} catch (err) {
|
|
1745
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1746
|
-
ctx.send({ type: "error", message: `Semantic rebuild failed: ${msg}` });
|
|
1747
|
-
}
|
|
1748
|
-
} else if (sub === "semantic-clear") {
|
|
1749
|
-
clearVectorStore(root);
|
|
1750
|
-
ctx.send({ type: "info", message: "\u2713 Cleared semantic index (symbol index preserved)" });
|
|
1751
|
-
} else {
|
|
1752
|
-
ctx.send({
|
|
1753
|
-
type: "error",
|
|
1754
|
-
message: `Unknown subcommand: ${sub}. Use status/rebuild/clear/semantic-rebuild/semantic-clear.`
|
|
1769
|
+
message: `\u2713 Embedded ${stats.symbolsEmbedded} symbols in ${stats.durationMs}ms${first}`
|
|
1755
1770
|
});
|
|
1771
|
+
} catch (err) {
|
|
1772
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1773
|
+
ctx.send({ type: "error", message: `Semantic rebuild failed: ${msg}` });
|
|
1756
1774
|
}
|
|
1775
|
+
} else if (sub === "semantic-clear") {
|
|
1776
|
+
clearVectorStore(root);
|
|
1777
|
+
ctx.send({ type: "info", message: "\u2713 Cleared semantic index (symbol index preserved)" });
|
|
1778
|
+
} else {
|
|
1779
|
+
ctx.send({
|
|
1780
|
+
type: "error",
|
|
1781
|
+
message: `Unknown subcommand: ${sub}. Use status/rebuild/clear/semantic-rebuild/semantic-clear.`
|
|
1782
|
+
});
|
|
1757
1783
|
}
|
|
1758
1784
|
}
|
|
1759
1785
|
async function handleMemory(args, ctx) {
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
ctx.memoryShow();
|
|
1768
|
-
}
|
|
1786
|
+
const sub = args[0];
|
|
1787
|
+
if (sub === "add" && args.length > 1) {
|
|
1788
|
+
ctx.memoryAdd(args.slice(1).join(" "));
|
|
1789
|
+
} else if (sub === "clear") {
|
|
1790
|
+
ctx.memoryClear();
|
|
1791
|
+
} else {
|
|
1792
|
+
ctx.memoryShow();
|
|
1769
1793
|
}
|
|
1770
1794
|
}
|
|
1771
1795
|
async function handleAddDir(args, ctx) {
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
ctx.send({ type: "info", message: ctx.addedDirs.size > 0 ? `\u{1F4C1} Added directories:
|
|
1796
|
+
const sub = args[0]?.trim();
|
|
1797
|
+
if (!sub) {
|
|
1798
|
+
ctx.send({ type: "info", message: ctx.addedDirs.size > 0 ? `\u{1F4C1} Added directories:
|
|
1776
1799
|
${[...ctx.addedDirs].map((d) => ` \u2022 ${d}`).join("\n")}
|
|
1777
1800
|
|
|
1778
1801
|
Use /add-dir remove to clear.` : "No directories added.\nUsage: /add-dir <path> | /add-dir remove" });
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
ctx.send({ type: "
|
|
1793
|
-
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
if (sub === "remove" || sub === "clear") {
|
|
1805
|
+
ctx.addedDirs.clear();
|
|
1806
|
+
ctx.send({ type: "info", message: "\u2713 All added directories removed from context." });
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
const dirPath = resolve(sub);
|
|
1810
|
+
if (!existsSync2(dirPath)) {
|
|
1811
|
+
ctx.send({ type: "error", message: `Directory not found: ${dirPath}` });
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
if (!statSync(dirPath).isDirectory()) {
|
|
1815
|
+
ctx.send({ type: "error", message: `Not a directory: ${dirPath}` });
|
|
1816
|
+
return;
|
|
1794
1817
|
}
|
|
1818
|
+
ctx.addedDirs.add(dirPath);
|
|
1819
|
+
ctx.send({ type: "info", message: `\u2713 Added directory: ${dirPath}
|
|
1820
|
+
It will be included in AI context for subsequent messages.` });
|
|
1795
1821
|
}
|
|
1796
1822
|
|
|
1797
1823
|
// src/web/commands/security-commands.ts
|
|
1798
1824
|
async function handleConfig(args, ctx) {
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
ctx.send({ type: "info", message: `\u2699\uFE0F Configuration:
|
|
1825
|
+
const sub = args[0];
|
|
1826
|
+
if (sub === "show" || !sub) {
|
|
1827
|
+
ctx.send({ type: "info", message: `\u2699\uFE0F Configuration:
|
|
1803
1828
|
\`\`\`json
|
|
1804
1829
|
${ctx.config.toFormattedJSON()}
|
|
1805
1830
|
\`\`\`` });
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
}
|
|
1817
|
-
} else {
|
|
1818
|
-
ctx.send({ type: "info", message: "Usage: /config [show] | /config get <key> | /config set <key> <value>" });
|
|
1831
|
+
} else if (sub === "get" && args[1]) {
|
|
1832
|
+
const val = ctx.config.getByPath(args[1]);
|
|
1833
|
+
const display = typeof val === "object" ? JSON.stringify(val, null, 2) : String(val ?? "undefined");
|
|
1834
|
+
ctx.send({ type: "info", message: `${args[1]} = ${display}` });
|
|
1835
|
+
} else if (sub === "set" && args[1] && args[2] !== void 0) {
|
|
1836
|
+
try {
|
|
1837
|
+
ctx.config.setByPath(args[1], args.slice(2).join(" "));
|
|
1838
|
+
ctx.send({ type: "info", message: `\u2713 ${args[1]} = ${args.slice(2).join(" ")}` });
|
|
1839
|
+
} catch (err) {
|
|
1840
|
+
ctx.send({ type: "error", message: `Config error: ${err.message}` });
|
|
1819
1841
|
}
|
|
1842
|
+
} else {
|
|
1843
|
+
ctx.send({ type: "info", message: "Usage: /config [show] | /config get <key> | /config set <key> <value>" });
|
|
1820
1844
|
}
|
|
1821
1845
|
}
|
|
1822
|
-
async function handleDoctor(
|
|
1823
|
-
{
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
ctx.send({ type: "info", message: formatDoctorReport(report, false) });
|
|
1831
|
-
}
|
|
1846
|
+
async function handleDoctor(_args, ctx) {
|
|
1847
|
+
const report = buildDoctorReport({
|
|
1848
|
+
config: ctx.config,
|
|
1849
|
+
providers: ctx.providers,
|
|
1850
|
+
cwd: process.cwd(),
|
|
1851
|
+
mcpStatuses: ctx.mcpManager?.getStatus() ?? []
|
|
1852
|
+
});
|
|
1853
|
+
ctx.send({ type: "info", message: formatDoctorReport(report, false) });
|
|
1832
1854
|
}
|
|
1833
|
-
async function handleBug(
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
ctx.send({ type: "info", message: lines.join("\n") });
|
|
1862
|
-
}
|
|
1855
|
+
async function handleBug(_args, ctx) {
|
|
1856
|
+
const session = ctx.sessions.current;
|
|
1857
|
+
const lines = [
|
|
1858
|
+
"\u{1F41B} **Bug Report Template**",
|
|
1859
|
+
"",
|
|
1860
|
+
"**Environment:**",
|
|
1861
|
+
` ai-cli version: ${VERSION}`,
|
|
1862
|
+
` Node.js: ${process.version}`,
|
|
1863
|
+
` OS: ${process.platform} ${process.arch}`,
|
|
1864
|
+
` Provider: ${ctx.currentProvider}`,
|
|
1865
|
+
` Model: ${ctx.currentModel}`,
|
|
1866
|
+
` Session: ${session?.id.slice(0, 8) ?? "none"} (${session?.messages.length ?? 0} messages)`,
|
|
1867
|
+
"",
|
|
1868
|
+
"**Description:**",
|
|
1869
|
+
"(Describe the bug)",
|
|
1870
|
+
"",
|
|
1871
|
+
"**Steps to reproduce:**",
|
|
1872
|
+
"1. ...",
|
|
1873
|
+
"",
|
|
1874
|
+
"**Expected behavior:**",
|
|
1875
|
+
"...",
|
|
1876
|
+
"",
|
|
1877
|
+
"**Actual behavior:**",
|
|
1878
|
+
"...",
|
|
1879
|
+
"",
|
|
1880
|
+
"Report at: https://github.com/jinzhengdong/ai-cli/issues"
|
|
1881
|
+
];
|
|
1882
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
1863
1883
|
}
|
|
1864
1884
|
|
|
1865
1885
|
// src/web/commands/agent-commands.ts
|
|
1866
1886
|
async function handleAuto(args, ctx) {
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
ctx.send({ type: "info", message: `Auto Mode: ${ctx.toolExecutor.sessionAutoMode ? "on" : "off"}
|
|
1887
|
+
const sub = args[0]?.toLowerCase() ?? "status";
|
|
1888
|
+
if (sub === "on") {
|
|
1889
|
+
ctx.toolExecutor.sessionAutoMode = true;
|
|
1890
|
+
ctx.send({ type: "info", message: "\u26A1 Auto Mode ON \u2014 low-risk actions may auto-run; high-risk actions still confirm or deny. Auto Mode is session-scoped." });
|
|
1891
|
+
} else if (sub === "off") {
|
|
1892
|
+
ctx.toolExecutor.sessionAutoMode = false;
|
|
1893
|
+
ctx.send({ type: "info", message: "Auto Mode disabled." });
|
|
1894
|
+
} else if (sub === "status") {
|
|
1895
|
+
ctx.send({ type: "info", message: `Auto Mode: ${ctx.toolExecutor.sessionAutoMode ? "on" : "off"}
|
|
1877
1896
|
Scope: current session only
|
|
1878
1897
|
Denied: /permissions recently-denied` });
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
}
|
|
1882
|
-
ctx.sendStatus();
|
|
1898
|
+
} else {
|
|
1899
|
+
ctx.send({ type: "error", message: "Usage: /auto [on|off|status]" });
|
|
1883
1900
|
}
|
|
1901
|
+
ctx.sendStatus();
|
|
1884
1902
|
}
|
|
1885
1903
|
async function handlePermissions(args, ctx) {
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
ctx.send({ type: "info", message: "No recently denied Auto Mode actions." });
|
|
1892
|
-
} else {
|
|
1893
|
-
const lines = [`Recently denied Auto Mode actions (${rows.length})`, ""];
|
|
1894
|
-
for (const r of rows.slice(0, 20)) {
|
|
1895
|
-
lines.push(`- ${r.timestamp} ${r.tool} ${r.ruleId}`);
|
|
1896
|
-
lines.push(` ${r.reason}`);
|
|
1897
|
-
lines.push(` ${r.argsPreview}`);
|
|
1898
|
-
}
|
|
1899
|
-
ctx.send({ type: "info", message: lines.join("\n") });
|
|
1900
|
-
}
|
|
1901
|
-
} else if (sub === "clear-denied") {
|
|
1902
|
-
clearRecentlyDeniedAutoActions();
|
|
1903
|
-
ctx.send({ type: "info", message: "Cleared recently denied Auto Mode actions." });
|
|
1904
|
+
const sub = args[0]?.toLowerCase() ?? "recently-denied";
|
|
1905
|
+
if (sub === "recently-denied") {
|
|
1906
|
+
const rows = getRecentlyDeniedAutoActions();
|
|
1907
|
+
if (rows.length === 0) {
|
|
1908
|
+
ctx.send({ type: "info", message: "No recently denied Auto Mode actions." });
|
|
1904
1909
|
} else {
|
|
1905
|
-
|
|
1910
|
+
const lines = [`Recently denied Auto Mode actions (${rows.length})`, ""];
|
|
1911
|
+
for (const r of rows.slice(0, 20)) {
|
|
1912
|
+
lines.push(`- ${r.timestamp} ${r.tool} ${r.ruleId}`);
|
|
1913
|
+
lines.push(` ${r.reason}`);
|
|
1914
|
+
lines.push(` ${r.argsPreview}`);
|
|
1915
|
+
}
|
|
1916
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
1906
1917
|
}
|
|
1918
|
+
} else if (sub === "clear-denied") {
|
|
1919
|
+
clearRecentlyDeniedAutoActions();
|
|
1920
|
+
ctx.send({ type: "info", message: "Cleared recently denied Auto Mode actions." });
|
|
1921
|
+
} else {
|
|
1922
|
+
ctx.send({ type: "error", message: "Usage: /permissions recently-denied|clear-denied" });
|
|
1907
1923
|
}
|
|
1908
1924
|
}
|
|
1909
1925
|
async function handleYolo(args, ctx) {
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
ctx.send({ type: "info", message: "\u26A1 YOLO mode ON \u2014 write tools auto-approved; destructive tools still require confirmation.\nUse /yolo off to re-enable write confirmations." });
|
|
1918
|
-
}
|
|
1926
|
+
const sub = args[0]?.toLowerCase();
|
|
1927
|
+
if (sub === "off") {
|
|
1928
|
+
ctx.toolExecutor.sessionAutoApprove = false;
|
|
1929
|
+
ctx.send({ type: "info", message: "\u{1F512} Auto-approve disabled \u2014 confirmations restored for this session." });
|
|
1930
|
+
} else {
|
|
1931
|
+
ctx.toolExecutor.sessionAutoApprove = true;
|
|
1932
|
+
ctx.send({ type: "info", message: "\u26A1 YOLO mode ON \u2014 write tools auto-approved; destructive tools still require confirmation.\nUse /yolo off to re-enable write confirmations." });
|
|
1919
1933
|
}
|
|
1920
1934
|
}
|
|
1921
1935
|
async function handleAgent(args, ctx) {
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1936
|
+
const sub = args[0]?.toLowerCase() ?? "summary";
|
|
1937
|
+
if (sub === "list") {
|
|
1938
|
+
const agents = listAgentConfigs(ctx.config.getConfigDir());
|
|
1939
|
+
const lines = [`Agents (${agents.length})`, ""];
|
|
1940
|
+
for (const agent of agents) {
|
|
1941
|
+
const active = agent.name === getPreferredAgentName() ? " *" : " ";
|
|
1942
|
+
const model = [agent.provider, agent.model].filter(Boolean).join("/") || "inherit";
|
|
1943
|
+
lines.push(`${active} ${agent.name} [${agent.source ?? "builtin"}] \u2014 ${agent.description ?? ""}`);
|
|
1944
|
+
lines.push(` model: ${model} \xB7 permission: ${agent.permissionProfile ?? "workspace-write"} \xB7 maxRounds: ${agent.maxToolRounds ?? "inherit"}`);
|
|
1945
|
+
}
|
|
1946
|
+
lines.push("", "Config dirs: ~/.aicli/agents/ and .aicli/agents/");
|
|
1947
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
if (sub === "switch") {
|
|
1951
|
+
const name = args[1];
|
|
1952
|
+
if (!name) {
|
|
1953
|
+
ctx.send({ type: "error", message: "Usage: /agent switch <name>" });
|
|
1954
|
+
return;
|
|
1935
1955
|
}
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
}
|
|
1941
|
-
const agent = resolveAgentConfig(name, ctx.config.getConfigDir());
|
|
1942
|
-
if (!agent) {
|
|
1943
|
-
const available = listAgentConfigs(ctx.config.getConfigDir()).map((a) => a.name).join(", ");
|
|
1944
|
-
ctx.send({ type: "error", message: `Unknown agent: ${name}
|
|
1956
|
+
const agent = resolveAgentConfig(name, ctx.config.getConfigDir());
|
|
1957
|
+
if (!agent) {
|
|
1958
|
+
const available = listAgentConfigs(ctx.config.getConfigDir()).map((a) => a.name).join(", ");
|
|
1959
|
+
ctx.send({ type: "error", message: `Unknown agent: ${name}
|
|
1945
1960
|
Available: ${available}` });
|
|
1946
|
-
|
|
1947
|
-
setPreferredAgentName(agent.name);
|
|
1948
|
-
ctx.send({ type: "info", message: `Preferred spawn_agent role: ${agent.name}` });
|
|
1949
|
-
ctx.sendStatus();
|
|
1961
|
+
return;
|
|
1950
1962
|
}
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1963
|
+
setPreferredAgentName(agent.name);
|
|
1964
|
+
ctx.send({ type: "info", message: `Preferred spawn_agent role: ${agent.name}` });
|
|
1965
|
+
ctx.sendStatus();
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
if (sub === "stop") {
|
|
1969
|
+
const target = args[1] ?? "all";
|
|
1970
|
+
const count = requestAgentStop(target);
|
|
1971
|
+
ctx.send({ type: "info", message: count > 0 ? `Stop requested for ${count} running agent(s).` : "No matching running agents." });
|
|
1972
|
+
ctx.sendStatus();
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
if (sub === "summary") {
|
|
1976
|
+
const runs = listAgentRuns();
|
|
1977
|
+
if (runs.length === 0) {
|
|
1978
|
+
ctx.send({ type: "info", message: "No sub-agent runs yet." });
|
|
1979
|
+
return;
|
|
1956
1980
|
}
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
}
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
lines.push(`${run.id} ${run.status} ${run.agentName} ${run.startedAt}`);
|
|
1965
|
-
lines.push(` task: ${run.task.slice(0, 120)}${run.task.length > 120 ? "..." : ""}`);
|
|
1966
|
-
if (run.toolNames.length > 0) lines.push(` tools: ${run.toolNames.join(", ")}`);
|
|
1967
|
-
if (run.summary) lines.push(` summary: ${run.summary.slice(0, 180)}${run.summary.length > 180 ? "..." : ""}`);
|
|
1968
|
-
if (run.error) lines.push(` error: ${run.error}`);
|
|
1969
|
-
}
|
|
1970
|
-
ctx.send({ type: "info", message: lines.join("\n") });
|
|
1981
|
+
const lines = [`Recent agent runs (${runs.length})`, ""];
|
|
1982
|
+
for (const run of runs.slice(0, 20)) {
|
|
1983
|
+
lines.push(`${run.id} ${run.status} ${run.agentName} ${run.startedAt}`);
|
|
1984
|
+
lines.push(` task: ${run.task.slice(0, 120)}${run.task.length > 120 ? "..." : ""}`);
|
|
1985
|
+
if (run.toolNames.length > 0) lines.push(` tools: ${run.toolNames.join(", ")}`);
|
|
1986
|
+
if (run.summary) lines.push(` summary: ${run.summary.slice(0, 180)}${run.summary.length > 180 ? "..." : ""}`);
|
|
1987
|
+
if (run.error) lines.push(` error: ${run.error}`);
|
|
1971
1988
|
}
|
|
1972
|
-
ctx.send({ type: "
|
|
1989
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
1990
|
+
return;
|
|
1973
1991
|
}
|
|
1992
|
+
ctx.send({ type: "error", message: "Usage: /agent list|switch <name>|stop <id|all>|summary" });
|
|
1974
1993
|
}
|
|
1975
1994
|
|
|
1976
1995
|
// src/web/commands/misc-commands.ts
|
|
1977
|
-
async function handleHelp(
|
|
1996
|
+
async function handleHelp(_args, ctx) {
|
|
1978
1997
|
ctx.send({
|
|
1979
1998
|
type: "info",
|
|
1980
1999
|
message: [
|
|
@@ -2032,90 +2051,87 @@ async function handleHelp(args, ctx) {
|
|
|
2032
2051
|
].join("\n")
|
|
2033
2052
|
});
|
|
2034
2053
|
}
|
|
2035
|
-
async function handleAbout(
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
ctx.send({ type: "info", message: lines.join("\n") });
|
|
2053
|
-
}
|
|
2054
|
+
async function handleAbout(_args, ctx) {
|
|
2055
|
+
const providerList = ctx.providers.listAll();
|
|
2056
|
+
const toolCount = ctx.toolRegistry.getDefinitions().length;
|
|
2057
|
+
const mcpTools = ctx.mcpManager?.getTotalToolCount() ?? 0;
|
|
2058
|
+
const lines = [
|
|
2059
|
+
`\u{1F916} **ai-cli** v${VERSION}`,
|
|
2060
|
+
"",
|
|
2061
|
+
`${DESCRIPTION}`,
|
|
2062
|
+
`Author: ${AUTHOR} <${AUTHOR_EMAIL}>`,
|
|
2063
|
+
"",
|
|
2064
|
+
`**Providers:** ${providerList.length} (${providerList.map((p) => p.id).join(", ")})`,
|
|
2065
|
+
`**Tools:** ${toolCount + mcpTools} (${toolCount} built-in${mcpTools > 0 ? ` + ${mcpTools} MCP` : ""})`,
|
|
2066
|
+
`**REPL Commands:** 37`,
|
|
2067
|
+
"",
|
|
2068
|
+
`GitHub: https://github.com/jinzhengdong/ai-cli`
|
|
2069
|
+
];
|
|
2070
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
2054
2071
|
}
|
|
2055
2072
|
async function handleSearch(args, ctx) {
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
}
|
|
2075
|
-
lines.push("");
|
|
2073
|
+
const query = args.join(" ").trim();
|
|
2074
|
+
if (!query) {
|
|
2075
|
+
ctx.send({ type: "error", message: "Usage: /search <keyword>" });
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
const results = ctx.sessions.searchMessages(query);
|
|
2079
|
+
if (results.length === 0) {
|
|
2080
|
+
ctx.send({ type: "info", message: `No sessions found containing "${query}".` });
|
|
2081
|
+
return;
|
|
2082
|
+
}
|
|
2083
|
+
const lines = [`\u{1F50D} Found ${results.length} session(s) containing "${query}"`, ""];
|
|
2084
|
+
for (const r of results) {
|
|
2085
|
+
const { sessionMeta, matches } = r;
|
|
2086
|
+
const dateStr = sessionMeta.updated instanceof Date ? sessionMeta.updated.toLocaleDateString() : String(sessionMeta.updated);
|
|
2087
|
+
lines.push(`**${sessionMeta.id.slice(0, 8)}** [${dateStr}] ${sessionMeta.provider} / ${sessionMeta.model}`);
|
|
2088
|
+
if (sessionMeta.title) lines.push(` ${sessionMeta.title}`);
|
|
2089
|
+
for (const m of matches) {
|
|
2090
|
+
const icon = m.role === "user" ? "\u{1F464}" : "\u{1F916}";
|
|
2091
|
+
lines.push(` ${icon} ${m.snippet}`);
|
|
2076
2092
|
}
|
|
2077
|
-
lines.push("
|
|
2078
|
-
ctx.send({ type: "info", message: lines.join("\n") });
|
|
2093
|
+
lines.push("");
|
|
2079
2094
|
}
|
|
2095
|
+
lines.push("Use /session load <id> to resume a session.");
|
|
2096
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
2080
2097
|
}
|
|
2081
2098
|
async function handleUndo(args, ctx) {
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
}
|
|
2089
|
-
const lines = [`\u{1F4CB} Undo Stack (${history.length} entries, newest last):`, ""];
|
|
2090
|
-
history.forEach((entry, i) => {
|
|
2091
|
-
const timeStr = entry.timestamp.toLocaleTimeString();
|
|
2092
|
-
const typeTag = entry.isDirectory ? "[dir]" : entry.previousContent === null ? "[new]" : "[mod]";
|
|
2093
|
-
lines.push(` ${String(i + 1).padStart(3)} ${typeTag} ${entry.description} ${timeStr}`);
|
|
2094
|
-
});
|
|
2095
|
-
ctx.send({ type: "info", message: lines.join("\n") });
|
|
2096
|
-
}
|
|
2097
|
-
const n = sub ? parseInt(sub, 10) : 1;
|
|
2098
|
-
if (isNaN(n) || n < 1) {
|
|
2099
|
-
ctx.send({ type: "info", message: "Usage: /undo [list | <n>]" });
|
|
2100
|
-
}
|
|
2101
|
-
if (undoStack.depth === 0) {
|
|
2102
|
-
ctx.send({ type: "info", message: "Nothing to undo." });
|
|
2103
|
-
}
|
|
2104
|
-
const count = Math.min(n, undoStack.depth);
|
|
2105
|
-
const undoResults = [];
|
|
2106
|
-
for (let i = 0; i < count; i++) {
|
|
2107
|
-
const top = undoStack.peek();
|
|
2108
|
-
if (!top) {
|
|
2109
|
-
}
|
|
2110
|
-
const undoResult = undoStack.undo();
|
|
2111
|
-
if (undoResult) undoResults.push(undoResult.result);
|
|
2099
|
+
const sub = args.join(" ").trim();
|
|
2100
|
+
if (sub === "list") {
|
|
2101
|
+
const history = undoStack.getHistory();
|
|
2102
|
+
if (history.length === 0) {
|
|
2103
|
+
ctx.send({ type: "info", message: "Undo stack is empty." });
|
|
2104
|
+
return;
|
|
2112
2105
|
}
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2106
|
+
const lines = [`\u{1F4CB} Undo Stack (${history.length} entries, newest last):`, ""];
|
|
2107
|
+
history.forEach((entry, i) => {
|
|
2108
|
+
const timeStr = entry.timestamp.toLocaleTimeString();
|
|
2109
|
+
const typeTag = entry.isDirectory ? "[dir]" : entry.previousContent === null ? "[new]" : "[mod]";
|
|
2110
|
+
lines.push(` ${String(i + 1).padStart(3)} ${typeTag} ${entry.description} ${timeStr}`);
|
|
2111
|
+
});
|
|
2112
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
2113
|
+
return;
|
|
2114
|
+
}
|
|
2115
|
+
const n = sub ? parseInt(sub, 10) : 1;
|
|
2116
|
+
if (isNaN(n) || n < 1) {
|
|
2117
|
+
ctx.send({ type: "info", message: "Usage: /undo [list | <n>]" });
|
|
2118
|
+
return;
|
|
2119
|
+
}
|
|
2120
|
+
if (undoStack.depth === 0) {
|
|
2121
|
+
ctx.send({ type: "info", message: "Nothing to undo." });
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
2124
|
+
const count = Math.min(n, undoStack.depth);
|
|
2125
|
+
const undoResults = [];
|
|
2126
|
+
for (let i = 0; i < count; i++) {
|
|
2127
|
+
const undoResult = undoStack.undo();
|
|
2128
|
+
if (undoResult) undoResults.push(undoResult.result);
|
|
2129
|
+
}
|
|
2130
|
+
if (undoResults.length === 0) {
|
|
2131
|
+
ctx.send({ type: "info", message: "Nothing to undo." });
|
|
2132
|
+
} else {
|
|
2133
|
+
ctx.send({ type: "info", message: `\u2713 ${undoResults.length} operation(s) undone:
|
|
2117
2134
|
${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
|
|
2118
|
-
}
|
|
2119
2135
|
}
|
|
2120
2136
|
}
|
|
2121
2137
|
|
|
@@ -2164,20 +2180,6 @@ var webCommandHandlers = {
|
|
|
2164
2180
|
search: handleSearch,
|
|
2165
2181
|
undo: handleUndo
|
|
2166
2182
|
};
|
|
2167
|
-
function lastAssistantText(messages) {
|
|
2168
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2169
|
-
const m = messages[i];
|
|
2170
|
-
if (!m || m.role !== "assistant") continue;
|
|
2171
|
-
const c = m.content;
|
|
2172
|
-
if (typeof c === "string") return c;
|
|
2173
|
-
if (Array.isArray(c)) {
|
|
2174
|
-
const text = c.filter((p) => p && p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
|
|
2175
|
-
if (text) return text;
|
|
2176
|
-
}
|
|
2177
|
-
return void 0;
|
|
2178
|
-
}
|
|
2179
|
-
return void 0;
|
|
2180
|
-
}
|
|
2181
2183
|
var SessionHandler = class {
|
|
2182
2184
|
ws;
|
|
2183
2185
|
config;
|
|
@@ -2220,6 +2222,8 @@ var SessionHandler = class {
|
|
|
2220
2222
|
addedDirs = /* @__PURE__ */ new Set();
|
|
2221
2223
|
/** Track MCP tool names used across this session (for MCP budget prioritization) */
|
|
2222
2224
|
usedMcpToolNames = /* @__PURE__ */ new Set();
|
|
2225
|
+
/** P2-PERF-05: bash session key for cleanup on disconnect */
|
|
2226
|
+
_bashSessionKey = "";
|
|
2223
2227
|
/**
|
|
2224
2228
|
* 未保存会话的内存缓存(per-handler)。
|
|
2225
2229
|
* 当客户端通过 `session new` 创建新会话时,会话仅存在于 SessionManager._current 中,
|
|
@@ -2238,7 +2242,9 @@ var SessionHandler = class {
|
|
|
2238
2242
|
this.mcpManager = shared.mcpManager;
|
|
2239
2243
|
this.skillManager = shared.skillManager;
|
|
2240
2244
|
this.toolExecutor = new ToolExecutorWeb(shared.toolRegistry, ws);
|
|
2241
|
-
|
|
2245
|
+
const bashSessionKey = `web-${Math.random().toString(36).slice(2)}-${Date.now()}`;
|
|
2246
|
+
this.toolExecutor.setSessionKey(bashSessionKey);
|
|
2247
|
+
this._bashSessionKey = bashSessionKey;
|
|
2242
2248
|
this.currentProvider = this.config.get("defaultProvider");
|
|
2243
2249
|
const allDefaultModels = this.config.get("defaultModels");
|
|
2244
2250
|
try {
|
|
@@ -2356,10 +2362,10 @@ var SessionHandler = class {
|
|
|
2356
2362
|
return;
|
|
2357
2363
|
}
|
|
2358
2364
|
case "ask_user_response": {
|
|
2359
|
-
const
|
|
2360
|
-
if (
|
|
2365
|
+
const resolve5 = this.pendingAskUser.get(msg.requestId);
|
|
2366
|
+
if (resolve5) {
|
|
2361
2367
|
this.pendingAskUser.delete(msg.requestId);
|
|
2362
|
-
|
|
2368
|
+
resolve5(msg.answer);
|
|
2363
2369
|
}
|
|
2364
2370
|
return;
|
|
2365
2371
|
}
|
|
@@ -2372,10 +2378,10 @@ var SessionHandler = class {
|
|
|
2372
2378
|
case "memory_manage":
|
|
2373
2379
|
return this.handleMemoryManage(msg.action, msg.id, msg.expiresAt);
|
|
2374
2380
|
case "auto_pause_response": {
|
|
2375
|
-
const
|
|
2376
|
-
if (
|
|
2381
|
+
const resolve5 = this.pendingAutoPause.get(msg.requestId);
|
|
2382
|
+
if (resolve5) {
|
|
2377
2383
|
this.pendingAutoPause.delete(msg.requestId);
|
|
2378
|
-
|
|
2384
|
+
resolve5({ action: msg.action, message: msg.message });
|
|
2379
2385
|
}
|
|
2380
2386
|
return;
|
|
2381
2387
|
}
|
|
@@ -2390,10 +2396,10 @@ var SessionHandler = class {
|
|
|
2390
2396
|
this.hubOrchestrator?.abort();
|
|
2391
2397
|
return;
|
|
2392
2398
|
case "hub_steer": {
|
|
2393
|
-
const
|
|
2394
|
-
if (
|
|
2399
|
+
const resolve5 = this.pendingHubReview.get(msg.requestId);
|
|
2400
|
+
if (resolve5) {
|
|
2395
2401
|
this.pendingHubReview.delete(msg.requestId);
|
|
2396
|
-
|
|
2402
|
+
resolve5({ action: msg.action, message: msg.message });
|
|
2397
2403
|
}
|
|
2398
2404
|
return;
|
|
2399
2405
|
}
|
|
@@ -2406,11 +2412,12 @@ var SessionHandler = class {
|
|
|
2406
2412
|
runLifecycleHooks(mergePluginHooks(this.config.get("hooks") ?? void 0, this.config.getConfigDir()), "Stop", { sessionId: this.sessions.current?.id }, { configDir: this.config.getConfigDir() });
|
|
2407
2413
|
this.toolExecutor.cancelAll();
|
|
2408
2414
|
if (this.abortController) this.abortController.abort();
|
|
2409
|
-
for (const
|
|
2415
|
+
for (const resolve5 of this.pendingAskUser.values()) resolve5(null);
|
|
2410
2416
|
this.pendingAskUser.clear();
|
|
2411
|
-
for (const
|
|
2417
|
+
for (const resolve5 of this.pendingAutoPause.values()) resolve5({ action: "stop" });
|
|
2412
2418
|
this.pendingAutoPause.clear();
|
|
2413
2419
|
this.saveIfNeeded();
|
|
2420
|
+
if (this._bashSessionKey) clearBashSession(this._bashSessionKey);
|
|
2414
2421
|
}
|
|
2415
2422
|
/** 根据当前模型 context window 更新工具输出截断上限 */
|
|
2416
2423
|
updateContextWindow() {
|
|
@@ -2542,9 +2549,9 @@ var SessionHandler = class {
|
|
|
2542
2549
|
this.hubOrchestrator = orchestrator;
|
|
2543
2550
|
orchestrator.onEvent = (event) => this.send({ type: "hub_event", event });
|
|
2544
2551
|
if (config.humanSteer) {
|
|
2545
|
-
orchestrator.onRoundReview = ({ round, maxRounds }) => new Promise((
|
|
2552
|
+
orchestrator.onRoundReview = ({ round, maxRounds }) => new Promise((resolve5) => {
|
|
2546
2553
|
const requestId = `hubrev_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
2547
|
-
this.pendingHubReview.set(requestId,
|
|
2554
|
+
this.pendingHubReview.set(requestId, resolve5);
|
|
2548
2555
|
this.send({ type: "hub_review", requestId, round, maxRounds });
|
|
2549
2556
|
});
|
|
2550
2557
|
}
|
|
@@ -2877,7 +2884,7 @@ Try: /compact to reduce context, /clear to reset, or switch to a larger-context
|
|
|
2877
2884
|
this.send({ type: "response_done", content, usage });
|
|
2878
2885
|
if (pendingTeeSave && isCleanDocumentBody(content)) {
|
|
2879
2886
|
try {
|
|
2880
|
-
|
|
2887
|
+
mkdirSync3(dirname3(pendingTeeSave), { recursive: true });
|
|
2881
2888
|
const bodyToSave = stripOuterCodeFence(content);
|
|
2882
2889
|
atomicWriteFileSync(pendingTeeSave, bodyToSave);
|
|
2883
2890
|
undoStack.push(pendingTeeSave, `save_last_response (deferred): ${pendingTeeSave}`);
|
|
@@ -2902,8 +2909,8 @@ Try: /compact to reduce context, /clear to reset, or switch to a larger-context
|
|
|
2902
2909
|
onMcpToolUsed: (name) => this.usedMcpToolNames.add(name),
|
|
2903
2910
|
requestAutoPause: async ({ effectiveRound, maxToolRounds: totalRounds, toolSummary }) => {
|
|
2904
2911
|
const requestId = `pause_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
2905
|
-
const pauseResp = await new Promise((
|
|
2906
|
-
this.pendingAutoPause.set(requestId,
|
|
2912
|
+
const pauseResp = await new Promise((resolve5) => {
|
|
2913
|
+
this.pendingAutoPause.set(requestId, resolve5);
|
|
2907
2914
|
this.send({
|
|
2908
2915
|
type: "auto_pause_request",
|
|
2909
2916
|
requestId,
|
|
@@ -3053,8 +3060,8 @@ ${summaryContent}`,
|
|
|
3053
3060
|
let isError = false;
|
|
3054
3061
|
let summary;
|
|
3055
3062
|
try {
|
|
3056
|
-
|
|
3057
|
-
fileStream =
|
|
3063
|
+
mkdirSync3(dirname3(saveToFile), { recursive: true });
|
|
3064
|
+
fileStream = createWriteStream3(saveToFile);
|
|
3058
3065
|
const teeSystemPrompt = stripToolCallReminder(systemPrompt ?? "") + CONTENT_ONLY_STREAM_REMINDER;
|
|
3059
3066
|
const teeExtraMessages = extraMessages.length > 0 ? [...extraMessages, { role: "user", content: TEE_FINAL_USER_NUDGE }] : [{ role: "user", content: TEE_FINAL_USER_NUDGE }];
|
|
3060
3067
|
const chatRequest = {
|
|
@@ -3082,8 +3089,8 @@ ${summaryContent}`,
|
|
|
3082
3089
|
}
|
|
3083
3090
|
if (chunk.done) break;
|
|
3084
3091
|
}
|
|
3085
|
-
await new Promise((
|
|
3086
|
-
fileStream.end((err) => err ? reject(err) :
|
|
3092
|
+
await new Promise((resolve5, reject) => {
|
|
3093
|
+
fileStream.end((err) => err ? reject(err) : resolve5());
|
|
3087
3094
|
});
|
|
3088
3095
|
const verdict = evaluateTeeContent(fullContent, saveToFile, priorContent);
|
|
3089
3096
|
if (verdict.kind === "reject") {
|
|
@@ -3109,7 +3116,7 @@ ${summaryContent}`,
|
|
|
3109
3116
|
} catch (err) {
|
|
3110
3117
|
if (fileStream) {
|
|
3111
3118
|
try {
|
|
3112
|
-
await new Promise((
|
|
3119
|
+
await new Promise((resolve5) => fileStream.end(() => resolve5()));
|
|
3113
3120
|
} catch {
|
|
3114
3121
|
}
|
|
3115
3122
|
}
|
|
@@ -3346,7 +3353,7 @@ ${entry.content}`;
|
|
|
3346
3353
|
const configDir = this.config.getConfigDir();
|
|
3347
3354
|
try {
|
|
3348
3355
|
atomicWriteFileSync(memoryStorePath(configDir), "");
|
|
3349
|
-
atomicWriteFileSync(
|
|
3356
|
+
atomicWriteFileSync(join3(configDir, MEMORY_FILE_NAME), "");
|
|
3350
3357
|
this.send({ type: "info", message: "\u{1F5D1}\uFE0F Persistent memory cleared." });
|
|
3351
3358
|
this.sendMemoryEntries();
|
|
3352
3359
|
} catch (err) {
|
|
@@ -3412,7 +3419,7 @@ ${entry.content}`;
|
|
|
3412
3419
|
return;
|
|
3413
3420
|
}
|
|
3414
3421
|
try {
|
|
3415
|
-
const { searchChatMemory, loadChatIndex } = await import("./chat-index-
|
|
3422
|
+
const { searchChatMemory, loadChatIndex } = await import("./chat-index-O56HDGFI.js");
|
|
3416
3423
|
const loaded = loadChatIndex();
|
|
3417
3424
|
if (!loaded || loaded.idx.chunks.length === 0) {
|
|
3418
3425
|
this.send({ type: "memory_hits", query: q, hits: [], indexMissing: true });
|
|
@@ -3448,7 +3455,7 @@ ${entry.content}`;
|
|
|
3448
3455
|
}
|
|
3449
3456
|
async handleMemoryStatus() {
|
|
3450
3457
|
try {
|
|
3451
|
-
const { getChatIndexStatus } = await import("./chat-index-
|
|
3458
|
+
const { getChatIndexStatus } = await import("./chat-index-O56HDGFI.js");
|
|
3452
3459
|
const s = getChatIndexStatus();
|
|
3453
3460
|
this.send({
|
|
3454
3461
|
type: "memory_status",
|
|
@@ -3473,7 +3480,7 @@ ${entry.content}`;
|
|
|
3473
3480
|
type: "info",
|
|
3474
3481
|
message: full ? "\u{1F9E0} Rebuilding chat memory index (this may take a while on first run \u2014 ~117 MB embedder)." : "\u{1F9E0} Refreshing chat memory index (incremental)\u2026"
|
|
3475
3482
|
});
|
|
3476
|
-
const { buildChatIndex } = await import("./chat-index-
|
|
3483
|
+
const { buildChatIndex } = await import("./chat-index-O56HDGFI.js");
|
|
3477
3484
|
const stats = await buildChatIndex({
|
|
3478
3485
|
full,
|
|
3479
3486
|
onProgress: (p) => {
|
|
@@ -3622,11 +3629,11 @@ function getModuleDir() {
|
|
|
3622
3629
|
if (typeof import.meta?.url === "string") {
|
|
3623
3630
|
const url = new URL(import.meta.url);
|
|
3624
3631
|
const filePath = url.pathname.replace(/^\/([A-Z]:)/i, "$1");
|
|
3625
|
-
return
|
|
3632
|
+
return dirname4(filePath);
|
|
3626
3633
|
}
|
|
3627
3634
|
} catch {
|
|
3628
3635
|
}
|
|
3629
|
-
return
|
|
3636
|
+
return resolve4(".");
|
|
3630
3637
|
}
|
|
3631
3638
|
async function startWebServer(options = {}) {
|
|
3632
3639
|
const port = options.port ?? 3e3;
|
|
@@ -3694,8 +3701,8 @@ async function startWebServer(options = {}) {
|
|
|
3694
3701
|
}
|
|
3695
3702
|
}
|
|
3696
3703
|
let skillManager = null;
|
|
3697
|
-
const skillsDir =
|
|
3698
|
-
if (
|
|
3704
|
+
const skillsDir = join4(config.getConfigDir(), SKILLS_DIR_NAME);
|
|
3705
|
+
if (existsSync5(skillsDir)) {
|
|
3699
3706
|
skillManager = new SkillManager(skillsDir, config.get("ui").skillSizeWarn);
|
|
3700
3707
|
skillManager.loadSkills(pluginAssets.skillDirs);
|
|
3701
3708
|
const count = skillManager.listSkills().length;
|
|
@@ -3766,18 +3773,18 @@ async function startWebServer(options = {}) {
|
|
|
3766
3773
|
next();
|
|
3767
3774
|
};
|
|
3768
3775
|
const moduleDir = getModuleDir();
|
|
3769
|
-
let clientDir =
|
|
3770
|
-
if (!
|
|
3771
|
-
clientDir =
|
|
3776
|
+
let clientDir = join4(moduleDir, "web", "client");
|
|
3777
|
+
if (!existsSync5(clientDir)) {
|
|
3778
|
+
clientDir = join4(moduleDir, "client");
|
|
3772
3779
|
}
|
|
3773
|
-
if (!
|
|
3774
|
-
clientDir =
|
|
3780
|
+
if (!existsSync5(clientDir)) {
|
|
3781
|
+
clientDir = join4(moduleDir, "..", "..", "src", "web", "client");
|
|
3775
3782
|
}
|
|
3776
|
-
if (!
|
|
3777
|
-
clientDir =
|
|
3783
|
+
if (!existsSync5(clientDir)) {
|
|
3784
|
+
clientDir = join4(process.cwd(), "src", "web", "client");
|
|
3778
3785
|
}
|
|
3779
3786
|
console.log(` Static files: ${clientDir}`);
|
|
3780
|
-
app.use("/vendor", express.static(
|
|
3787
|
+
app.use("/vendor", express.static(join4(clientDir, "vendor"), { maxAge: "1y", immutable: true }));
|
|
3781
3788
|
app.use(express.static(clientDir));
|
|
3782
3789
|
app.get("/api/status", (_req, res) => {
|
|
3783
3790
|
res.json({
|
|
@@ -3844,10 +3851,10 @@ async function startWebServer(options = {}) {
|
|
|
3844
3851
|
app.get("/api/files", requireAuth, (req, res) => {
|
|
3845
3852
|
const cwd = process.cwd();
|
|
3846
3853
|
const prefix = req.query.prefix || "";
|
|
3847
|
-
const targetDir =
|
|
3854
|
+
const targetDir = join4(cwd, prefix);
|
|
3848
3855
|
try {
|
|
3849
|
-
const canonicalTarget = realpathSync(
|
|
3850
|
-
const canonicalCwd = realpathSync(
|
|
3856
|
+
const canonicalTarget = realpathSync(resolve4(targetDir));
|
|
3857
|
+
const canonicalCwd = realpathSync(resolve4(cwd));
|
|
3851
3858
|
if (!canonicalTarget.startsWith(canonicalCwd + sep) && canonicalTarget !== canonicalCwd) {
|
|
3852
3859
|
res.json({ files: [] });
|
|
3853
3860
|
return;
|
|
@@ -3858,10 +3865,10 @@ async function startWebServer(options = {}) {
|
|
|
3858
3865
|
}
|
|
3859
3866
|
try {
|
|
3860
3867
|
const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "dist-cjs", "release", "__pycache__", ".next", ".nuxt", "coverage", ".cache"]);
|
|
3861
|
-
const entries =
|
|
3868
|
+
const entries = readdirSync4(targetDir, { withFileTypes: true });
|
|
3862
3869
|
const files = entries.filter((e) => !SKIP.has(e.name) && !e.name.startsWith(".")).slice(0, 50).map((e) => ({
|
|
3863
3870
|
name: e.name,
|
|
3864
|
-
path: relative(cwd,
|
|
3871
|
+
path: relative(cwd, join4(targetDir, e.name)).replace(/\\/g, "/"),
|
|
3865
3872
|
isDir: e.isDirectory()
|
|
3866
3873
|
}));
|
|
3867
3874
|
res.json({ files });
|
|
@@ -3901,8 +3908,8 @@ async function startWebServer(options = {}) {
|
|
|
3901
3908
|
try {
|
|
3902
3909
|
const authUser = req._authUser;
|
|
3903
3910
|
const histDir = authUser ? getUserShared(authUser).config.getHistoryDir() : config.getHistoryDir();
|
|
3904
|
-
const filePath =
|
|
3905
|
-
if (!
|
|
3911
|
+
const filePath = join4(histDir, `${id}.json`);
|
|
3912
|
+
if (!existsSync5(filePath)) {
|
|
3906
3913
|
res.status(404).json({ error: "Session not found" });
|
|
3907
3914
|
return;
|
|
3908
3915
|
}
|
|
@@ -3917,7 +3924,7 @@ async function startWebServer(options = {}) {
|
|
|
3917
3924
|
res.status(404).json({ error: "Session not found" });
|
|
3918
3925
|
return;
|
|
3919
3926
|
}
|
|
3920
|
-
const data = JSON.parse(
|
|
3927
|
+
const data = JSON.parse(readFileSync5(filePath, "utf-8"));
|
|
3921
3928
|
res.json({ session: data });
|
|
3922
3929
|
} catch (err) {
|
|
3923
3930
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
@@ -3930,10 +3937,10 @@ async function startWebServer(options = {}) {
|
|
|
3930
3937
|
return;
|
|
3931
3938
|
}
|
|
3932
3939
|
const cwd = process.cwd();
|
|
3933
|
-
const fullPath =
|
|
3940
|
+
const fullPath = resolve4(join4(cwd, filePath));
|
|
3934
3941
|
try {
|
|
3935
3942
|
const canonicalFull = realpathSync(fullPath);
|
|
3936
|
-
const canonicalCwd = realpathSync(
|
|
3943
|
+
const canonicalCwd = realpathSync(resolve4(cwd));
|
|
3937
3944
|
if (!canonicalFull.startsWith(canonicalCwd + sep) && canonicalFull !== canonicalCwd) {
|
|
3938
3945
|
res.json({ error: "Access denied" });
|
|
3939
3946
|
return;
|
|
@@ -3943,12 +3950,12 @@ async function startWebServer(options = {}) {
|
|
|
3943
3950
|
return;
|
|
3944
3951
|
}
|
|
3945
3952
|
try {
|
|
3946
|
-
const stat =
|
|
3953
|
+
const stat = statSync4(fullPath);
|
|
3947
3954
|
if (stat.size > 512 * 1024) {
|
|
3948
3955
|
res.json({ error: `File too large (${(stat.size / 1024).toFixed(0)} KB, max 512 KB)` });
|
|
3949
3956
|
return;
|
|
3950
3957
|
}
|
|
3951
|
-
const content =
|
|
3958
|
+
const content = readFileSync5(fullPath, "utf-8");
|
|
3952
3959
|
res.json({ content, size: stat.size });
|
|
3953
3960
|
} catch {
|
|
3954
3961
|
res.json({ error: "Cannot read file" });
|
|
@@ -4152,7 +4159,7 @@ async function startWebServer(options = {}) {
|
|
|
4152
4159
|
});
|
|
4153
4160
|
const MAX_PORT_ATTEMPTS = 10;
|
|
4154
4161
|
let actualPort = port;
|
|
4155
|
-
const result = await new Promise((
|
|
4162
|
+
const result = await new Promise((resolve5, reject) => {
|
|
4156
4163
|
const tryListen = (attempt) => {
|
|
4157
4164
|
server.once("error", (err) => {
|
|
4158
4165
|
if (err.code === "EADDRINUSE" && attempt < MAX_PORT_ATTEMPTS) {
|
|
@@ -4183,7 +4190,7 @@ async function startWebServer(options = {}) {
|
|
|
4183
4190
|
}
|
|
4184
4191
|
console.log(` Press Ctrl+C to stop
|
|
4185
4192
|
`);
|
|
4186
|
-
|
|
4193
|
+
resolve5({ port: actualPort, host, url });
|
|
4187
4194
|
});
|
|
4188
4195
|
};
|
|
4189
4196
|
tryListen(1);
|
|
@@ -4203,17 +4210,17 @@ function resolveProjectMcpPath() {
|
|
|
4203
4210
|
const cwd = process.cwd();
|
|
4204
4211
|
const gitRoot = getGitRoot(cwd);
|
|
4205
4212
|
const projectRoot = gitRoot ?? cwd;
|
|
4206
|
-
const configPath =
|
|
4207
|
-
return
|
|
4213
|
+
const configPath = join4(projectRoot, MCP_PROJECT_CONFIG_NAME);
|
|
4214
|
+
return existsSync5(configPath) ? configPath : null;
|
|
4208
4215
|
}
|
|
4209
4216
|
function loadProjectMcpConfig() {
|
|
4210
4217
|
const cwd = process.cwd();
|
|
4211
4218
|
const gitRoot = getGitRoot(cwd);
|
|
4212
4219
|
const projectRoot = gitRoot ?? cwd;
|
|
4213
|
-
const configPath =
|
|
4214
|
-
if (!
|
|
4220
|
+
const configPath = join4(projectRoot, MCP_PROJECT_CONFIG_NAME);
|
|
4221
|
+
if (!existsSync5(configPath)) return null;
|
|
4215
4222
|
try {
|
|
4216
|
-
const raw = JSON.parse(
|
|
4223
|
+
const raw = JSON.parse(readFileSync5(configPath, "utf-8"));
|
|
4217
4224
|
return raw.mcpServers ?? raw;
|
|
4218
4225
|
} catch {
|
|
4219
4226
|
return null;
|