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.
Files changed (40) hide show
  1. package/dist/{batch-QOOO7HRW.js → batch-EQER63CQ.js} +2 -2
  2. package/dist/{chat-index-FDSGKWQV.js → chat-index-O56HDGFI.js} +2 -2
  3. package/dist/{chat-index-PS274XM7.js → chat-index-WSI7ULRQ.js} +1 -1
  4. package/dist/{chunk-OJL3PY36.js → chunk-2X2DOHA6.js} +3 -2
  5. package/dist/{chunk-VWTUYDZG.js → chunk-34RV4ZKA.js} +1 -1
  6. package/dist/{chunk-UWUTLIOH.js → chunk-35OPVVEK.js} +1 -1
  7. package/dist/{chunk-NZ4X6GUC.js → chunk-3ARPAVNV.js} +2 -2
  8. package/dist/{chunk-FBK7NSIK.js → chunk-4CZZJQXJ.js} +2 -2
  9. package/dist/{chunk-5EE4GRGG.js → chunk-4RQPCOEE.js} +281 -41
  10. package/dist/{chunk-VBRCWH55.js → chunk-6FEZAWH5.js} +3 -3
  11. package/dist/{chunk-SISFAT6W.js → chunk-A2AVDSXJ.js} +1 -1
  12. package/dist/{chunk-SLSWPBK3.js → chunk-B5TYJO7V.js} +4 -1
  13. package/dist/{chunk-QT5FA2ZY.js → chunk-DEHVRQWW.js} +5 -5
  14. package/dist/{chunk-AV5OPK7Q.js → chunk-HLXQXV23.js} +1 -1
  15. package/dist/{chunk-TNW22OUY.js → chunk-JDMFVRFG.js} +3 -3
  16. package/dist/{chunk-S6L5R6SS.js → chunk-JU7OI4BN.js} +1 -1
  17. package/dist/{chunk-VHY6NVMQ.js → chunk-LF3OUJHK.js} +4 -1
  18. package/dist/{chunk-TB4W4Y4T.js → chunk-NVUCDUXE.js} +1 -1
  19. package/dist/{chunk-NNWWMGYK.js → chunk-QJUAQKH2.js} +1 -1
  20. package/dist/{chunk-5GTWZ4QF.js → chunk-QYQCCIBT.js} +1 -1
  21. package/dist/{chunk-Q3ZUDA6S.js → chunk-VT6NRDD2.js} +1 -1
  22. package/dist/{ci-IB7NWHZN.js → ci-UAQFOAMS.js} +4 -4
  23. package/dist/{ci-format-YLGZN3D4.js → ci-format-MNUHYUTO.js} +2 -2
  24. package/dist/{constants-BOMLZEXC.js → constants-PGDTYD2R.js} +1 -1
  25. package/dist/{doctor-cli-2ND4LONW.js → doctor-cli-F6A3GOVY.js} +4 -4
  26. package/dist/electron-server.js +2017 -1786
  27. package/dist/{hub-6WADFJNC.js → hub-EDTZKS7E.js} +6 -6
  28. package/dist/{hub-server-LJ2JSKZ2.js → hub-server-XX36DX7O.js} +2 -2
  29. package/dist/index.js +26 -22
  30. package/dist/{persist-GTBEKVFL.js → persist-XUXIPYUU.js} +3 -3
  31. package/dist/{pr-F7WXUH27.js → pr-YC7DMVC5.js} +4 -4
  32. package/dist/{run-tests-5NEMM6EF.js → run-tests-AUEKHJCC.js} +2 -2
  33. package/dist/{run-tests-NIY24YDP.js → run-tests-S36TE2W2.js} +1 -1
  34. package/dist/{server-ACFX2J66.js → server-347YIBHS.js} +6 -6
  35. package/dist/{server-QGZYDDZW.js → server-LQFHSFN4.js} +1278 -1271
  36. package/dist/{task-orchestrator-4GOBFXPN.js → task-orchestrator-FIR5MCND.js} +17 -9
  37. package/dist/{usage-JXT2YVA6.js → usage-3MXPFFBU.js} +2 -2
  38. package/dist/web/client/app.js +3 -2
  39. package/dist/web/client/util.js +42 -0
  40. package/package.json +1 -1
@@ -3,11 +3,11 @@ import {
3
3
  DiscussionOrchestrator,
4
4
  getPreset,
5
5
  resolveRoleProviders
6
- } from "./chunk-Q3ZUDA6S.js";
7
- import "./chunk-NZ4X6GUC.js";
6
+ } from "./chunk-VT6NRDD2.js";
7
+ import "./chunk-3ARPAVNV.js";
8
8
  import {
9
9
  persistDiscussion
10
- } from "./chunk-QT5FA2ZY.js";
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-FBK7NSIK.js";
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-5EE4GRGG.js";
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-5GTWZ4QF.js";
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-VBRCWH55.js";
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-SISFAT6W.js";
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-UWUTLIOH.js";
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-S6L5R6SS.js";
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-NNWWMGYK.js";
149
- import "./chunk-TB4W4Y4T.js";
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-SLSWPBK3.js";
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 join5, dirname as dirname5, resolve as resolve5, relative, sep } from "path";
163
- import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync5, statSync as statSync5, realpathSync } from "fs";
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 resolve6 = this.pendingConfirms.get(requestId);
240
- if (resolve6) {
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
- resolve6(approved);
245
+ resolve5(approved);
245
246
  }
246
247
  }
247
248
  /** Resolve a pending batch confirm from client response */
248
249
  resolveBatchConfirm(requestId, decision) {
249
- const resolve6 = this.pendingBatchConfirms.get(requestId);
250
- if (resolve6) {
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
- resolve6(decision);
256
+ resolve5(decision);
256
257
  } else {
257
- resolve6(new Set(decision));
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 resolve6 of this.pendingConfirms.values()) resolve6(false);
264
+ for (const resolve5 of this.pendingConfirms.values()) resolve5(false);
264
265
  this.pendingConfirms.clear();
265
- for (const resolve6 of this.pendingBatchConfirms.values()) resolve6("none");
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((resolve6) => {
342
- this.pendingConfirms.set(requestId, resolve6);
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((resolve6) => {
367
- this.pendingBatchConfirms.set(requestId, resolve6);
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 mkdirSync4, createWriteStream as createWriteStream4 } from "fs";
671
- import { join as join4, dirname as dirname4 } from "path";
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
- const id = args[0];
677
- if (!id) {
678
- ctx.send({ type: "error", message: "Usage: /provider <id>" });
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
- const sub = args[0];
696
- if (!sub) {
697
- ctx.send({ type: "error", message: "Usage: /model <id>|refresh [provider]|cache [clear [provider]]" });
698
- return;
699
- }
700
- if (sub === "refresh") {
701
- const providerId = args[1] ?? ctx.currentProvider;
702
- try {
703
- const models2 = await ctx.providers.refreshModels(providerId);
704
- ctx.send({ type: "info", message: `Refreshed ${models2.length} model(s) for ${providerId}.` });
705
- ctx.sendStatus();
706
- } catch (err) {
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
- const modelId = sub;
726
- const models = await ctx.providers.listModels(ctx.currentProvider);
727
- const found = models.find((m) => m.id === modelId);
728
- if (!found) {
729
- ctx.send({ type: "error", message: `Model "${modelId}" not found` });
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
- ctx.currentModel = modelId;
733
- ctx.updateContextWindow();
734
- ctx.send({ type: "info", message: `Switched to model: ${modelId}` });
735
- ctx.sendStatus();
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
- const sub = args[0];
741
- if (sub === "on") ctx.runtimeThinking = true;
742
- else if (sub === "off") ctx.runtimeThinking = false;
743
- else ctx.runtimeThinking = !ctx.runtimeThinking;
744
- ctx.send({ type: "info", message: `Thinking mode: ${ctx.runtimeThinking ? "ON" : "OFF"}` });
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
- const sub = args[0];
751
- if (sub === "exit" || sub === "cancel") {
752
- ctx.planMode = false;
753
- ctx.send({ type: "info", message: "Plan mode OFF." });
754
- } else if (sub === "execute") {
755
- ctx.planMode = false;
756
- ctx.send({ type: "info", message: "Plan mode OFF. Executing with all tools enabled." });
757
- } else {
758
- ctx.planMode = !ctx.planMode;
759
- ctx.send({ type: "info", message: `Plan mode: ${ctx.planMode ? "ON (read-only tools only)" : "OFF"}` });
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
- ctx.sendStatus();
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(args, ctx) {
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(args, ctx) {
778
- {
779
- const session = ctx.sessions.current;
780
- const cacheRead = ctx.sessionTokenUsage.cacheReadTokens;
781
- const cacheCreate = ctx.sessionTokenUsage.cacheCreationTokens;
782
- const cost = computeCost(ctx.currentProvider, ctx.currentModel, ctx.sessionTokenUsage);
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
- const costLine = cost != null ? `
795
+ const costLine = cost != null ? `
786
796
  Cost: ${formatCost(cost)}` : "";
787
- ctx.send({
788
- type: "info",
789
- message: `Provider: ${ctx.currentProvider}
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
- const sub = args[0];
799
- if (sub === "new") {
800
- ctx.saveIfNeeded();
801
- const created = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
802
- ctx.unsavedSessions.set(created.id, created);
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({ type: "info", message: "New session created." });
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
- } else if (sub === "load" && args[1]) {
808
- const targetId = args[1];
809
- ctx.saveIfNeeded();
810
- const cachedExact = ctx.unsavedSessions.get(targetId);
811
- const cached = cachedExact ?? [...ctx.unsavedSessions.values()].find((s) => s.id.startsWith(targetId));
812
- if (cached) {
813
- ctx.sessions.setCurrent(cached);
814
- ctx.resetWebSessionUsage();
815
- ctx.send({
816
- type: "info",
817
- message: `Loaded session: ${cached.id.slice(0, 8)} "${cached.title ?? ""}" (${cached.messages.length} messages)`
818
- });
819
- ctx.sendSessionMessages();
820
- ctx.sendStatus();
821
- ctx.sendSessionList();
822
- }
823
- const list = ctx.sessions.listSessions();
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
- } else if (sub === "list") {
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
- } else if (sub === "delete" && args[1]) {
857
- const targetId = args[1];
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
- if (ctx.sessions.current?.id === cachedKey) {
862
- ctx.sessions.clearCurrent();
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
- ctx.send({ type: "info", message: `Deleted session: ${found.id.slice(0, 8)}` });
872
- ctx.sendSessionList();
873
- } else {
874
- ctx.send({ type: "error", message: `Session not found: ${targetId}` });
912
+ deleted++;
875
913
  }
876
- } else if (sub === "batch-delete") {
877
- const ids = args.slice(1).filter(Boolean);
878
- if (ids.length === 0) {
879
- ctx.send({ type: "error", message: "No session IDs provided." });
880
- }
881
- const list = ctx.sessions.listSessions();
882
- let deleted = 0;
883
- for (const targetId of ids) {
884
- const cachedKey = ctx.unsavedSessions.has(targetId) ? targetId : [...ctx.unsavedSessions.keys()].find((k) => k.startsWith(targetId));
885
- if (cachedKey) {
886
- ctx.unsavedSessions.delete(cachedKey);
887
- deleted++;
888
- continue;
889
- }
890
- const found = list.find((s) => s.id.startsWith(targetId));
891
- if (found) {
892
- ctx.sessions.deleteSession(found.id);
893
- deleted++;
894
- }
895
- }
896
- ctx.send({ type: "info", message: `Deleted ${deleted} session(s).` });
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: "info", message: "Usage: /session new | list | load <id> | delete <id> | rename <id> <title>" });
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
- const format = args[0] === "json" ? "json" : "md";
930
- ctx.exportConversation(format);
931
- }
950
+ const format = args[0] === "json" ? "json" : "md";
951
+ ctx.exportConversation(format);
932
952
  }
933
- async function handleCost(args, ctx) {
934
- {
935
- const u = ctx.sessionTokenUsage;
936
- const total = u.inputTokens + u.outputTokens + u.cacheCreationTokens + u.cacheReadTokens;
937
- const cost = computeCost(ctx.currentProvider, ctx.currentModel, u);
938
- let savings = null;
939
- if (cost != null && u.cacheReadTokens > 0) {
940
- const withoutCache = computeCost(ctx.currentProvider, ctx.currentModel, {
941
- inputTokens: u.inputTokens + u.cacheReadTokens,
942
- outputTokens: u.outputTokens,
943
- cacheCreationTokens: u.cacheCreationTokens,
944
- cacheReadTokens: 0
945
- });
946
- if (withoutCache != null) savings = withoutCache - cost;
947
- }
948
- const lines = [
949
- "\u{1F4B0} Session Cost & Token Usage",
950
- ` Provider : ${ctx.currentProvider}`,
951
- ` Model : ${ctx.currentModel}`,
952
- ` Input (uncached): ${u.inputTokens.toLocaleString()}`,
953
- ` Output : ${u.outputTokens.toLocaleString()}`
954
- ];
955
- if (u.cacheCreationTokens > 0) lines.push(` Cache write : ${u.cacheCreationTokens.toLocaleString()}`);
956
- if (u.cacheReadTokens > 0) {
957
- const pct = Math.round(u.cacheReadTokens / (u.cacheReadTokens + u.inputTokens) * 100);
958
- lines.push(` Cache read : ${u.cacheReadTokens.toLocaleString()} (${pct}% hit rate)`);
959
- }
960
- lines.push(` Total tokens : ${total.toLocaleString()}`);
961
- if (cost != null) {
962
- lines.push(` Cost : ${formatCost(cost)}`);
963
- if (savings != null && savings > 0) {
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
- ctx.send({ type: "info", message: lines.join("\n") });
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
- const session = ctx.sessions.current;
975
- if (!session) {
976
- ctx.send({ type: "error", message: "No active session." });
977
- }
978
- const sub = args[0]?.toLowerCase();
979
- if (!sub || sub === "list") {
980
- const cps = session.checkpoints;
981
- if (cps.length === 0) {
982
- ctx.send({ type: "info", message: "No checkpoints. Use /checkpoint save <name> to create one." });
983
- } else {
984
- const lines = [`\u{1F4CC} Checkpoints (${cps.length}):`, ""];
985
- for (const cp of cps) {
986
- const ts = cp.timestamp.toLocaleString();
987
- lines.push(` ${cp.name.padEnd(20)} msg #${cp.messageIndex} ${ts}`);
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
- const cpName = args.slice(1).join(" ").trim();
993
- if (sub === "save") {
994
- if (!cpName) {
995
- ctx.send({ type: "error", message: "Usage: /checkpoint save <name>" });
996
- }
997
- session.createCheckpoint(cpName);
998
- ctx.send({ type: "info", message: `\u2713 Checkpoint "${cpName}" saved at message #${session.messages.length}` });
999
- } else if (sub === "restore") {
1000
- if (!cpName) {
1001
- ctx.send({ type: "error", message: "Usage: /checkpoint restore <name>" });
1002
- }
1003
- const ok = session.restoreCheckpoint(cpName);
1004
- if (ok) {
1005
- ctx.send({ type: "info", message: `\u2713 Restored to checkpoint "${cpName}" (${session.messages.length} messages)` });
1006
- ctx.sendSessionMessages();
1007
- ctx.sendStatus();
1008
- } else {
1009
- ctx.send({ type: "error", message: `Checkpoint "${cpName}" not found.` });
1010
- }
1011
- } else if (sub === "delete") {
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: `Unknown subcommand: ${sub}. Use save/restore/list/delete.` });
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
- const session = ctx.sessions.current;
1029
- if (!session) {
1030
- ctx.send({ type: "error", message: "No active session." });
1031
- }
1032
- const sub = args[0]?.toLowerCase();
1033
- const resolve6 = (ref) => {
1034
- const r = session.resolveBranchRef(ref);
1035
- if (r.ok) return r.id;
1036
- if (r.reason === "ambiguous") {
1037
- const list = r.matches.map((m) => `${m.id} (${m.title})`).join(", ");
1038
- ctx.send({ type: "error", message: `Ambiguous branch reference "${ref}". Matches: ${list}. Use the 6-char id.` });
1039
- } else {
1040
- ctx.send({ type: "error", message: `Branch "${ref}" not found. Use /branch list to see all branches.` });
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
- if (sub === "new") {
1058
- const idxArg = args[1];
1059
- if (!idxArg) {
1060
- ctx.send({ type: "error", message: "Usage: /branch new <msgIndex> [title]" });
1061
- }
1062
- const fromIndex = parseInt(idxArg, 10);
1063
- if (isNaN(fromIndex) || fromIndex < 0 || fromIndex > session.messages.length) {
1064
- ctx.send({ type: "error", message: `Invalid msgIndex: ${idxArg}. Range: 0-${session.messages.length}` });
1065
- }
1066
- const title = args.slice(2).join(" ").trim() || void 0;
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
- if (sub === "switch") {
1078
- const ref = args[1];
1079
- if (!ref) {
1080
- ctx.send({ type: "error", message: "Usage: /branch switch <id|title>" });
1081
- }
1082
- const id = resolve6(ref);
1083
- if (!id) {
1084
- }
1085
- const ok = session.switchBranch(id);
1086
- if (ok) {
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
- if (sub === "delete") {
1097
- const ref = args[1];
1098
- if (!ref) {
1099
- ctx.send({ type: "error", message: "Usage: /branch delete <id|title>" });
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
- if (sub === "rename") {
1114
- const ref = args[1];
1115
- const title = args.slice(2).join(" ").trim();
1116
- if (!ref || !title) {
1117
- ctx.send({ type: "error", message: "Usage: /branch rename <id|title> <new title>" });
1118
- }
1119
- const id = resolve6(ref);
1120
- if (!id) {
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
- if (sub === "diff") {
1132
- const ref = args[1];
1133
- if (!ref) {
1134
- ctx.send({ type: "error", message: "Usage: /branch diff <id|title>" });
1135
- }
1136
- const id = resolve6(ref);
1137
- if (!id) {
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
- if (sub === "cherry-pick") {
1171
- const ref = args[1];
1172
- const idxStr = args[2];
1173
- if (!ref || !idxStr) {
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 preview = typeof picked.content === "string" ? picked.content.replace(/\s+/g, " ").trim().slice(0, 60) : "[complex content]";
1189
- ctx.send({ type: "info", message: `\u2713 Cherry-picked ${picked.role} message from "${id}"[${idx}] \u2192 active branch (${session.messages.length} msgs): ${preview}` });
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
- ctx.send({ type: "error", message: `Unknown subcommand: ${sub}. Use list/new/switch/delete/rename/diff/cherry-pick.` });
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
- const session = ctx.sessions.current;
1199
- if (!session) {
1200
- ctx.send({ type: "info", message: "No active session to fork." });
1201
- }
1202
- const sub = args.join(" ").trim();
1203
- let messageCount = session.messages.length;
1204
- let fromLabel = "current position";
1205
- if (sub) {
1206
- const cp = session.checkpoints.find((c) => c.name === sub);
1207
- if (!cp) {
1208
- const available = session.checkpoints.map((c) => c.name);
1209
- ctx.send({ type: "error", message: available.length > 0 ? `Checkpoint "${sub}" not found. Available: ${available.join(", ")}` : `Checkpoint "${sub}" not found. No checkpoints saved.` });
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
- try {
1215
- const originalId = session.id.slice(0, 8);
1216
- const forked = await ctx.sessions.forkSession(messageCount);
1217
- ctx.send({ type: "info", message: `\u{1F500} Session Forked
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
- ctx.sendSessionMessages();
1223
- ctx.sendStatus();
1224
- ctx.sendSessionList();
1225
- } catch (err) {
1226
- ctx.send({ type: "error", message: `Fork failed: ${err.message}` });
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
- const session = ctx.sessions.current;
1233
- if (!session || session.messages.length === 0) {
1234
- ctx.send({ type: "info", message: "No messages to rewind." });
1235
- }
1236
- const rewindSub = args[0];
1237
- if (rewindSub === "list" || !rewindSub) {
1238
- const lines = [`Conversation messages (${session.messages.length} total):
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
- const cpIndices = (await import("./file-checkpoint-NKBHGC7L.js")).fileCheckpoints.getMessageIndices();
1241
- for (let i = 0; i < session.messages.length; i++) {
1242
- const m = session.messages[i];
1243
- const text = getContentText(m.content).replace(/\n/g, " ").slice(0, 60);
1244
- const pin = cpIndices.includes(i) ? " \u{1F4CC}" : "";
1245
- lines.push(` [${i + 1}] ${m.role.padEnd(10)} ${text}${pin}`);
1246
- }
1247
- lines.push("", "Usage: /rewind <n> \u2014 rewind to message N", "\u{1F4CC} = file checkpoint");
1248
- ctx.send({ type: "info", message: lines.join("\n") });
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
- import { existsSync as existsSync2, readdirSync } from "fs";
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(args, ctx) {
1281
- {
1282
- const configDir = ctx.config.getConfigDir();
1283
- const pluginsDir = join(configDir, PLUGINS_DIR_NAME);
1284
- const pluginTools = ctx.toolRegistry.listPluginTools();
1285
- const pluginPackages = listInstalledPlugins(configDir);
1286
- const lines = [`\u{1F50C} **Plugins:**`, `Dir: ${pluginsDir}`, ""];
1287
- if (pluginTools.length === 0 && pluginPackages.length === 0) {
1288
- lines.push("No plugins loaded.");
1289
- lines.push("Use /plugin install <path>, /plugin trust <name>, then /plugin enable <name>.");
1290
- } else {
1291
- if (pluginPackages.length > 0) {
1292
- lines.push(`Package plugins (${pluginPackages.length}):`);
1293
- for (const p of pluginPackages) lines.push(` \u2022 ${p.name}@${p.manifest.version} \u2014 ${p.enabled ? "enabled" : "disabled"} / ${p.trusted ? "trusted" : "untrusted"}`);
1294
- lines.push("");
1295
- }
1296
- if (pluginTools.length > 0) {
1297
- lines.push(`Legacy JS plugin tools (${pluginTools.length}):`);
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
- const configDir = ctx.config.getConfigDir();
1307
- const sub = args[0] ?? "list";
1308
- try {
1309
- if (sub === "list") {
1310
- const plugins = listInstalledPlugins(configDir);
1311
- const lines = [`\u{1F50C} **Package Plugins**`, `Dir: ${pluginRoot(configDir)}`, ""];
1312
- if (plugins.length === 0) lines.push("No package plugins installed.");
1313
- for (const p of plugins) lines.push(` \u2022 ${p.name}@${p.manifest.version} \u2014 ${p.enabled ? "enabled" : "disabled"} / ${p.trusted ? "trusted" : "untrusted"}`);
1314
- ctx.send({ type: "info", message: lines.join("\n") });
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
- async function handleMcp(args, ctx) {
1346
- {
1347
- if (!ctx.mcpManager) {
1348
- ctx.send({ type: "info", message: "MCP not configured. Add mcpServers to config.json." });
1349
- }
1350
- const sub = args[0];
1351
- if (sub === "reconnect") {
1352
- ctx.send({ type: "info", message: "\u{1F504} Reconnecting MCP servers..." });
1353
- try {
1354
- await ctx.mcpManager.reconnectAll();
1355
- ctx.send({ type: "info", message: "\u2713 MCP reconnect complete." });
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
- const statuses = ctx.mcpManager.getStatus();
1362
- if (statuses.length === 0) {
1363
- ctx.send({ type: "info", message: "No MCP servers configured." });
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
- const lines = [`\u{1F50C} **MCP Servers (${statuses.length}):**`, ""];
1366
- for (const s of statuses) {
1367
- const state = s.connected ? `\u2713 connected \xB7 ${s.serverName} \xB7 ${s.toolCount} tools` : `\u2717 disconnected${s.error ? ` \xB7 ${s.error}` : ""}`;
1368
- lines.push(` ${s.serverId}: ${state}`);
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
- lines.push("");
1371
- lines.push("Use /mcp reconnect to reconnect.");
1372
- ctx.send({ type: "info", message: lines.join("\n") });
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 handleSkill(args, ctx) {
1376
- {
1377
- const sub = args[0];
1378
- if (!ctx.skillManager) {
1379
- ctx.send({ type: "error", message: "Skill system not available." });
1380
- }
1381
- if (!sub || sub === "list") {
1382
- const skills = ctx.skillManager.listSkills();
1383
- const active = ctx.skillManager.getActive();
1384
- if (skills.length === 0) {
1385
- ctx.send({ type: "info", message: "No skills available. Add .md files to ~/.aicli/skills/" });
1386
- } else {
1387
- const lines = skills.map((s) => {
1388
- const isActive = active?.meta.name === s.meta.name;
1389
- const marker = isActive ? " \u2705 (active)" : "";
1390
- return ` ${s.meta.name}${marker} \u2014 ${s.meta.description || "(no description)"}`;
1391
- });
1392
- ctx.send({ type: "info", message: `\u{1F4DA} Skills:
1393
- ${lines.join("\n")}` });
1394
- }
1395
- } else if (sub === "off") {
1396
- ctx.skillManager.deactivate();
1397
- ctx.send({ type: "info", message: "\u{1F50C} Skill deactivated." });
1398
- ctx.sendToolsList();
1399
- ctx.sendStatus();
1400
- } else if (sub === "reload") {
1401
- ctx.skillManager.loadSkills(getActivePluginAssets(ctx.config.getConfigDir()).skillDirs);
1402
- ctx.send({ type: "info", message: `\u{1F504} Reloaded ${ctx.skillManager.listSkills().length} skill(s).` });
1403
- ctx.sendToolsList();
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 activated = ctx.skillManager.activate(sub);
1406
- if (activated) {
1407
- ctx.send({ type: "info", message: `\u2705 Skill activated: ${activated.meta.name}
1408
- ${activated.meta.description || ""}` });
1409
- ctx.sendToolsList();
1410
- ctx.sendStatus();
1411
- } else {
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(args, ctx) {
1419
- {
1420
- const configDir = ctx.config.getConfigDir();
1421
- const commandsDir = join(configDir, CUSTOM_COMMANDS_DIR_NAME);
1422
- if (!existsSync2(commandsDir)) {
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
- try {
1427
- const files = readdirSync(commandsDir).filter((f) => f.endsWith(".md"));
1428
- if (files.length === 0) {
1429
- ctx.send({ type: "info", message: `No custom commands found in ${commandsDir}
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
- } else {
1432
- const lines = [`\u{1F4CB} **Custom Commands (${files.length}):**`, `Dir: ${commandsDir}`, ""];
1433
- for (const f of files) {
1434
- const name = f.replace(/\.md$/, "");
1435
- lines.push(` /${name}`);
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
- } catch {
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
- const gitCtx = getGitContext();
1451
- if (!gitCtx) {
1452
- ctx.send({ type: "error", message: "Not a git repository." });
1453
- }
1454
- const staged = args.includes("--staged");
1455
- const detailed = args.includes("--detailed");
1456
- let diff;
1457
- try {
1458
- diff = readGitDiff({ staged });
1459
- } catch {
1460
- ctx.send({ type: "error", message: "Failed to run git diff." });
1461
- }
1462
- if (!diff) {
1463
- ctx.send({ type: "info", message: "No changes to review." + (staged ? "" : " Try --staged for staged changes.") });
1464
- }
1465
- const MAX_DIFF = 8e3;
1466
- let truncated = false;
1467
- if (diff.length > MAX_DIFF) {
1468
- const head = diff.slice(0, Math.floor(MAX_DIFF * 0.7));
1469
- const tail = diff.slice(diff.length - Math.floor(MAX_DIFF * 0.2));
1470
- diff = head + "\n\n... [diff truncated, " + diff.length + " chars total] ...\n\n" + tail;
1471
- truncated = true;
1472
- }
1473
- const reviewPrompt = buildReviewPrompt(diff, formatGitContextForPrompt(gitCtx), detailed);
1474
- ctx.send({ type: "info", message: "\u{1F50D} Analyzing changes..." });
1475
- try {
1476
- const review = await ctx.chatOnce(reviewPrompt, { temperature: 0.3, maxTokens: 8192 });
1477
- const msg = truncated ? review + "\n\n\u26A0 Diff was truncated. Consider reviewing smaller changesets." : review;
1478
- ctx.send({ type: "info", message: msg });
1479
- } catch (err) {
1480
- ctx.send({ type: "error", message: `Review failed: ${err.message}` });
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
- const gitCtx = getGitContext();
1487
- if (!gitCtx) {
1488
- ctx.send({ type: "error", message: "Not a git repository." });
1489
- }
1490
- const secStaged = args.includes("--staged");
1491
- let secDiff;
1492
- try {
1493
- secDiff = readGitDiff({ staged: secStaged });
1494
- } catch {
1495
- ctx.send({ type: "error", message: "Failed to run git diff." });
1496
- }
1497
- if (!secDiff) {
1498
- ctx.send({ type: "info", message: "No changes to review." + (secStaged ? "" : " Try --staged for staged changes.") });
1499
- }
1500
- const SEC_MAX_DIFF = 8e3;
1501
- let secTruncated = false;
1502
- if (secDiff.length > SEC_MAX_DIFF) {
1503
- const head = secDiff.slice(0, Math.floor(SEC_MAX_DIFF * 0.7));
1504
- const tail = secDiff.slice(secDiff.length - Math.floor(SEC_MAX_DIFF * 0.2));
1505
- secDiff = head + "\n\n... [diff truncated, " + secDiff.length + " chars total] ...\n\n" + tail;
1506
- secTruncated = true;
1507
- }
1508
- const secPrompt = buildSecurityReviewPrompt(secDiff, formatGitContextForPrompt(gitCtx));
1509
- ctx.send({ type: "info", message: "\u{1F512} Scanning for security vulnerabilities..." });
1510
- try {
1511
- const secReview = await ctx.chatOnce(secPrompt, { temperature: 0.2, maxTokens: 8192 });
1512
- const secMsg = secTruncated ? secReview + "\n\n\u26A0 Diff was truncated. Consider reviewing smaller changesets." : secReview;
1513
- ctx.send({ type: "info", message: secMsg });
1514
- } catch (err) {
1515
- ctx.send({ type: "error", message: `Security review failed: ${err.message}` });
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
- ctx.send({ type: "info", message: "\u{1F9EA} Running tests..." });
1522
- try {
1523
- const { executeTests } = await import("./run-tests-5NEMM6EF.js");
1524
- const argStr = args.join(" ").trim();
1525
- let testArgs = {};
1526
- if (argStr) {
1527
- const isCommand = argStr.includes(" ") || /^(mvn|gradle|npm|pytest|cargo|go)\b/.test(argStr);
1528
- testArgs = isCommand ? { command: argStr } : { filter: argStr };
1529
- }
1530
- const report = await executeTests(testArgs);
1531
- ctx.send({ type: "info", message: report });
1532
- } catch (err) {
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
- const history = undoStack.getHistory();
1540
- if (history.length === 0) {
1541
- ctx.send({ type: "info", message: "No file modifications in this session." });
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
- const fileMap = /* @__PURE__ */ new Map();
1544
- for (const entry of history) {
1545
- if (!fileMap.has(entry.filePath)) {
1546
- fileMap.set(entry.filePath, { earliest: entry.previousContent });
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 statsOnly = args[0]?.toLowerCase() === "--stats";
1550
- let newFiles = 0;
1551
- let modifiedFiles = 0;
1552
- const diffLines = [];
1553
- for (const [filePath, { earliest }] of fileMap) {
1554
- const currentContent = existsSync3(filePath) ? (() => {
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
- if (currentContent !== null) {
1569
- diffLines.push(` ${currentContent.split("\n").length} lines`);
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
- const oldText = earliest ?? "";
1577
- const newText = currentContent ?? "";
1578
- if (oldText === newText) {
1579
- diffLines.push(" (no net change)");
1580
- } else {
1581
- diffLines.push(renderDiff(oldText, newText, { filePath, maxLines: 80 }));
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
- const cwd = process.cwd();
1593
- const targetPath = join2(cwd, "AICLI.md");
1594
- const force = args.includes("--force");
1595
- if (existsSync3(targetPath) && !force) {
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
- ctx.send({ type: "info", message: "\u{1F4DD} Scanning project structure..." });
1600
- try {
1601
- const projectInfo = scanProject(cwd);
1602
- const prompt = buildInitPrompt(projectInfo, cwd);
1603
- const content = await ctx.chatOnce(prompt, { temperature: 0.3, maxTokens: 4096 });
1604
- writeFileSync2(targetPath, content, "utf-8");
1605
- ctx.send({ type: "info", message: `\u2713 Generated: ${targetPath} (${content.length} chars)
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
- } catch (err) {
1608
- ctx.send({ type: "error", message: `Failed to generate AICLI.md: ${err.message}` });
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
- const description = args.join(" ").trim();
1615
- if (!description) {
1616
- ctx.send({ type: "error", message: "Usage: /scaffold <project description>" });
1617
- }
1618
- ctx.send({ type: "info", message: "\u{1F3D7}\uFE0F Generating project scaffold..." });
1619
- const scaffoldPrompt = `Please scaffold a project based on this description: ${description}
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
- await ctx.handleChat(scaffoldPrompt);
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
- const text = args.join(" ").trim();
1632
- if (!text) {
1633
- const current = ctx.activeSystemPrompt;
1634
- if (current) {
1635
- const preview = current.length > 500 ? current.slice(0, 500) + "..." : current;
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.activeSystemPrompt = text;
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(args, ctx) {
1653
- {
1654
- const sub = args[0];
1655
- if (sub === "reload") {
1656
- ctx.activeSystemPrompt = ctx.loadContextFiles();
1657
- ctx.send({ type: "info", message: ctx.activeSystemPrompt ? `\u2713 Context reloaded (${ctx.activeSystemPrompt.length} chars).` : "\u2713 No context files found." });
1658
- }
1659
- const result = ctx.contextLoadResult;
1660
- const layers = ["\u{1F4DA} **Context Layers:**", ""];
1661
- if (result && result.layers.length > 0) {
1662
- const labels = { global: "Global ", project: "Project", plugin: "Plugin ", local: "Local " };
1663
- for (const layer of result.layers) {
1664
- layers.push(` \u2713 ${labels[layer.level] ?? layer.level}: ${layer.displayPath} (${layer.charCount} chars${layer.truncated ? ", truncated" : ""})`);
1665
- }
1666
- layers.push(` Total: ${result.totalChars} chars (${result.layers.length} layer${result.layers.length > 1 ? "s" : ""})`);
1667
- } else {
1668
- layers.push(" No context files loaded.");
1669
- layers.push(" Search order per layer: AICLI.override.md \u2192 AGENTS.override.md \u2192 AICLI.md \u2192 CLAUDE.md \u2192 AGENTS.md");
1670
- }
1671
- if (result && result.skipped.length > 0) {
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(`Total prompt length: ${ctx.activeSystemPrompt?.length ?? 0} chars`);
1680
- layers.push("Use /context reload to refresh.");
1681
- ctx.send({ type: "info", message: layers.join("\n") });
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
- const sub = (args[0] ?? "status").toLowerCase();
1687
- const root = process.cwd();
1688
- const { loadIndex, clearIndex } = await import("./store-MWNHVGJT.js");
1689
- const { indexProject } = await import("./indexer-2AG4G6B5.js");
1690
- const { loadVectorStore, clearVectorStore } = await import("./vector-store-BBDXB5IQ.js");
1691
- if (sub === "status") {
1692
- const idx = loadIndex(root);
1693
- const vec = loadVectorStore(root);
1694
- if (!idx) {
1695
- ctx.send({ type: "info", message: `No symbol index for ${root}. Run /index rebuild.` });
1696
- } else {
1697
- const semantic = vec ? `${vec.count} vectors \xD7 ${vec.dim}-dim` : "not built \u2014 run /index semantic-rebuild to enable search_code";
1698
- ctx.send({
1699
- type: "info",
1700
- message: [
1701
- "\u{1F50E} Symbol index:",
1702
- ` Root: ${idx.root}`,
1703
- ` Generated: ${idx.generated}`,
1704
- ` Files: ${idx.fileCount}`,
1705
- ` Symbols: ${idx.symbolCount}`,
1706
- ` Semantic: ${semantic}`
1707
- ].join("\n")
1708
- });
1709
- }
1710
- } else if (sub === "rebuild") {
1711
- ctx.send({ type: "info", message: `Indexing ${root}\u2026` });
1712
- const { stats } = await indexProject(root, { force: true });
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: "info",
1715
- message: `\u2713 Indexed ${stats.filesParsed} files (${stats.symbols} symbols) in ${stats.durationMs}ms`
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
- } else if (sub === "clear") {
1718
- clearIndex(root);
1719
- clearVectorStore(root);
1720
- ctx.send({ type: "info", message: `\u2713 Cleared symbol + semantic index for ${root}` });
1721
- } else if (sub === "semantic-rebuild") {
1722
- const idx = loadIndex(root);
1723
- if (!idx) {
1724
- ctx.send({ type: "error", message: "No symbol index yet. Run /index rebuild first." });
1725
- }
1726
- if (idx.symbolCount === 0) {
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: `Building semantic index for ${idx.symbolCount} symbols\u2026 (first run downloads ~117 MB model)`
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
- const sub = args[0];
1762
- if (sub === "add" && args.length > 1) {
1763
- ctx.memoryAdd(args.slice(1).join(" "));
1764
- } else if (sub === "clear") {
1765
- ctx.memoryClear();
1766
- } else {
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
- const sub = args[0]?.trim();
1774
- if (!sub) {
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
- if (sub === "remove" || sub === "clear") {
1781
- ctx.addedDirs.clear();
1782
- ctx.send({ type: "info", message: "\u2713 All added directories removed from context." });
1783
- }
1784
- const dirPath = resolve3(sub);
1785
- if (!existsSync4(dirPath)) {
1786
- ctx.send({ type: "error", message: `Directory not found: ${dirPath}` });
1787
- }
1788
- if (!statSync3(dirPath).isDirectory()) {
1789
- ctx.send({ type: "error", message: `Not a directory: ${dirPath}` });
1790
- }
1791
- ctx.addedDirs.add(dirPath);
1792
- ctx.send({ type: "info", message: `\u2713 Added directory: ${dirPath}
1793
- It will be included in AI context for subsequent messages.` });
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
- const sub = args[0];
1801
- if (sub === "show" || !sub) {
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
- } else if (sub === "get" && args[1]) {
1807
- const val = ctx.config.getByPath(args[1]);
1808
- const display = typeof val === "object" ? JSON.stringify(val, null, 2) : String(val ?? "undefined");
1809
- ctx.send({ type: "info", message: `${args[1]} = ${display}` });
1810
- } else if (sub === "set" && args[1] && args[2] !== void 0) {
1811
- try {
1812
- ctx.config.setByPath(args[1], args.slice(2).join(" "));
1813
- ctx.send({ type: "info", message: `\u2713 ${args[1]} = ${args.slice(2).join(" ")}` });
1814
- } catch (err) {
1815
- ctx.send({ type: "error", message: `Config error: ${err.message}` });
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(args, ctx) {
1823
- {
1824
- const report = buildDoctorReport({
1825
- config: ctx.config,
1826
- providers: ctx.providers,
1827
- cwd: process.cwd(),
1828
- mcpStatuses: ctx.mcpManager?.getStatus() ?? []
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(args, ctx) {
1834
- {
1835
- const session = ctx.sessions.current;
1836
- const lines = [
1837
- "\u{1F41B} **Bug Report Template**",
1838
- "",
1839
- "**Environment:**",
1840
- ` ai-cli version: ${VERSION}`,
1841
- ` Node.js: ${process.version}`,
1842
- ` OS: ${process.platform} ${process.arch}`,
1843
- ` Provider: ${ctx.currentProvider}`,
1844
- ` Model: ${ctx.currentModel}`,
1845
- ` Session: ${session?.id.slice(0, 8) ?? "none"} (${session?.messages.length ?? 0} messages)`,
1846
- "",
1847
- "**Description:**",
1848
- "(Describe the bug)",
1849
- "",
1850
- "**Steps to reproduce:**",
1851
- "1. ...",
1852
- "",
1853
- "**Expected behavior:**",
1854
- "...",
1855
- "",
1856
- "**Actual behavior:**",
1857
- "...",
1858
- "",
1859
- "Report at: https://github.com/jinzhengdong/ai-cli/issues"
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
- const sub = args[0]?.toLowerCase() ?? "status";
1869
- if (sub === "on") {
1870
- ctx.toolExecutor.sessionAutoMode = true;
1871
- 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." });
1872
- } else if (sub === "off") {
1873
- ctx.toolExecutor.sessionAutoMode = false;
1874
- ctx.send({ type: "info", message: "Auto Mode disabled." });
1875
- } else if (sub === "status") {
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
- } else {
1880
- ctx.send({ type: "error", message: "Usage: /auto [on|off|status]" });
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
- const sub = args[0]?.toLowerCase() ?? "recently-denied";
1888
- if (sub === "recently-denied") {
1889
- const rows = getRecentlyDeniedAutoActions();
1890
- if (rows.length === 0) {
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
- ctx.send({ type: "error", message: "Usage: /permissions recently-denied|clear-denied" });
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
- const sub = args[0]?.toLowerCase();
1912
- if (sub === "off") {
1913
- ctx.toolExecutor.sessionAutoApprove = false;
1914
- ctx.send({ type: "info", message: "\u{1F512} Auto-approve disabled \u2014 confirmations restored for this session." });
1915
- } else {
1916
- ctx.toolExecutor.sessionAutoApprove = true;
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
- const sub = args[0]?.toLowerCase() ?? "summary";
1924
- if (sub === "list") {
1925
- const agents = listAgentConfigs(ctx.config.getConfigDir());
1926
- const lines = [`Agents (${agents.length})`, ""];
1927
- for (const agent of agents) {
1928
- const active = agent.name === getPreferredAgentName() ? " *" : " ";
1929
- const model = [agent.provider, agent.model].filter(Boolean).join("/") || "inherit";
1930
- lines.push(`${active} ${agent.name} [${agent.source ?? "builtin"}] \u2014 ${agent.description ?? ""}`);
1931
- lines.push(` model: ${model} \xB7 permission: ${agent.permissionProfile ?? "workspace-write"} \xB7 maxRounds: ${agent.maxToolRounds ?? "inherit"}`);
1932
- }
1933
- lines.push("", "Config dirs: ~/.aicli/agents/ and .aicli/agents/");
1934
- ctx.send({ type: "info", message: lines.join("\n") });
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
- if (sub === "switch") {
1937
- const name = args[1];
1938
- if (!name) {
1939
- ctx.send({ type: "error", message: "Usage: /agent switch <name>" });
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
- if (sub === "stop") {
1952
- const target = args[1] ?? "all";
1953
- const count = requestAgentStop(target);
1954
- ctx.send({ type: "info", message: count > 0 ? `Stop requested for ${count} running agent(s).` : "No matching running agents." });
1955
- ctx.sendStatus();
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
- if (sub === "summary") {
1958
- const runs = listAgentRuns();
1959
- if (runs.length === 0) {
1960
- ctx.send({ type: "info", message: "No sub-agent runs yet." });
1961
- }
1962
- const lines = [`Recent agent runs (${runs.length})`, ""];
1963
- for (const run of runs.slice(0, 20)) {
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: "error", message: "Usage: /agent list|switch <name>|stop <id|all>|summary" });
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(args, ctx) {
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(args, ctx) {
2036
- {
2037
- const providerList = ctx.providers.listAll();
2038
- const toolCount = ctx.toolRegistry.getDefinitions().length;
2039
- const mcpTools = ctx.mcpManager?.getTotalToolCount() ?? 0;
2040
- const lines = [
2041
- `\u{1F916} **ai-cli** v${VERSION}`,
2042
- "",
2043
- `${DESCRIPTION}`,
2044
- `Author: ${AUTHOR} <${AUTHOR_EMAIL}>`,
2045
- "",
2046
- `**Providers:** ${providerList.length} (${providerList.map((p) => p.id).join(", ")})`,
2047
- `**Tools:** ${toolCount + mcpTools} (${toolCount} built-in${mcpTools > 0 ? ` + ${mcpTools} MCP` : ""})`,
2048
- `**REPL Commands:** 37`,
2049
- "",
2050
- `GitHub: https://github.com/jinzhengdong/ai-cli`
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
- const query = args.join(" ").trim();
2058
- if (!query) {
2059
- ctx.send({ type: "error", message: "Usage: /search <keyword>" });
2060
- }
2061
- const results = ctx.sessions.searchMessages(query);
2062
- if (results.length === 0) {
2063
- ctx.send({ type: "info", message: `No sessions found containing "${query}".` });
2064
- }
2065
- const lines = [`\u{1F50D} Found ${results.length} session(s) containing "${query}"`, ""];
2066
- for (const r of results) {
2067
- const { sessionMeta, matches } = r;
2068
- const dateStr = sessionMeta.updated instanceof Date ? sessionMeta.updated.toLocaleDateString() : String(sessionMeta.updated);
2069
- lines.push(`**${sessionMeta.id.slice(0, 8)}** [${dateStr}] ${sessionMeta.provider} / ${sessionMeta.model}`);
2070
- if (sessionMeta.title) lines.push(` ${sessionMeta.title}`);
2071
- for (const m of matches) {
2072
- const icon = m.role === "user" ? "\u{1F464}" : "\u{1F916}";
2073
- lines.push(` ${icon} ${m.snippet}`);
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("Use /session load <id> to resume a session.");
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
- const sub = args.join(" ").trim();
2084
- if (sub === "list") {
2085
- const history = undoStack.getHistory();
2086
- if (history.length === 0) {
2087
- ctx.send({ type: "info", message: "Undo stack is empty." });
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
- if (undoResults.length === 0) {
2114
- ctx.send({ type: "info", message: "Nothing to undo." });
2115
- } else {
2116
- ctx.send({ type: "info", message: `\u2713 ${undoResults.length} operation(s) undone:
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
- this.toolExecutor.setSessionKey(`web-${Math.random().toString(36).slice(2)}-${Date.now()}`);
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 resolve6 = this.pendingAskUser.get(msg.requestId);
2360
- if (resolve6) {
2365
+ const resolve5 = this.pendingAskUser.get(msg.requestId);
2366
+ if (resolve5) {
2361
2367
  this.pendingAskUser.delete(msg.requestId);
2362
- resolve6(msg.answer);
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 resolve6 = this.pendingAutoPause.get(msg.requestId);
2376
- if (resolve6) {
2381
+ const resolve5 = this.pendingAutoPause.get(msg.requestId);
2382
+ if (resolve5) {
2377
2383
  this.pendingAutoPause.delete(msg.requestId);
2378
- resolve6({ action: msg.action, message: msg.message });
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 resolve6 = this.pendingHubReview.get(msg.requestId);
2394
- if (resolve6) {
2399
+ const resolve5 = this.pendingHubReview.get(msg.requestId);
2400
+ if (resolve5) {
2395
2401
  this.pendingHubReview.delete(msg.requestId);
2396
- resolve6({ action: msg.action, message: msg.message });
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 resolve6 of this.pendingAskUser.values()) resolve6(null);
2415
+ for (const resolve5 of this.pendingAskUser.values()) resolve5(null);
2410
2416
  this.pendingAskUser.clear();
2411
- for (const resolve6 of this.pendingAutoPause.values()) resolve6({ action: "stop" });
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((resolve6) => {
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, resolve6);
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
- mkdirSync4(dirname4(pendingTeeSave), { recursive: true });
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((resolve6) => {
2906
- this.pendingAutoPause.set(requestId, resolve6);
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
- mkdirSync4(dirname4(saveToFile), { recursive: true });
3057
- fileStream = createWriteStream4(saveToFile);
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((resolve6, reject) => {
3086
- fileStream.end((err) => err ? reject(err) : resolve6());
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((resolve6) => fileStream.end(() => resolve6()));
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(join4(configDir, MEMORY_FILE_NAME), "");
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-FDSGKWQV.js");
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-FDSGKWQV.js");
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-FDSGKWQV.js");
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 dirname5(filePath);
3632
+ return dirname4(filePath);
3626
3633
  }
3627
3634
  } catch {
3628
3635
  }
3629
- return resolve5(".");
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 = join5(config.getConfigDir(), SKILLS_DIR_NAME);
3698
- if (existsSync6(skillsDir)) {
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 = join5(moduleDir, "web", "client");
3770
- if (!existsSync6(clientDir)) {
3771
- clientDir = join5(moduleDir, "client");
3776
+ let clientDir = join4(moduleDir, "web", "client");
3777
+ if (!existsSync5(clientDir)) {
3778
+ clientDir = join4(moduleDir, "client");
3772
3779
  }
3773
- if (!existsSync6(clientDir)) {
3774
- clientDir = join5(moduleDir, "..", "..", "src", "web", "client");
3780
+ if (!existsSync5(clientDir)) {
3781
+ clientDir = join4(moduleDir, "..", "..", "src", "web", "client");
3775
3782
  }
3776
- if (!existsSync6(clientDir)) {
3777
- clientDir = join5(process.cwd(), "src", "web", "client");
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(join5(clientDir, "vendor"), { maxAge: "1y", immutable: true }));
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 = join5(cwd, prefix);
3854
+ const targetDir = join4(cwd, prefix);
3848
3855
  try {
3849
- const canonicalTarget = realpathSync(resolve5(targetDir));
3850
- const canonicalCwd = realpathSync(resolve5(cwd));
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 = readdirSync5(targetDir, { withFileTypes: true });
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, join5(targetDir, e.name)).replace(/\\/g, "/"),
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 = join5(histDir, `${id}.json`);
3905
- if (!existsSync6(filePath)) {
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(readFileSync6(filePath, "utf-8"));
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 = resolve5(join5(cwd, filePath));
3940
+ const fullPath = resolve4(join4(cwd, filePath));
3934
3941
  try {
3935
3942
  const canonicalFull = realpathSync(fullPath);
3936
- const canonicalCwd = realpathSync(resolve5(cwd));
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 = statSync5(fullPath);
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 = readFileSync6(fullPath, "utf-8");
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((resolve6, reject) => {
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
- resolve6({ port: actualPort, host, url });
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 = join5(projectRoot, MCP_PROJECT_CONFIG_NAME);
4207
- return existsSync6(configPath) ? configPath : null;
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 = join5(projectRoot, MCP_PROJECT_CONFIG_NAME);
4214
- if (!existsSync6(configPath)) return null;
4220
+ const configPath = join4(projectRoot, MCP_PROJECT_CONFIG_NAME);
4221
+ if (!existsSync5(configPath)) return null;
4215
4222
  try {
4216
- const raw = JSON.parse(readFileSync6(configPath, "utf-8"));
4223
+ const raw = JSON.parse(readFileSync5(configPath, "utf-8"));
4217
4224
  return raw.mcpServers ?? raw;
4218
4225
  } catch {
4219
4226
  return null;