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
@@ -38,7 +38,7 @@ import {
38
38
  VERSION,
39
39
  buildUserIdentityPrompt,
40
40
  runTestsTool
41
- } from "./chunk-AV5OPK7Q.js";
41
+ } from "./chunk-HLXQXV23.js";
42
42
  import {
43
43
  hasSemanticIndex,
44
44
  semanticSearch
@@ -54,7 +54,7 @@ import {
54
54
  redactString,
55
55
  scanString,
56
56
  searchChatMemory
57
- } from "./chunk-VHY6NVMQ.js";
57
+ } from "./chunk-LF3OUJHK.js";
58
58
  import "./chunk-JV5N65KN.js";
59
59
  import {
60
60
  atomicWriteFileSync
@@ -64,8 +64,8 @@ import {
64
64
  import express from "express";
65
65
  import { createServer } from "http";
66
66
  import { WebSocketServer } from "ws";
67
- import { join as join29, dirname as dirname12, resolve as resolve14, relative as relative4, sep as sep3 } from "path";
68
- import { existsSync as existsSync34, readFileSync as readFileSync27, readdirSync as readdirSync18, statSync as statSync16, realpathSync } from "fs";
67
+ import { join as join28, dirname as dirname11, resolve as resolve13, relative as relative4, sep as sep3 } from "path";
68
+ import { existsSync as existsSync33, readFileSync as readFileSync26, readdirSync as readdirSync17, statSync as statSync15, realpathSync } from "fs";
69
69
  import { networkInterfaces } from "os";
70
70
 
71
71
  // src/config/config-manager.ts
@@ -4857,9 +4857,9 @@ var SessionManager = class {
4857
4857
  };
4858
4858
 
4859
4859
  // src/tools/builtin/bash.ts
4860
- import { spawn } from "child_process";
4860
+ import { spawn as spawn2 } from "child_process";
4861
4861
  import { existsSync as existsSync5, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
4862
- import { platform as platform2 } from "os";
4862
+ import { platform as platform3 } from "os";
4863
4863
  import { resolve } from "path";
4864
4864
 
4865
4865
  // src/tools/undo-stack.ts
@@ -5020,9 +5020,192 @@ function decodeWindowsBuffer(buf) {
5020
5020
  }
5021
5021
  }
5022
5022
 
5023
- // src/tools/builtin/bash.ts
5023
+ // src/tools/shell-pool.ts
5024
+ import { spawn } from "child_process";
5025
+ import { platform as platform2 } from "os";
5024
5026
  var IS_WINDOWS2 = platform2() === "win32";
5025
5027
  var SHELL = IS_WINDOWS2 ? "powershell.exe" : process.env["SHELL"] ?? "/bin/bash";
5028
+ var pool = /* @__PURE__ */ new Map();
5029
+ var IDLE_EVICT_MS = 5 * 6e4;
5030
+ var evictionTimer = null;
5031
+ function spawnShell(cwd) {
5032
+ const shellArgs = IS_WINDOWS2 ? ["-NoProfile", "-NonInteractive", "-Command", "-"] : ["-s"];
5033
+ const proc = spawn(SHELL, shellArgs, {
5034
+ cwd,
5035
+ windowsHide: true,
5036
+ stdio: ["pipe", "pipe", "pipe"]
5037
+ });
5038
+ proc.on("exit", () => {
5039
+ for (const [key, entry] of pool) {
5040
+ if (entry.proc === proc) {
5041
+ pool.delete(key);
5042
+ break;
5043
+ }
5044
+ }
5045
+ });
5046
+ return proc;
5047
+ }
5048
+ function startEvictionTimer() {
5049
+ if (evictionTimer) return;
5050
+ evictionTimer = setInterval(() => {
5051
+ const now = Date.now();
5052
+ for (const [key, entry] of pool) {
5053
+ if (now - entry.lastUsed > IDLE_EVICT_MS) {
5054
+ try {
5055
+ entry.proc.kill();
5056
+ } catch {
5057
+ }
5058
+ pool.delete(key);
5059
+ }
5060
+ }
5061
+ }, 6e4).unref();
5062
+ }
5063
+ async function execInPool(sessionKey, command, timeout, cwd, abortSignal) {
5064
+ let entry = pool.get(sessionKey);
5065
+ if (!entry || entry.proc.exitCode !== null || entry.cwd !== cwd) {
5066
+ if (entry) {
5067
+ try {
5068
+ entry.proc.kill();
5069
+ } catch {
5070
+ }
5071
+ }
5072
+ const proc = spawnShell(cwd);
5073
+ entry = { proc, cwd, lastUsed: Date.now() };
5074
+ pool.set(sessionKey, entry);
5075
+ }
5076
+ entry.lastUsed = Date.now();
5077
+ startEvictionTimer();
5078
+ const id = `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
5079
+ const markerStart = `AICLI_MARKER_START:${id}`;
5080
+ const markerEnd = `AICLI_MARKER_END:${id}`;
5081
+ const wrappedCmd = IS_WINDOWS2 ? `echo "${markerStart}"; ${command}; echo "${markerEnd}:0"` : `echo "${markerStart}"; ${command}; echo "${markerEnd}:$?"`;
5082
+ return new Promise((resolve14) => {
5083
+ let stdout = "";
5084
+ let stderr = "";
5085
+ let timedOut = false;
5086
+ let aborted = false;
5087
+ let resolved = false;
5088
+ const markerStartPattern = `AICLI_MARKER_START:${id}`;
5089
+ const markerEndPattern = `AICLI_MARKER_END:${id}`;
5090
+ const onData = (chunk) => {
5091
+ stdout += chunk.toString("utf-8");
5092
+ if (!resolved && stdout.includes(markerEndPattern)) {
5093
+ const endLine = stdout.split("\n").find((l) => l.includes(markerEndPattern));
5094
+ const exitCode = endLine ? parseEndMarker(endLine, markerEndPattern) : null;
5095
+ stdout = stripMarkerOutput(stdout, markerStartPattern, markerEndPattern);
5096
+ resolved = true;
5097
+ cleanup();
5098
+ resolve14({ stdout, stderr, exitCode, timedOut, aborted });
5099
+ }
5100
+ };
5101
+ const onErr = (chunk) => {
5102
+ stderr += chunk.toString("utf-8");
5103
+ };
5104
+ const proc = entry.proc;
5105
+ proc.stdout?.on("data", onData);
5106
+ proc.stderr?.on("data", onErr);
5107
+ const timeoutTimer = setTimeout(() => {
5108
+ if (resolved) return;
5109
+ timedOut = true;
5110
+ resolved = true;
5111
+ stdout = stripMarkerOutput(stdout, markerStartPattern, markerEndPattern);
5112
+ cleanup();
5113
+ resolve14({ stdout, stderr, exitCode: null, timedOut, aborted });
5114
+ try {
5115
+ proc.kill();
5116
+ } catch {
5117
+ }
5118
+ pool.delete(sessionKey);
5119
+ }, timeout);
5120
+ const onAbort = () => {
5121
+ if (resolved) return;
5122
+ aborted = true;
5123
+ resolved = true;
5124
+ cleanup();
5125
+ stdout = stripMarkerOutput(stdout, markerStartPattern, markerEndPattern);
5126
+ resolve14({ stdout, stderr, exitCode: null, timedOut, aborted });
5127
+ try {
5128
+ proc.kill();
5129
+ } catch {
5130
+ }
5131
+ pool.delete(sessionKey);
5132
+ };
5133
+ if (abortSignal?.aborted) {
5134
+ onAbort();
5135
+ } else {
5136
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
5137
+ }
5138
+ const onError = (err) => {
5139
+ if (resolved) return;
5140
+ resolved = true;
5141
+ stderr += `
5142
+ [pool error] ${err.message}`;
5143
+ cleanup();
5144
+ resolve14({ stdout, stderr, exitCode: null, timedOut, aborted });
5145
+ pool.delete(sessionKey);
5146
+ };
5147
+ proc.on("error", onError);
5148
+ function cleanup() {
5149
+ clearTimeout(timeoutTimer);
5150
+ abortSignal?.removeEventListener("abort", onAbort);
5151
+ proc.stdout?.removeListener("data", onData);
5152
+ proc.stderr?.removeListener("data", onErr);
5153
+ proc.removeListener("error", onError);
5154
+ }
5155
+ try {
5156
+ proc.stdin?.write(wrappedCmd + "\n");
5157
+ } catch (err) {
5158
+ if (!resolved) {
5159
+ cleanup();
5160
+ pool.delete(sessionKey);
5161
+ }
5162
+ }
5163
+ });
5164
+ }
5165
+ function parseEndMarker(line, markerEndPattern) {
5166
+ const idx = line.lastIndexOf(":");
5167
+ if (idx < 0) return null;
5168
+ const code = line.slice(idx + 1).trim();
5169
+ return /^\d+$/.test(code) ? Number(code) : null;
5170
+ }
5171
+ function stripMarkerOutput(stdout, startMarker, endMarker) {
5172
+ const lines = stdout.split("\n");
5173
+ let capturing = false;
5174
+ const out = [];
5175
+ for (const line of lines) {
5176
+ if (line.includes(startMarker)) {
5177
+ capturing = true;
5178
+ continue;
5179
+ }
5180
+ if (line.includes(endMarker)) {
5181
+ capturing = false;
5182
+ continue;
5183
+ }
5184
+ if (capturing) {
5185
+ out.push(line);
5186
+ }
5187
+ }
5188
+ if (!stdout.includes(endMarker) && out.length === 0) {
5189
+ const startIdx = stdout.indexOf(startMarker);
5190
+ if (startIdx >= 0) {
5191
+ return stdout.slice(startIdx + startMarker.length).trimStart();
5192
+ }
5193
+ }
5194
+ return out.join("\n").trim();
5195
+ }
5196
+ function cleanupPoolSession(sessionKey) {
5197
+ const entry = pool.get(sessionKey);
5198
+ if (!entry) return;
5199
+ try {
5200
+ entry.proc.kill();
5201
+ } catch {
5202
+ }
5203
+ pool.delete(sessionKey);
5204
+ }
5205
+
5206
+ // src/tools/builtin/bash.ts
5207
+ var IS_WINDOWS3 = platform3() === "win32";
5208
+ var SHELL2 = IS_WINDOWS3 ? "powershell.exe" : process.env["SHELL"] ?? "/bin/bash";
5026
5209
  var cwdBySession = /* @__PURE__ */ new Map();
5027
5210
  function getCwd() {
5028
5211
  const key = getCurrentSessionKey();
@@ -5036,17 +5219,21 @@ function getCwd() {
5036
5219
  function setCwd(next) {
5037
5220
  cwdBySession.set(getCurrentSessionKey(), next);
5038
5221
  }
5222
+ function clearBashSession(sessionKey) {
5223
+ cwdBySession.delete(sessionKey);
5224
+ cleanupPoolSession(sessionKey);
5225
+ }
5039
5226
  var bashTool = {
5040
5227
  definition: {
5041
5228
  name: "bash",
5042
- description: IS_WINDOWS2 ? `Execute commands in PowerShell. Supports mkdir, ls, cat, python, etc.
5229
+ description: IS_WINDOWS3 ? `Execute commands in PowerShell. Supports mkdir, ls, cat, python, etc.
5043
5230
  Important rules:
5044
5231
  1. Each bash call runs in an independent subprocess; cd commands do not persist. To run in a specific directory, use the cwd parameter, or combine commands: e.g. "cd mydir; ls" or "mkdir mydir; cd mydir; New-Item file.txt".
5045
5232
  2. If a command fails (returns an error or non-zero exit code), stop immediately, report the error to the user, and do not retry the same or similar commands.
5046
5233
  3. Multiple commands can be combined with semicolons in a single call to reduce rounds.
5047
5234
  4. To delete directories, use Remove-Item -Recurse (the system will automatically optimize to a more reliable method).
5048
5235
  5. IMPORTANT: On Windows, "curl" is an alias for Invoke-WebRequest and does NOT support curl flags like -s, -X, -H. Use Invoke-RestMethod instead for HTTP requests. Example: Invoke-RestMethod -Uri "http://localhost:3000/api/health" -Method Get
5049
- 6. NEVER run long-running / never-exiting commands through bash \u2014 they block until timeout. This includes starting an Android emulator (emulator -avd X), streaming logs (adb logcat), and dev servers (node server.js, npm run dev, metro / react-native start). Use the 'task_create' tool to run these in the background, then poll status with short bash calls (e.g. adb shell getprop sys.boot_completed).` : `Execute commands in ${SHELL}.
5236
+ 6. NEVER run long-running / never-exiting commands through bash \u2014 they block until timeout. This includes starting an Android emulator (emulator -avd X), streaming logs (adb logcat), and dev servers (node server.js, npm run dev, metro / react-native start). Use the 'task_create' tool to run these in the background, then poll status with short bash calls (e.g. adb shell getprop sys.boot_completed).` : `Execute commands in ${SHELL2}.
5050
5237
  Important rules:
5051
5238
  1. Each bash call runs in an independent subprocess; cd commands do not persist. To run in a specific directory, use the cwd parameter, or combine commands: e.g. "cd mydir && ls" or "mkdir -p mydir && touch mydir/file.txt".
5052
5239
  2. If a command fails (returns an error or non-zero exit code), stop immediately, report the error to the user, and do not retry the same or similar commands.
@@ -5055,7 +5242,7 @@ Important rules:
5055
5242
  parameters: {
5056
5243
  command: {
5057
5244
  type: "string",
5058
- description: IS_WINDOWS2 ? `PowerShell command to execute. Combine multiple commands with semicolons, e.g.: "mkdir mydir; Set-Content mydir/file.txt 'content'"` : `${SHELL} command to execute. Combine multiple commands with &&, e.g.: "mkdir -p mydir && echo 'content' > mydir/file.txt"`,
5245
+ description: IS_WINDOWS3 ? `PowerShell command to execute. Combine multiple commands with semicolons, e.g.: "mkdir mydir; Set-Content mydir/file.txt 'content'"` : `${SHELL2} command to execute. Combine multiple commands with &&, e.g.: "mkdir -p mydir && echo 'content' > mydir/file.txt"`,
5059
5246
  required: true
5060
5247
  },
5061
5248
  cwd: {
@@ -5107,7 +5294,7 @@ Important rules:
5107
5294
  setCwd(resolved);
5108
5295
  }
5109
5296
  let actualCommand;
5110
- if (IS_WINDOWS2) {
5297
+ if (IS_WINDOWS3) {
5111
5298
  const fixedCommand = fixWindowsDeleteCommand(command);
5112
5299
  actualCommand = WIN_UTF8_PREAMBLE + fixedCommand;
5113
5300
  } else {
@@ -5119,6 +5306,44 @@ Important rules:
5119
5306
  for (const t of parsedTargets) {
5120
5307
  parsedTargetsBefore.set(t, existsSync5(t));
5121
5308
  }
5309
+ let poolResult = null;
5310
+ try {
5311
+ poolResult = await execInPool(
5312
+ getCurrentSessionKey(),
5313
+ actualCommand,
5314
+ timeout,
5315
+ effectiveCwd,
5316
+ currentAbortSignal()
5317
+ );
5318
+ } catch {
5319
+ poolResult = null;
5320
+ }
5321
+ if (poolResult) {
5322
+ const { stdout, stderr, exitCode, timedOut, aborted } = poolResult;
5323
+ const status = exitCode;
5324
+ if (aborted) {
5325
+ pushBashUndoEntries(beforeSnapshot, parsedTargetsBefore, effectiveCwd);
5326
+ throw new ToolError(
5327
+ "bash",
5328
+ `Command interrupted by user (Ctrl+C).
5329
+
5330
+ [The user pressed Ctrl+C while this command was running. Stop the current task, summarize what was done so far, and wait for the user's next instruction. Do NOT retry this command or continue with further tool calls unless explicitly asked.]`
5331
+ );
5332
+ }
5333
+ if (status !== 0 || timedOut) {
5334
+ throw {
5335
+ status,
5336
+ signal: null,
5337
+ code: timedOut ? "ETIMEDOUT" : void 0,
5338
+ stdout: Buffer.from(stdout, "utf-8"),
5339
+ stderr: Buffer.from(stderr, "utf-8"),
5340
+ message: timedOut ? "ETIMEDOUT" : `exit ${status}`
5341
+ };
5342
+ }
5343
+ pushBashUndoEntries(beforeSnapshot, parsedTargetsBefore, effectiveCwd);
5344
+ updateCwdFromCommand(command, effectiveCwd);
5345
+ return stdout || "(command completed with no output)";
5346
+ }
5122
5347
  try {
5123
5348
  const { stdout, stderr, status, signal, timedOut, aborted } = await runShell(
5124
5349
  actualCommand,
@@ -5153,7 +5378,7 @@ Important rules:
5153
5378
  }
5154
5379
  updateCwdFromCommand(command, effectiveCwd);
5155
5380
  pushBashUndoEntries(beforeSnapshot, parsedTargetsBefore, effectiveCwd);
5156
- const result = Buffer.isBuffer(stdout) ? IS_WINDOWS2 ? decodeWindowsBuffer(stdout) : stdout.toString("utf-8") : String(stdout ?? "");
5381
+ const result = Buffer.isBuffer(stdout) ? IS_WINDOWS3 ? decodeWindowsBuffer(stdout) : stdout.toString("utf-8") : String(stdout ?? "");
5157
5382
  return result || "(command completed with no output)";
5158
5383
  } catch (err) {
5159
5384
  pushBashUndoEntries(beforeSnapshot, parsedTargetsBefore, effectiveCwd);
@@ -5178,8 +5403,8 @@ How to recover (pick ONE \u2014 do NOT retry the same command):
5178
5403
  );
5179
5404
  }
5180
5405
  if ("status" in execErr && execErr.status !== void 0) {
5181
- const stderr = IS_WINDOWS2 && Buffer.isBuffer(execErr.stderr) ? decodeWindowsBuffer(execErr.stderr).trim() : execErr.stderr?.toString().trim() ?? "";
5182
- const stdout = IS_WINDOWS2 && Buffer.isBuffer(execErr.stdout) ? decodeWindowsBuffer(execErr.stdout).trim() : execErr.stdout?.toString().trim() ?? "";
5406
+ const stderr = IS_WINDOWS3 && Buffer.isBuffer(execErr.stderr) ? decodeWindowsBuffer(execErr.stderr).trim() : execErr.stderr?.toString().trim() ?? "";
5407
+ const stdout = IS_WINDOWS3 && Buffer.isBuffer(execErr.stdout) ? decodeWindowsBuffer(execErr.stdout).trim() : execErr.stdout?.toString().trim() ?? "";
5183
5408
  const combined = [stdout, stderr].filter(Boolean).join("\n");
5184
5409
  const hint = buildErrorHint(command, combined);
5185
5410
  throw new ToolError(
@@ -5215,12 +5440,12 @@ function fixWindowsDeleteCommand(command) {
5215
5440
  }
5216
5441
  function buildErrorHint(command, stderr) {
5217
5442
  const hints = [];
5218
- if (IS_WINDOWS2 && /\|\|/.test(command) && /(\|\||is not a valid argument|不是此版本中的有效|ParserError|Unexpected token)/i.test(stderr)) {
5443
+ if (IS_WINDOWS3 && /\|\|/.test(command) && /(\|\||is not a valid argument|不是此版本中的有效|ParserError|Unexpected token)/i.test(stderr)) {
5219
5444
  hints.push(
5220
5445
  `Hint: PowerShell parses '||' as a pipeline operator (PS 5.x) or logical-or (PS 7+), it CANNOT be passed inline as SQL string concatenation. Workaround: write the SQL to a local .sql file with write_file, then 'scp' it to the remote host and run 'psql -f /tmp/x.sql'. Do NOT try to escape || or wrap the command differently \u2014 it will not work.`
5221
5446
  );
5222
5447
  }
5223
- if (IS_WINDOWS2 && /\bpython3\b/.test(command)) {
5448
+ if (IS_WINDOWS3 && /\bpython3\b/.test(command)) {
5224
5449
  const trivialStderr = stderr.trim().length === 0 || /^[·\s]*exit 1\b/i.test(stderr.trim());
5225
5450
  if (/(not recognized|is not recognized|无法将.*识别)/i.test(stderr) || trivialStderr) {
5226
5451
  hints.push(
@@ -5228,27 +5453,27 @@ function buildErrorHint(command, stderr) {
5228
5453
  );
5229
5454
  }
5230
5455
  }
5231
- if (IS_WINDOWS2 && /^\s*ssh\b/.test(command) && /\\"/.test(command) && /(syntax error|ERROR:|ParserError|unexpected|parser|意外的|语法|不是此版本|所在位置\s+行)/i.test(stderr)) {
5456
+ if (IS_WINDOWS3 && /^\s*ssh\b/.test(command) && /\\"/.test(command) && /(syntax error|ERROR:|ParserError|unexpected|parser|意外的|语法|不是此版本|所在位置\s+行)/i.test(stderr)) {
5232
5457
  hints.push(
5233
5458
  `Hint: SSH + nested quotes ("...\\"...\\"") is fragile across PS \u2192 ssh \u2192 remote-shell \u2192 psql layers. Use the file-based flow: (1) write_file locally to a .sql file, (2) 'scp x.sql root@host:/tmp/', (3) 'ssh root@host "sudo -u postgres psql -d <db> -f /tmp/x.sql"'. Or for remote-only SQL: 'ssh host "cat > /tmp/x.sql << \\'SQLEOF\\'\\n...\\nSQLEOF"' then run psql -f. Avoid inline psql -c with embedded quotes.`
5234
5459
  );
5235
5460
  }
5236
- if (IS_WINDOWS2 && /\bmkdir\b/.test(command) && /\s-p\b/.test(command) && /(已存在|already exists|ItemExistsUnauthorizedAccessError|exists)/i.test(stderr)) {
5461
+ if (IS_WINDOWS3 && /\bmkdir\b/.test(command) && /\s-p\b/.test(command) && /(已存在|already exists|ItemExistsUnauthorizedAccessError|exists)/i.test(stderr)) {
5237
5462
  hints.push(
5238
5463
  `Hint: PowerShell's mkdir does not support the '-p' flag. When the directory already exists it exits with code 1. Use instead: New-Item -ItemType Directory -Force -Path <dir> ('-Force' silently succeeds even if the directory already exists, mimicking 'mkdir -p').`
5239
5464
  );
5240
5465
  }
5241
- if (IS_WINDOWS2 && /Remove-Item/i.test(command) && /cannot find path|no such file|exit 1/i.test(stderr || command)) {
5466
+ if (IS_WINDOWS3 && /Remove-Item/i.test(command) && /cannot find path|no such file|exit 1/i.test(stderr || command)) {
5242
5467
  hints.push(
5243
5468
  `Hint: On Windows, "Remove-Item ... -ErrorAction SilentlyContinue" still makes the powershell process exit 1 when no files match (even though the error is silenced). Use Test-Path as a guard: @('tmp_a.sql','tmp_b.sql','tmp_c.csv') | Where-Object { Test-Path $_ } | Remove-Item -Force \u2014 Test-Path returns a bool without raising, so missing paths are filtered out and exit code is 0.`
5244
5469
  );
5245
5470
  }
5246
- if (IS_WINDOWS2 && /-(?:EA|ErrorAction)\s+(?:SilentlyContinue|Ignore)\b/i.test(command) && /\b(Get-ChildItem|Get-Item|Get-Content|gci|gi|gc|dir|ls)\b/i.test(command) && !/Remove-Item/i.test(command) && !/Test-Path/i.test(command) && /cannot find path|no such file|does not exist|exit 1|不存在|找不到/i.test(stderr || command)) {
5471
+ if (IS_WINDOWS3 && /-(?:EA|ErrorAction)\s+(?:SilentlyContinue|Ignore)\b/i.test(command) && /\b(Get-ChildItem|Get-Item|Get-Content|gci|gi|gc|dir|ls)\b/i.test(command) && !/Remove-Item/i.test(command) && !/Test-Path/i.test(command) && /cannot find path|no such file|does not exist|exit 1|不存在|找不到/i.test(stderr || command)) {
5247
5472
  hints.push(
5248
5473
  `Hint: On Windows, "-ErrorAction SilentlyContinue" silences the error MESSAGE but does NOT change the exit code \u2014 the process still exits 1 when the path doesn't exist (because powershell.exe -NonInteractive flips the exit code on any non-terminating error, silenced or not). If you're just probing for existence, use Test-Path instead \u2014 it returns $true/$false without raising: \`if (Test-Path ./mydir) { Get-ChildItem ./mydir }\` or unconditionally use \`New-Item -ItemType Directory -Force -Path ./mydir\` which is idempotent. Don't retry the same -ErrorAction SilentlyContinue probe expecting a different result.`
5249
5474
  );
5250
5475
  }
5251
- if (IS_WINDOWS2 && /\bWrite-(?:Content|File)\b/i.test(command) && /(not recognized|无法将.*识别|不是.*cmdlet|CommandNotFoundException)/i.test(stderr)) {
5476
+ if (IS_WINDOWS3 && /\bWrite-(?:Content|File)\b/i.test(command) && /(not recognized|无法将.*识别|不是.*cmdlet|CommandNotFoundException)/i.test(stderr)) {
5252
5477
  hints.push(
5253
5478
  `Hint: 'Write-Content' / 'Write-File' are NOT PowerShell cmdlets (common LLM hallucination). To write a file, prefer the built-in 'write_file' tool \u2014 it handles encoding/escaping cleanly and supports diff preview. If you must stay in bash: use 'Set-Content -Path <file> -Value <string> -Encoding utf8' (overwrites) or 'Add-Content' (appends) or 'Out-File -FilePath <file> -Encoding utf8'.`
5254
5479
  );
@@ -5360,8 +5585,8 @@ function pushBashUndoEntries(beforeSnapshot, parsedTargetsBefore, cwd) {
5360
5585
  }
5361
5586
  function runShell(command, opts) {
5362
5587
  return new Promise((resolvePromise) => {
5363
- const shellArgs = IS_WINDOWS2 ? ["-NoProfile", "-NonInteractive", "-Command", command] : ["-c", command];
5364
- const child = spawn(SHELL, shellArgs, {
5588
+ const shellArgs = IS_WINDOWS3 ? ["-NoProfile", "-NonInteractive", "-Command", command] : ["-c", command];
5589
+ const child = spawn2(SHELL2, shellArgs, {
5365
5590
  cwd: opts.cwd,
5366
5591
  env: opts.env,
5367
5592
  windowsHide: true,
@@ -5423,9 +5648,9 @@ function runShell(command, opts) {
5423
5648
  }
5424
5649
  function killChild(child) {
5425
5650
  if (child.killed || child.exitCode !== null) return;
5426
- if (IS_WINDOWS2 && child.pid) {
5651
+ if (IS_WINDOWS3 && child.pid) {
5427
5652
  try {
5428
- spawn("taskkill", ["/PID", String(child.pid), "/T", "/F"], {
5653
+ spawn2("taskkill", ["/PID", String(child.pid), "/T", "/F"], {
5429
5654
  windowsHide: true,
5430
5655
  stdio: "ignore"
5431
5656
  });
@@ -7257,7 +7482,7 @@ var ToolExecutor = class {
7257
7482
  rl.resume();
7258
7483
  process.stdout.write(prompt);
7259
7484
  this.confirming = true;
7260
- return new Promise((resolve15) => {
7485
+ return new Promise((resolve14) => {
7261
7486
  let completed = false;
7262
7487
  const cleanup = (result) => {
7263
7488
  if (completed) return;
@@ -7267,7 +7492,7 @@ var ToolExecutor = class {
7267
7492
  rl.pause();
7268
7493
  rlAny.output = savedOutput;
7269
7494
  this.confirming = false;
7270
- resolve15(result);
7495
+ resolve14(result);
7271
7496
  };
7272
7497
  const onLine = (line) => {
7273
7498
  const trimmed = line.trim();
@@ -7441,7 +7666,7 @@ var ToolExecutor = class {
7441
7666
  rl.resume();
7442
7667
  process.stdout.write(color("Proceed? [y/N] (type y + Enter to confirm) "));
7443
7668
  this.confirming = true;
7444
- return new Promise((resolve15) => {
7669
+ return new Promise((resolve14) => {
7445
7670
  let completed = false;
7446
7671
  const cleanup = (answer) => {
7447
7672
  if (completed) return;
@@ -7451,7 +7676,7 @@ var ToolExecutor = class {
7451
7676
  rl.pause();
7452
7677
  rlAny.output = savedOutput;
7453
7678
  this.confirming = false;
7454
- resolve15(answer === "y");
7679
+ resolve14(answer === "y");
7455
7680
  };
7456
7681
  const onLine = (line) => {
7457
7682
  const trimmed = line.trim();
@@ -8637,9 +8862,9 @@ function collectMatchingFiles(dirPath, rootPath, regex, results, maxResults) {
8637
8862
  }
8638
8863
 
8639
8864
  // src/tools/builtin/run-interactive.ts
8640
- import { spawn as spawn2 } from "child_process";
8641
- import { platform as platform3 } from "os";
8642
- var IS_WINDOWS3 = platform3() === "win32";
8865
+ import { spawn as spawn3 } from "child_process";
8866
+ import { platform as platform4 } from "os";
8867
+ var IS_WINDOWS4 = platform4() === "win32";
8643
8868
  var runInteractiveTool = {
8644
8869
  definition: {
8645
8870
  name: "run_interactive",
@@ -8700,8 +8925,8 @@ var runInteractiveTool = {
8700
8925
  PYTHONDONTWRITEBYTECODE: "1"
8701
8926
  };
8702
8927
  const prefixWarnings = [argsTypeWarning, stdinTypeWarning].filter(Boolean).join("");
8703
- return new Promise((resolve15) => {
8704
- const child = spawn2(executable, cmdArgs.map(String), {
8928
+ return new Promise((resolve14) => {
8929
+ const child = spawn3(executable, cmdArgs.map(String), {
8705
8930
  cwd: process.cwd(),
8706
8931
  env,
8707
8932
  stdio: ["pipe", "pipe", "pipe"]
@@ -8719,7 +8944,7 @@ var runInteractiveTool = {
8719
8944
  let lineIdx = 0;
8720
8945
  const writeNextLine = () => {
8721
8946
  if (lineIdx < stdinLines.length && !child.stdin.destroyed) {
8722
- const line = stdinLines[lineIdx++] + (IS_WINDOWS3 ? "\r\n" : "\n");
8947
+ const line = stdinLines[lineIdx++] + (IS_WINDOWS4 ? "\r\n" : "\n");
8723
8948
  const canContinue = child.stdin.write(line);
8724
8949
  if (canContinue) {
8725
8950
  setTimeout(writeNextLine, 150);
@@ -8733,22 +8958,22 @@ var runInteractiveTool = {
8733
8958
  setTimeout(writeNextLine, 400);
8734
8959
  const timer = setTimeout(() => {
8735
8960
  child.kill();
8736
- resolve15(`${prefixWarnings}[Timeout after ${timeout}ms]
8961
+ resolve14(`${prefixWarnings}[Timeout after ${timeout}ms]
8737
8962
  ${buildOutput(stdout, stderr)}`);
8738
8963
  }, timeout);
8739
8964
  child.on("close", (code) => {
8740
8965
  clearTimeout(timer);
8741
8966
  const output = buildOutput(stdout, stderr);
8742
8967
  if (code !== 0 && code !== null) {
8743
- resolve15(`${prefixWarnings}Exit code ${code}:
8968
+ resolve14(`${prefixWarnings}Exit code ${code}:
8744
8969
  ${output}`);
8745
8970
  } else {
8746
- resolve15(`${prefixWarnings}${output || "(no output)"}`);
8971
+ resolve14(`${prefixWarnings}${output || "(no output)"}`);
8747
8972
  }
8748
8973
  });
8749
8974
  child.on("error", (err) => {
8750
8975
  clearTimeout(timer);
8751
- resolve15(
8976
+ resolve14(
8752
8977
  `${prefixWarnings}Failed to start process "${executable}": ${err.message}
8753
8978
  Hint: On Windows, use the full path to the executable, e.g.:
8754
8979
  C:\\Users\\Jinzd\\anaconda3\\envs\\python312\\python.exe`
@@ -9798,7 +10023,7 @@ function promptUser(rl, question) {
9798
10023
  console.log();
9799
10024
  console.log(chalk4.cyan("\u2753 ") + chalk4.bold(question));
9800
10025
  process.stdout.write(chalk4.cyan("> "));
9801
- return new Promise((resolve15) => {
10026
+ return new Promise((resolve14) => {
9802
10027
  let completed = false;
9803
10028
  const cleanup = (answer) => {
9804
10029
  if (completed) return;
@@ -9808,7 +10033,7 @@ function promptUser(rl, question) {
9808
10033
  rl.pause();
9809
10034
  rlAny.output = savedOutput;
9810
10035
  askUserContext.prompting = false;
9811
- resolve15(answer);
10036
+ resolve14(answer);
9812
10037
  };
9813
10038
  const onLine = (line) => {
9814
10039
  cleanup(line);
@@ -10935,9 +11160,9 @@ var spawnAgentTool = {
10935
11160
  };
10936
11161
 
10937
11162
  // src/tools/builtin/task-manager.ts
10938
- import { spawn as spawn3 } from "child_process";
11163
+ import { spawn as spawn4 } from "child_process";
10939
11164
  import { randomUUID as randomUUID2 } from "crypto";
10940
- import { platform as platform4 } from "os";
11165
+ import { platform as platform5 } from "os";
10941
11166
  var MAX_OUTPUT_CHARS = 1e4;
10942
11167
  var MAX_OUTPUT_BYTES = 64 * 1024;
10943
11168
  var MAX_TASKS = 20;
@@ -10971,7 +11196,7 @@ function createTask(command, description) {
10971
11196
  }
10972
11197
  const id = randomUUID2().slice(0, 8);
10973
11198
  const { shell, args } = shellInvocation(command);
10974
- const proc = spawn3(shell, args, {
11199
+ const proc = spawn4(shell, args, {
10975
11200
  stdio: ["ignore", "pipe", "pipe"],
10976
11201
  detached: false,
10977
11202
  env: { ...process.env, PYTHONUTF8: "1", PYTHONIOENCODING: "utf-8" }
@@ -11021,9 +11246,9 @@ function stopTask(id) {
11021
11246
  if (!task || task.status !== "running") return false;
11022
11247
  try {
11023
11248
  const proc = task.process;
11024
- if (platform4() === "win32" && proc.pid) {
11249
+ if (platform5() === "win32" && proc.pid) {
11025
11250
  try {
11026
- spawn3("taskkill", ["/PID", String(proc.pid), "/T", "/F"], {
11251
+ spawn4("taskkill", ["/PID", String(proc.pid), "/T", "/F"], {
11027
11252
  windowsHide: true,
11028
11253
  stdio: "ignore"
11029
11254
  });
@@ -12119,7 +12344,7 @@ var ToolRegistry = class {
12119
12344
  };
12120
12345
 
12121
12346
  // src/mcp/client.ts
12122
- import { spawn as spawn4 } from "child_process";
12347
+ import { spawn as spawn5 } from "child_process";
12123
12348
  var McpClient = class {
12124
12349
  serverId;
12125
12350
  config;
@@ -12157,7 +12382,7 @@ var McpClient = class {
12157
12382
  async connect() {
12158
12383
  const timeout = this.config.timeout ?? MCP_CONNECT_TIMEOUT;
12159
12384
  try {
12160
- this.process = spawn4(this.config.command, this.config.args ?? [], {
12385
+ this.process = spawn5(this.config.command, this.config.args ?? [], {
12161
12386
  stdio: ["pipe", "pipe", "pipe"],
12162
12387
  env: { ...process.env, ...this.config.env },
12163
12388
  // Windows 上 npx 等是 .cmd 脚本,需要 shell 模式
@@ -12253,7 +12478,7 @@ var McpClient = class {
12253
12478
  // 内部方法:JSON-RPC 通信
12254
12479
  // ══════════════════════════════════════════════════════════════════
12255
12480
  sendRequest(method, params) {
12256
- return new Promise((resolve15, reject) => {
12481
+ return new Promise((resolve14, reject) => {
12257
12482
  if (!this.process?.stdin?.writable) {
12258
12483
  return reject(new Error(`MCP server [${this.serverId}] stdin not writable`));
12259
12484
  }
@@ -12277,7 +12502,7 @@ var McpClient = class {
12277
12502
  this.pendingRequests.set(id, {
12278
12503
  resolve: (result) => {
12279
12504
  cleanup();
12280
- resolve15(result);
12505
+ resolve14(result);
12281
12506
  },
12282
12507
  reject: (error) => {
12283
12508
  cleanup();
@@ -12354,13 +12579,13 @@ var McpClient = class {
12354
12579
  }
12355
12580
  /** Promise 超时包装 */
12356
12581
  withTimeout(promise, ms, label) {
12357
- return new Promise((resolve15, reject) => {
12582
+ return new Promise((resolve14, reject) => {
12358
12583
  const timer = setTimeout(() => {
12359
12584
  reject(new Error(`MCP [${this.serverId}] ${label} timed out after ${ms}ms`));
12360
12585
  }, ms);
12361
12586
  promise.then((val) => {
12362
12587
  clearTimeout(timer);
12363
- resolve15(val);
12588
+ resolve14(val);
12364
12589
  }).catch((err) => {
12365
12590
  clearTimeout(timer);
12366
12591
  reject(err);
@@ -12864,33 +13089,33 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
12864
13089
  }
12865
13090
  /** Resolve a pending confirm from client response */
12866
13091
  resolveConfirm(requestId, approved) {
12867
- const resolve15 = this.pendingConfirms.get(requestId);
12868
- if (resolve15) {
13092
+ const resolve14 = this.pendingConfirms.get(requestId);
13093
+ if (resolve14) {
12869
13094
  this.clearPendingTimer(requestId);
12870
13095
  this.pendingConfirms.delete(requestId);
12871
13096
  this.confirming = false;
12872
- resolve15(approved);
13097
+ resolve14(approved);
12873
13098
  }
12874
13099
  }
12875
13100
  /** Resolve a pending batch confirm from client response */
12876
13101
  resolveBatchConfirm(requestId, decision) {
12877
- const resolve15 = this.pendingBatchConfirms.get(requestId);
12878
- if (resolve15) {
13102
+ const resolve14 = this.pendingBatchConfirms.get(requestId);
13103
+ if (resolve14) {
12879
13104
  this.clearPendingTimer(requestId);
12880
13105
  this.pendingBatchConfirms.delete(requestId);
12881
13106
  this.confirming = false;
12882
13107
  if (decision === "all" || decision === "none") {
12883
- resolve15(decision);
13108
+ resolve14(decision);
12884
13109
  } else {
12885
- resolve15(new Set(decision));
13110
+ resolve14(new Set(decision));
12886
13111
  }
12887
13112
  }
12888
13113
  }
12889
13114
  /** Cancel all pending confirms (e.g., on disconnect) */
12890
13115
  cancelAll() {
12891
- for (const resolve15 of this.pendingConfirms.values()) resolve15(false);
13116
+ for (const resolve14 of this.pendingConfirms.values()) resolve14(false);
12892
13117
  this.pendingConfirms.clear();
12893
- for (const resolve15 of this.pendingBatchConfirms.values()) resolve15("none");
13118
+ for (const resolve14 of this.pendingBatchConfirms.values()) resolve14("none");
12894
13119
  this.pendingBatchConfirms.clear();
12895
13120
  this.confirming = false;
12896
13121
  }
@@ -12966,8 +13191,8 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
12966
13191
  diff: this.getDiffPreview(call)
12967
13192
  };
12968
13193
  this.send(msg);
12969
- return new Promise((resolve15) => {
12970
- this.pendingConfirms.set(requestId, resolve15);
13194
+ return new Promise((resolve14) => {
13195
+ this.pendingConfirms.set(requestId, resolve14);
12971
13196
  this.pendingTimers.set(requestId, setTimeout(() => {
12972
13197
  if (this.pendingConfirms.has(requestId)) {
12973
13198
  this.resolveConfirm(requestId, false);
@@ -12991,8 +13216,8 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
12991
13216
  files
12992
13217
  };
12993
13218
  this.send(msg);
12994
- return new Promise((resolve15) => {
12995
- this.pendingBatchConfirms.set(requestId, resolve15);
13219
+ return new Promise((resolve14) => {
13220
+ this.pendingBatchConfirms.set(requestId, resolve14);
12996
13221
  this.pendingTimers.set(requestId, setTimeout(() => {
12997
13222
  if (this.pendingBatchConfirms.has(requestId)) {
12998
13223
  this.resolveBatchConfirm(requestId, "none");
@@ -13661,8 +13886,8 @@ function loadContextFiles(options) {
13661
13886
  }
13662
13887
 
13663
13888
  // src/web/session-handler.ts
13664
- import { mkdirSync as mkdirSync17, createWriteStream as createWriteStream4 } from "fs";
13665
- import { join as join27, dirname as dirname11 } from "path";
13889
+ import { mkdirSync as mkdirSync16, createWriteStream as createWriteStream3 } from "fs";
13890
+ import { join as join26, dirname as dirname10 } from "path";
13666
13891
 
13667
13892
  // src/repl/commands/project-init.ts
13668
13893
  import { existsSync as existsSync26, readFileSync as readFileSync20, readdirSync as readdirSync11, statSync as statSync9 } from "fs";
@@ -14026,7 +14251,7 @@ ${topic}${contextSection}`;
14026
14251
  messages.push({
14027
14252
  role: "assistant",
14028
14253
  content: msg.content,
14029
- timestamp: msg.timestamp
14254
+ timestamp: new Date(msg.timestamp)
14030
14255
  });
14031
14256
  } else {
14032
14257
  const prefix = `[${msg.speakerName} (${msg.speaker})]:`;
@@ -14034,7 +14259,7 @@ ${topic}${contextSection}`;
14034
14259
  role: "user",
14035
14260
  content: `${prefix}
14036
14261
  ${msg.content}`,
14037
- timestamp: msg.timestamp
14262
+ timestamp: new Date(msg.timestamp)
14038
14263
  });
14039
14264
  }
14040
14265
  }
@@ -14384,27 +14609,27 @@ function getPreset(id) {
14384
14609
  // src/hub/resolve-providers.ts
14385
14610
  function resolveRoleProviders(roles, lookup, defaultProvider, defaultModel, available, mix) {
14386
14611
  const warnings = [];
14387
- let pool = null;
14612
+ let pool2 = null;
14388
14613
  if (mix !== void 0 && mix !== false) {
14389
14614
  if (typeof mix === "string" && mix.trim().length > 0) {
14390
14615
  const requested = mix.split(",").map((s) => s.trim()).filter(Boolean);
14391
- pool = [];
14616
+ pool2 = [];
14392
14617
  for (const id of requested) {
14393
- if (lookup.has(id)) pool.push(id);
14618
+ if (lookup.has(id)) pool2.push(id);
14394
14619
  else warnings.push(`--mix: provider "${id}" not configured \u2014 skipped.`);
14395
14620
  }
14396
14621
  } else {
14397
- pool = [...available];
14622
+ pool2 = [...available];
14398
14623
  }
14399
- if (pool.length === 0) {
14624
+ if (pool2.length === 0) {
14400
14625
  warnings.push(`--mix: no usable providers \u2014 all roles fall back to "${defaultProvider}".`);
14401
- pool = [defaultProvider];
14626
+ pool2 = [defaultProvider];
14402
14627
  }
14403
14628
  }
14404
14629
  const providerDefaultModel = (pid) => pid === defaultProvider ? defaultModel : lookup.defaultModelFor(pid) ?? defaultModel;
14405
14630
  const assignments = [];
14406
14631
  const outRoles = roles.map((role, i) => {
14407
- const desired = pool ? pool[i % pool.length] : role.provider;
14632
+ const desired = pool2 ? pool2[i % pool2.length] : role.provider;
14408
14633
  let providerId;
14409
14634
  let fellBack = false;
14410
14635
  if (desired && lookup.has(desired)) {
@@ -14417,7 +14642,7 @@ function resolveRoleProviders(roles, lookup, defaultProvider, defaultModel, avai
14417
14642
  providerId = defaultProvider;
14418
14643
  }
14419
14644
  const explicitProvider = role.provider ?? defaultProvider;
14420
- const modelId = !pool && role.model && explicitProvider === providerId ? role.model : providerDefaultModel(providerId);
14645
+ const modelId = !pool2 && role.model && explicitProvider === providerId ? role.model : providerDefaultModel(providerId);
14421
14646
  assignments.push({ roleId: role.id, roleName: role.name, providerId, modelId, fellBack });
14422
14647
  return { ...role, provider: providerId, model: modelId };
14423
14648
  });
@@ -14429,14 +14654,14 @@ import { join as join21 } from "path";
14429
14654
  function discussionToMessages(state2) {
14430
14655
  const out = [];
14431
14656
  const t0 = state2.messages[0]?.timestamp ?? /* @__PURE__ */ new Date();
14432
- out.push({ role: "user", content: `\u{1F3DB} Topic: ${state2.topic}`, timestamp: t0 });
14657
+ out.push({ role: "user", content: `\u{1F3DB} Topic: ${state2.topic}`, timestamp: new Date(t0) });
14433
14658
  for (const m of state2.messages) {
14434
14659
  if (m.speaker === "system") {
14435
- out.push({ role: "system", content: m.content, timestamp: m.timestamp });
14660
+ out.push({ role: "system", content: m.content, timestamp: new Date(m.timestamp) });
14436
14661
  continue;
14437
14662
  }
14438
14663
  if (m.speaker === "human") {
14439
- out.push({ role: "user", content: `\u{1F9ED} ${m.speakerName}: ${m.content}`, timestamp: m.timestamp });
14664
+ out.push({ role: "user", content: `\u{1F9ED} ${m.speakerName}: ${m.content}`, timestamp: new Date(m.timestamp) });
14440
14665
  continue;
14441
14666
  }
14442
14667
  if (m.passed || !m.content.trim()) continue;
@@ -14446,7 +14671,7 @@ function discussionToMessages(state2) {
14446
14671
  content: `**${m.speakerName}** (${m.speaker})${tag}
14447
14672
 
14448
14673
  ${m.content}`,
14449
- timestamp: m.timestamp
14674
+ timestamp: new Date(m.timestamp)
14450
14675
  });
14451
14676
  }
14452
14677
  if (state2.summary && state2.summary.trim()) {
@@ -14468,1708 +14693,1726 @@ async function persistDiscussion(state2, config, defaultProvider, defaultModel)
14468
14693
 
14469
14694
  // src/web/commands/provider-commands.ts
14470
14695
  async function handleProvider(args, ctx) {
14471
- {
14472
- const id = args[0];
14473
- if (!id) {
14474
- ctx.send({ type: "error", message: "Usage: /provider <id>" });
14475
- return;
14476
- }
14477
- const p = ctx.providers.get(id);
14478
- if (!p) {
14479
- ctx.send({ type: "error", message: `Provider "${id}" not available` });
14480
- return;
14481
- }
14482
- ctx.currentProvider = id;
14483
- ctx.currentModel = p.info.defaultModel;
14484
- ctx.updateContextWindow();
14485
- ctx.send({ type: "info", message: `Switched to provider: ${id} (${ctx.currentModel})` });
14486
- ctx.sendStatus();
14696
+ const id = args[0];
14697
+ if (!id) {
14698
+ ctx.send({ type: "error", message: "Usage: /provider <id>" });
14699
+ return;
14700
+ }
14701
+ const p = ctx.providers.get(id);
14702
+ if (!p) {
14703
+ ctx.send({ type: "error", message: `Provider "${id}" not available` });
14704
+ return;
14487
14705
  }
14706
+ ctx.currentProvider = id;
14707
+ ctx.currentModel = p.info.defaultModel;
14708
+ ctx.updateContextWindow();
14709
+ ctx.send({ type: "info", message: `Switched to provider: ${id} (${ctx.currentModel})` });
14710
+ ctx.sendStatus();
14488
14711
  }
14489
14712
  async function handleModel(args, ctx) {
14490
- {
14491
- const sub = args[0];
14492
- if (!sub) {
14493
- ctx.send({ type: "error", message: "Usage: /model <id>|refresh [provider]|cache [clear [provider]]" });
14494
- return;
14495
- }
14496
- if (sub === "refresh") {
14497
- const providerId = args[1] ?? ctx.currentProvider;
14498
- try {
14499
- const models2 = await ctx.providers.refreshModels(providerId);
14500
- ctx.send({ type: "info", message: `Refreshed ${models2.length} model(s) for ${providerId}.` });
14501
- ctx.sendStatus();
14502
- } catch (err) {
14503
- ctx.send({ type: "error", message: `Model refresh failed: ${err instanceof Error ? err.message : String(err)}` });
14504
- }
14505
- return;
14506
- }
14507
- if (sub === "cache") {
14508
- if ((args[1] ?? "").toLowerCase() === "clear") {
14509
- const providerId = args[2];
14510
- ctx.providers.clearModelCache(providerId);
14511
- ctx.send({ type: "info", message: providerId ? `Cleared model cache for ${providerId}.` : "Cleared model cache." });
14512
- return;
14513
- }
14514
- const rows = ctx.providers.getModelCacheStatus();
14515
- ctx.send({
14516
- type: "info",
14517
- 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")
14518
- });
14519
- return;
14713
+ const sub = args[0];
14714
+ if (!sub) {
14715
+ ctx.send({ type: "error", message: "Usage: /model <id>|refresh [provider]|cache [clear [provider]]" });
14716
+ return;
14717
+ }
14718
+ if (sub === "refresh") {
14719
+ const providerId = args[1] ?? ctx.currentProvider;
14720
+ try {
14721
+ const models2 = await ctx.providers.refreshModels(providerId);
14722
+ ctx.send({ type: "info", message: `Refreshed ${models2.length} model(s) for ${providerId}.` });
14723
+ ctx.sendStatus();
14724
+ } catch (err) {
14725
+ ctx.send({ type: "error", message: `Model refresh failed: ${err instanceof Error ? err.message : String(err)}` });
14520
14726
  }
14521
- const modelId = sub;
14522
- const models = await ctx.providers.listModels(ctx.currentProvider);
14523
- const found = models.find((m) => m.id === modelId);
14524
- if (!found) {
14525
- ctx.send({ type: "error", message: `Model "${modelId}" not found` });
14727
+ return;
14728
+ }
14729
+ if (sub === "cache") {
14730
+ if ((args[1] ?? "").toLowerCase() === "clear") {
14731
+ const providerId = args[2];
14732
+ ctx.providers.clearModelCache(providerId);
14733
+ ctx.send({ type: "info", message: providerId ? `Cleared model cache for ${providerId}.` : "Cleared model cache." });
14526
14734
  return;
14527
14735
  }
14528
- ctx.currentModel = modelId;
14529
- ctx.updateContextWindow();
14530
- ctx.send({ type: "info", message: `Switched to model: ${modelId}` });
14531
- ctx.sendStatus();
14736
+ const rows = ctx.providers.getModelCacheStatus();
14737
+ ctx.send({
14738
+ type: "info",
14739
+ 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")
14740
+ });
14741
+ return;
14742
+ }
14743
+ const modelId = sub;
14744
+ const models = await ctx.providers.listModels(ctx.currentProvider);
14745
+ const found = models.find((m) => m.id === modelId);
14746
+ if (!found) {
14747
+ ctx.send({ type: "error", message: `Model "${modelId}" not found` });
14748
+ return;
14532
14749
  }
14750
+ ctx.currentModel = modelId;
14751
+ ctx.updateContextWindow();
14752
+ ctx.send({ type: "info", message: `Switched to model: ${modelId}` });
14753
+ ctx.sendStatus();
14533
14754
  }
14534
14755
  async function handleThink(args, ctx) {
14535
- {
14536
- const sub = args[0];
14537
- if (sub === "on") ctx.runtimeThinking = true;
14538
- else if (sub === "off") ctx.runtimeThinking = false;
14539
- else ctx.runtimeThinking = !ctx.runtimeThinking;
14540
- ctx.send({ type: "info", message: `Thinking mode: ${ctx.runtimeThinking ? "ON" : "OFF"}` });
14541
- ctx.sendStatus();
14542
- }
14756
+ const sub = args[0];
14757
+ if (sub === "on") ctx.runtimeThinking = true;
14758
+ else if (sub === "off") ctx.runtimeThinking = false;
14759
+ else ctx.runtimeThinking = !ctx.runtimeThinking;
14760
+ ctx.send({ type: "info", message: `Thinking mode: ${ctx.runtimeThinking ? "ON" : "OFF"}` });
14761
+ ctx.sendStatus();
14543
14762
  }
14544
14763
  async function handlePlan(args, ctx) {
14545
- {
14546
- const sub = args[0];
14547
- if (sub === "exit" || sub === "cancel") {
14548
- ctx.planMode = false;
14549
- ctx.send({ type: "info", message: "Plan mode OFF." });
14550
- } else if (sub === "execute") {
14551
- ctx.planMode = false;
14552
- ctx.send({ type: "info", message: "Plan mode OFF. Executing with all tools enabled." });
14553
- } else {
14554
- ctx.planMode = !ctx.planMode;
14555
- ctx.send({ type: "info", message: `Plan mode: ${ctx.planMode ? "ON (read-only tools only)" : "OFF"}` });
14556
- }
14557
- ctx.sendStatus();
14764
+ const sub = args[0];
14765
+ if (sub === "exit" || sub === "cancel") {
14766
+ ctx.planMode = false;
14767
+ ctx.send({ type: "info", message: "Plan mode OFF." });
14768
+ } else if (sub === "execute") {
14769
+ ctx.planMode = false;
14770
+ ctx.send({ type: "info", message: "Plan mode OFF. Executing with all tools enabled." });
14771
+ } else {
14772
+ ctx.planMode = !ctx.planMode;
14773
+ ctx.send({ type: "info", message: `Plan mode: ${ctx.planMode ? "ON (read-only tools only)" : "OFF"}` });
14558
14774
  }
14775
+ ctx.sendStatus();
14559
14776
  }
14560
14777
 
14561
- // src/web/commands/session-commands.ts
14562
- async function handleClear(args, ctx) {
14563
- ctx.saveIfNeeded();
14564
- ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
14565
- ctx.resetWebSessionUsage();
14566
- ctx.send({ type: "info", message: "Conversation cleared." });
14567
- ctx.sendStatus();
14568
- ctx.sendSessionList();
14778
+ // src/web/commands/_shared.ts
14779
+ import { existsSync as existsSync29, readFileSync as readFileSync22, writeFileSync as writeFileSync3, mkdirSync as mkdirSync14, readdirSync as readdirSync13, statSync as statSync12, createWriteStream } from "fs";
14780
+ import { join as join24, resolve as resolve10, dirname as dirname8 } from "path";
14781
+
14782
+ // src/cli/review-prompts.ts
14783
+ function buildReviewPrompt(diff, gitContextStr, detailed) {
14784
+ const level = detailed ? "Please perform a detailed in-depth review covering: security, performance, maintainability, error handling, naming conventions, and code duplication." : "Please perform a concise code review focusing on bugs, security issues, and key improvement suggestions.";
14785
+ return `# Code Review Request
14786
+
14787
+ ${level}
14788
+
14789
+ ## Git Status
14790
+ ${gitContextStr}
14791
+
14792
+ ## Code Changes (diff)
14793
+ \`\`\`diff
14794
+ ${diff}
14795
+ \`\`\`
14796
+
14797
+ ## Output Format
14798
+ Please structure your review as follows:
14799
+ 1. **Overall Assessment**: One-sentence summary of the change quality
14800
+ 2. **Issues** (if any): Each issue with [Severity] file:line \u2014 description + suggested fix
14801
+ 3. **Improvement Suggestions** (if any): Non-critical but recommended optimizations
14802
+ 4. **Highlights** (if any): Good practices worth acknowledging
14803
+
14804
+ Severity levels: \u{1F534} Critical / \u{1F7E1} Warning / \u{1F535} Info`;
14569
14805
  }
14570
- async function handleCompact(args, ctx) {
14571
- await ctx.compactSession(args.join(" ") || void 0);
14806
+ function buildSecurityReviewPrompt(diff, gitContextStr) {
14807
+ return `# Security Vulnerability Review
14808
+
14809
+ Analyze the following code changes **exclusively for security vulnerabilities**.
14810
+
14811
+ ## Categories to check:
14812
+ 1. **Injection** \u2014 SQL, command, path traversal, XSS, template injection
14813
+ 2. **Authentication & Authorization** \u2014 hardcoded credentials, missing auth checks, privilege escalation
14814
+ 3. **Secrets & Sensitive Data** \u2014 API keys, tokens, passwords in code, logging sensitive data
14815
+ 4. **Input Validation** \u2014 missing validation, unsafe deserialization, buffer issues
14816
+ 5. **Cryptography** \u2014 weak algorithms, improper random, hardcoded IVs/salts
14817
+ 6. **Dependencies** \u2014 known vulnerable packages, unsafe dynamic imports
14818
+ 7. **File System** \u2014 path traversal, unsafe file permissions, symlink attacks
14819
+ 8. **Network** \u2014 SSRF, insecure protocols, missing TLS validation
14820
+
14821
+ ## Git Status
14822
+ ${gitContextStr}
14823
+
14824
+ ## Code Changes (diff)
14825
+ \`\`\`diff
14826
+ ${diff}
14827
+ \`\`\`
14828
+
14829
+ ## Output Format
14830
+ For each finding:
14831
+ - **Severity**: \u{1F534} CRITICAL / \u{1F7E0} HIGH / \u{1F7E1} MEDIUM / \u{1F535} LOW / \u2139\uFE0F INFO
14832
+ - **Category**: (from list above)
14833
+ - **File & location**: file:line
14834
+ - **Description**: what the vulnerability is and how it could be exploited
14835
+ - **Recommended fix**: specific code change to resolve
14836
+
14837
+ If no security issues found, state "\u2705 No security vulnerabilities detected" with a brief explanation of what was checked.`;
14572
14838
  }
14573
- async function handleStatus(args, ctx) {
14574
- {
14575
- const session = ctx.sessions.current;
14576
- const cacheRead = ctx.sessionTokenUsage.cacheReadTokens;
14577
- const cacheCreate = ctx.sessionTokenUsage.cacheCreationTokens;
14578
- const cost = computeCost(ctx.currentProvider, ctx.currentModel, ctx.sessionTokenUsage);
14579
- const cacheLine = cacheRead > 0 || cacheCreate > 0 ? `
14580
- Cache: write=${cacheCreate} read=${cacheRead}` : "";
14581
- const costLine = cost != null ? `
14582
- Cost: ${formatCost(cost)}` : "";
14583
- ctx.send({
14584
- type: "info",
14585
- message: `Provider: ${ctx.currentProvider}
14586
- Model: ${ctx.currentModel}
14587
- Session: ${session?.id ?? "none"} (${session?.messages.length ?? 0} messages)
14588
- Tokens: in=${ctx.sessionTokenUsage.inputTokens} out=${ctx.sessionTokenUsage.outputTokens}${cacheLine}${costLine}`
14589
- });
14590
- }
14839
+
14840
+ // src/core/git-diff.ts
14841
+ import { execFileSync as execFileSync3 } from "child_process";
14842
+ function readGitDiff(options = {}) {
14843
+ const args = ["diff"];
14844
+ if (options.staged) args.push("--staged");
14845
+ return execFileSync3("git", args, {
14846
+ encoding: "utf-8",
14847
+ timeout: options.timeoutMs ?? 1e4
14848
+ }).trim();
14591
14849
  }
14592
- async function handleSession(args, ctx) {
14593
- {
14594
- const sub = args[0];
14595
- if (sub === "new") {
14596
- ctx.saveIfNeeded();
14597
- const created = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
14598
- ctx.unsavedSessions.set(created.id, created);
14599
- ctx.resetWebSessionUsage();
14600
- ctx.send({ type: "info", message: "New session created." });
14601
- ctx.sendStatus();
14602
- ctx.sendSessionList();
14603
- } else if (sub === "load" && args[1]) {
14604
- const targetId = args[1];
14605
- ctx.saveIfNeeded();
14606
- const cachedExact = ctx.unsavedSessions.get(targetId);
14607
- const cached = cachedExact ?? [...ctx.unsavedSessions.values()].find((s) => s.id.startsWith(targetId));
14608
- if (cached) {
14609
- ctx.sessions.setCurrent(cached);
14610
- ctx.resetWebSessionUsage();
14611
- ctx.send({
14612
- type: "info",
14613
- message: `Loaded session: ${cached.id.slice(0, 8)} "${cached.title ?? ""}" (${cached.messages.length} messages)`
14614
- });
14615
- ctx.sendSessionMessages();
14616
- ctx.sendStatus();
14617
- ctx.sendSessionList();
14618
- }
14619
- const list = ctx.sessions.listSessions();
14620
- const found = list.find((s) => s.id.startsWith(targetId));
14621
- if (found) {
14622
- ctx.sessions.loadSession(found.id);
14623
- ctx.resetWebSessionUsage();
14624
- ctx.send({ type: "info", message: `Loaded session: ${found.id.slice(0, 8)} "${found.title ?? ""}" (${found.messageCount} messages)` });
14625
- const loadedSession = ctx.sessions.current;
14626
- if (loadedSession && loadedSession.messages.length > 0) {
14627
- const lastMsg = loadedSession.messages[loadedSession.messages.length - 1];
14628
- const isIncomplete = lastMsg.role === "tool" || lastMsg.role === "assistant" && lastMsg.toolCalls && lastMsg.toolCalls.length > 0;
14629
- if (isIncomplete) {
14630
- ctx.send({
14631
- type: "info",
14632
- 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.'
14633
- });
14634
- }
14635
- }
14636
- ctx.sendSessionMessages();
14637
- ctx.sendStatus();
14638
- ctx.sendSessionList();
14639
- } else {
14640
- const recreated = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
14641
- ctx.unsavedSessions.set(recreated.id, recreated);
14642
- ctx.resetWebSessionUsage();
14643
- ctx.send({
14644
- type: "info",
14645
- message: `Previous session (${targetId.slice(0, 8)}) is no longer available \u2014 started a new one.`
14646
- });
14647
- ctx.sendStatus();
14648
- ctx.sendSessionList();
14649
- }
14650
- } else if (sub === "list") {
14651
- ctx.sendSessionList();
14652
- } else if (sub === "delete" && args[1]) {
14653
- const targetId = args[1];
14654
- const cachedKey = ctx.unsavedSessions.has(targetId) ? targetId : [...ctx.unsavedSessions.keys()].find((k) => k.startsWith(targetId));
14655
- if (cachedKey) {
14656
- ctx.unsavedSessions.delete(cachedKey);
14657
- if (ctx.sessions.current?.id === cachedKey) {
14658
- ctx.sessions.clearCurrent();
14659
- }
14660
- ctx.send({ type: "info", message: `Deleted session: ${cachedKey.slice(0, 8)}` });
14661
- ctx.sendSessionList();
14662
- }
14663
- const list = ctx.sessions.listSessions();
14664
- const found = list.find((s) => s.id.startsWith(targetId));
14665
- if (found) {
14666
- ctx.sessions.deleteSession(found.id);
14667
- ctx.send({ type: "info", message: `Deleted session: ${found.id.slice(0, 8)}` });
14668
- ctx.sendSessionList();
14669
- } else {
14670
- ctx.send({ type: "error", message: `Session not found: ${targetId}` });
14671
- }
14672
- } else if (sub === "batch-delete") {
14673
- const ids = args.slice(1).filter(Boolean);
14674
- if (ids.length === 0) {
14675
- ctx.send({ type: "error", message: "No session IDs provided." });
14676
- }
14677
- const list = ctx.sessions.listSessions();
14678
- let deleted = 0;
14679
- for (const targetId of ids) {
14680
- const cachedKey = ctx.unsavedSessions.has(targetId) ? targetId : [...ctx.unsavedSessions.keys()].find((k) => k.startsWith(targetId));
14681
- if (cachedKey) {
14682
- ctx.unsavedSessions.delete(cachedKey);
14683
- deleted++;
14684
- continue;
14685
- }
14686
- const found = list.find((s) => s.id.startsWith(targetId));
14687
- if (found) {
14688
- ctx.sessions.deleteSession(found.id);
14689
- deleted++;
14690
- }
14691
- }
14692
- ctx.send({ type: "info", message: `Deleted ${deleted} session(s).` });
14693
- ctx.sendSessionList();
14694
- } else if (sub === "rename" && args[1] && args.length >= 3) {
14695
- const targetId = args[1];
14696
- const newTitle = args.slice(2).join(" ").trim();
14697
- if (!newTitle) {
14698
- ctx.send({ type: "error", message: "Title cannot be empty." });
14699
- }
14700
- const cachedKey = ctx.unsavedSessions.has(targetId) ? targetId : [...ctx.unsavedSessions.keys()].find((k) => k.startsWith(targetId));
14701
- if (cachedKey) {
14702
- const session = ctx.unsavedSessions.get(cachedKey);
14703
- session.title = newTitle;
14704
- ctx.send({ type: "info", message: `Renamed session: "${newTitle}"` });
14705
- ctx.sendSessionList();
14706
- }
14707
- const list = ctx.sessions.listSessions();
14708
- const found = list.find((s) => s.id.startsWith(targetId));
14709
- if (found) {
14710
- const session = ctx.sessions.loadSession(found.id);
14711
- session.title = newTitle;
14712
- ctx.sessions.save();
14713
- ctx.send({ type: "info", message: `Renamed session: "${newTitle}"` });
14714
- ctx.sendSessionList();
14715
- } else {
14716
- ctx.send({ type: "error", message: `Session not found: ${targetId}` });
14717
- }
14718
- } else {
14719
- ctx.send({ type: "info", message: "Usage: /session new | list | load <id> | delete <id> | rename <id> <title>" });
14850
+
14851
+ // src/diagnostics/doctor-report.ts
14852
+ import { existsSync as existsSync28, statSync as statSync11 } from "fs";
14853
+ import { join as join23 } from "path";
14854
+ import { arch as arch2, platform as platform7, release as release2 } from "os";
14855
+
14856
+ // src/diagnostics/crash-log.ts
14857
+ import {
14858
+ existsSync as existsSync27,
14859
+ mkdirSync as mkdirSync13,
14860
+ readdirSync as readdirSync12,
14861
+ readFileSync as readFileSync21,
14862
+ statSync as statSync10,
14863
+ unlinkSync as unlinkSync6,
14864
+ writeFileSync as writeFileSync2
14865
+ } from "fs";
14866
+ import { join as join22 } from "path";
14867
+ import { homedir as homedir8, platform as platform6, release, arch } from "os";
14868
+ var LOGS_DIR_NAME = "logs";
14869
+ var CRASH_LOG_PREFIX = "crash-";
14870
+ function getLogsDir(configDir) {
14871
+ const base = configDir ?? join22(homedir8(), CONFIG_DIR_NAME);
14872
+ return join22(base, LOGS_DIR_NAME);
14873
+ }
14874
+ function truncate(s, n) {
14875
+ return s.length <= n ? s : s.slice(0, n) + "\u2026";
14876
+ }
14877
+ function listRecentCrashes(limit = 10, configDir) {
14878
+ const dir = getLogsDir(configDir);
14879
+ if (!existsSync27(dir)) return [];
14880
+ const files = [];
14881
+ let names;
14882
+ try {
14883
+ names = readdirSync12(dir);
14884
+ } catch {
14885
+ return [];
14886
+ }
14887
+ for (const name of names) {
14888
+ if (!name.startsWith(CRASH_LOG_PREFIX) || !name.endsWith(".log")) continue;
14889
+ const full = join22(dir, name);
14890
+ try {
14891
+ const st = statSync10(full);
14892
+ const head = readFileSync21(full, "utf-8").split("\n").slice(0, 20);
14893
+ const tsLine = head.find((l) => l.startsWith("timestamp:"));
14894
+ const kindLine = head.find((l) => l.startsWith("kind:"));
14895
+ const msgIdx = head.findIndex((l) => l.trim() === "## message");
14896
+ const message = msgIdx >= 0 && head[msgIdx + 1] ? head[msgIdx + 1] : "";
14897
+ files.push({
14898
+ file: full,
14899
+ timestamp: tsLine ? tsLine.replace("timestamp:", "").trim() : st.mtime.toISOString(),
14900
+ kind: kindLine ? kindLine.replace("kind:", "").trim() : "unknown",
14901
+ message: truncate(message, 200),
14902
+ sizeBytes: st.size
14903
+ });
14904
+ } catch {
14720
14905
  }
14721
14906
  }
14907
+ files.sort((a, b) => a.timestamp < b.timestamp ? 1 : -1);
14908
+ return files.slice(0, limit);
14722
14909
  }
14723
- async function handleExport(args, ctx) {
14724
- {
14725
- const format = args[0] === "json" ? "json" : "md";
14726
- ctx.exportConversation(format);
14910
+ function getConfigDirUsage(configDir) {
14911
+ const base = configDir ?? join22(homedir8(), CONFIG_DIR_NAME);
14912
+ if (!existsSync27(base)) return { totalBytes: 0, entries: [] };
14913
+ const entries = [];
14914
+ let total = 0;
14915
+ let names;
14916
+ try {
14917
+ names = readdirSync12(base);
14918
+ } catch {
14919
+ return { totalBytes: 0, entries: [] };
14920
+ }
14921
+ for (const name of names) {
14922
+ const full = join22(base, name);
14923
+ const bytes = dirSize(full);
14924
+ entries.push({ name, bytes });
14925
+ total += bytes;
14727
14926
  }
14927
+ entries.sort((a, b) => b.bytes - a.bytes);
14928
+ return { totalBytes: total, entries };
14728
14929
  }
14729
- async function handleCost(args, ctx) {
14730
- {
14731
- const u = ctx.sessionTokenUsage;
14732
- const total = u.inputTokens + u.outputTokens + u.cacheCreationTokens + u.cacheReadTokens;
14733
- const cost = computeCost(ctx.currentProvider, ctx.currentModel, u);
14734
- let savings = null;
14735
- if (cost != null && u.cacheReadTokens > 0) {
14736
- const withoutCache = computeCost(ctx.currentProvider, ctx.currentModel, {
14737
- inputTokens: u.inputTokens + u.cacheReadTokens,
14738
- outputTokens: u.outputTokens,
14739
- cacheCreationTokens: u.cacheCreationTokens,
14740
- cacheReadTokens: 0
14741
- });
14742
- if (withoutCache != null) savings = withoutCache - cost;
14743
- }
14744
- const lines = [
14745
- "\u{1F4B0} Session Cost & Token Usage",
14746
- ` Provider : ${ctx.currentProvider}`,
14747
- ` Model : ${ctx.currentModel}`,
14748
- ` Input (uncached): ${u.inputTokens.toLocaleString()}`,
14749
- ` Output : ${u.outputTokens.toLocaleString()}`
14750
- ];
14751
- if (u.cacheCreationTokens > 0) lines.push(` Cache write : ${u.cacheCreationTokens.toLocaleString()}`);
14752
- if (u.cacheReadTokens > 0) {
14753
- const pct = Math.round(u.cacheReadTokens / (u.cacheReadTokens + u.inputTokens) * 100);
14754
- lines.push(` Cache read : ${u.cacheReadTokens.toLocaleString()} (${pct}% hit rate)`);
14755
- }
14756
- lines.push(` Total tokens : ${total.toLocaleString()}`);
14757
- if (cost != null) {
14758
- lines.push(` Cost : ${formatCost(cost)}`);
14759
- if (savings != null && savings > 0) {
14760
- lines.push(` Cache savings : -${formatCost(savings)} (vs no cache)`);
14761
- }
14762
- } else {
14763
- lines.push(` Cost : \u2014 (pricing unknown for this model)`);
14930
+ function dirSize(path3) {
14931
+ try {
14932
+ const st = statSync10(path3);
14933
+ if (st.isFile()) return st.size;
14934
+ if (!st.isDirectory()) return 0;
14935
+ let sum = 0;
14936
+ for (const name of readdirSync12(path3)) {
14937
+ sum += dirSize(join22(path3, name));
14764
14938
  }
14765
- ctx.send({ type: "info", message: lines.join("\n") });
14939
+ return sum;
14940
+ } catch {
14941
+ return 0;
14766
14942
  }
14767
14943
  }
14768
- async function handleCheckpoint(args, ctx) {
14769
- {
14770
- const session = ctx.sessions.current;
14771
- if (!session) {
14772
- ctx.send({ type: "error", message: "No active session." });
14773
- }
14774
- const sub = args[0]?.toLowerCase();
14775
- if (!sub || sub === "list") {
14776
- const cps = session.checkpoints;
14777
- if (cps.length === 0) {
14778
- ctx.send({ type: "info", message: "No checkpoints. Use /checkpoint save <name> to create one." });
14779
- } else {
14780
- const lines = [`\u{1F4CC} Checkpoints (${cps.length}):`, ""];
14781
- for (const cp of cps) {
14782
- const ts = cp.timestamp.toLocaleString();
14783
- lines.push(` ${cp.name.padEnd(20)} msg #${cp.messageIndex} ${ts}`);
14784
- }
14785
- ctx.send({ type: "info", message: lines.join("\n") });
14786
- }
14787
- }
14788
- const cpName = args.slice(1).join(" ").trim();
14789
- if (sub === "save") {
14790
- if (!cpName) {
14791
- ctx.send({ type: "error", message: "Usage: /checkpoint save <name>" });
14792
- }
14793
- session.createCheckpoint(cpName);
14794
- ctx.send({ type: "info", message: `\u2713 Checkpoint "${cpName}" saved at message #${session.messages.length}` });
14795
- } else if (sub === "restore") {
14796
- if (!cpName) {
14797
- ctx.send({ type: "error", message: "Usage: /checkpoint restore <name>" });
14798
- }
14799
- const ok = session.restoreCheckpoint(cpName);
14800
- if (ok) {
14801
- ctx.send({ type: "info", message: `\u2713 Restored to checkpoint "${cpName}" (${session.messages.length} messages)` });
14802
- ctx.sendSessionMessages();
14803
- ctx.sendStatus();
14804
- } else {
14805
- ctx.send({ type: "error", message: `Checkpoint "${cpName}" not found.` });
14806
- }
14807
- } else if (sub === "delete") {
14808
- if (!cpName) {
14809
- ctx.send({ type: "error", message: "Usage: /checkpoint delete <name>" });
14810
- }
14811
- const ok = session.deleteCheckpoint(cpName);
14812
- if (ok) {
14813
- ctx.send({ type: "info", message: `\u2713 Deleted checkpoint "${cpName}"` });
14814
- } else {
14815
- ctx.send({ type: "error", message: `Checkpoint "${cpName}" not found.` });
14816
- }
14817
- } else {
14818
- ctx.send({ type: "error", message: `Unknown subcommand: ${sub}. Use save/restore/list/delete.` });
14944
+
14945
+ // src/diagnostics/doctor-report.ts
14946
+ function checkFile(label, path3) {
14947
+ const exists = existsSync28(path3);
14948
+ let sizeBytes = null;
14949
+ if (exists) {
14950
+ try {
14951
+ sizeBytes = statSync11(path3).size;
14952
+ } catch {
14819
14953
  }
14820
14954
  }
14955
+ return { label, path: path3, exists, sizeBytes };
14821
14956
  }
14822
- async function handleBranch(args, ctx) {
14823
- {
14824
- const session = ctx.sessions.current;
14825
- if (!session) {
14826
- ctx.send({ type: "error", message: "No active session." });
14827
- }
14828
- const sub = args[0]?.toLowerCase();
14829
- const resolve15 = (ref) => {
14830
- const r = session.resolveBranchRef(ref);
14831
- if (r.ok) return r.id;
14832
- if (r.reason === "ambiguous") {
14833
- const list = r.matches.map((m) => `${m.id} (${m.title})`).join(", ");
14834
- ctx.send({ type: "error", message: `Ambiguous branch reference "${ref}". Matches: ${list}. Use the 6-char id.` });
14835
- } else {
14836
- ctx.send({ type: "error", message: `Branch "${ref}" not found. Use /branch list to see all branches.` });
14837
- }
14838
- return null;
14839
- };
14840
- if (!sub || sub === "list") {
14841
- const branches = session.listBranches();
14842
- const lines = [`\u{1F33F} Branches (${branches.length}):`, ""];
14843
- for (const b of branches) {
14844
- const marker = b.id === session.activeBranchId ? "\u25CF " : " ";
14845
- const count = b.id === session.activeBranchId ? session.messages.length : session.getBranchMessages(b.id)?.length ?? 0;
14846
- const parent = b.parentBranchId ? ` \u2190 ${b.parentBranchId}@${b.parentMessageIndex}` : "";
14847
- lines.push(` ${marker}${b.id.padEnd(10)} ${b.title.padEnd(20)} (${count} msgs)${parent}`);
14848
- }
14849
- lines.push("");
14850
- 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>");
14851
- ctx.send({ type: "info", message: lines.join("\n") });
14852
- }
14853
- if (sub === "new") {
14854
- const idxArg = args[1];
14855
- if (!idxArg) {
14856
- ctx.send({ type: "error", message: "Usage: /branch new <msgIndex> [title]" });
14857
- }
14858
- const fromIndex = parseInt(idxArg, 10);
14859
- if (isNaN(fromIndex) || fromIndex < 0 || fromIndex > session.messages.length) {
14860
- ctx.send({ type: "error", message: `Invalid msgIndex: ${idxArg}. Range: 0-${session.messages.length}` });
14861
- }
14862
- const title = args.slice(2).join(" ").trim() || void 0;
14863
- try {
14864
- const newId = session.createBranch(fromIndex, title);
14865
- await ctx.sessions.save();
14866
- ctx.send({ type: "info", message: `\u2713 Created branch "${newId}" from message #${fromIndex}. Now active (${session.messages.length} messages).` });
14867
- ctx.sendSessionMessages();
14868
- ctx.sendStatus();
14869
- } catch (err) {
14870
- ctx.send({ type: "error", message: err.message });
14871
- }
14872
- }
14873
- if (sub === "switch") {
14874
- const ref = args[1];
14875
- if (!ref) {
14876
- ctx.send({ type: "error", message: "Usage: /branch switch <id|title>" });
14877
- }
14878
- const id = resolve15(ref);
14879
- if (!id) {
14880
- }
14881
- const ok = session.switchBranch(id);
14882
- if (ok) {
14883
- await ctx.sessions.save();
14884
- const b = session.getActiveBranch();
14885
- ctx.send({ type: "info", message: `\u2713 Switched to branch "${b.id}" \u2014 ${b.title} (${session.messages.length} messages)` });
14886
- ctx.sendSessionMessages();
14887
- ctx.sendStatus();
14888
- } else {
14889
- ctx.send({ type: "error", message: `Cannot switch to "${id}" (already active).` });
14890
- }
14891
- }
14892
- if (sub === "delete") {
14893
- const ref = args[1];
14894
- if (!ref) {
14895
- ctx.send({ type: "error", message: "Usage: /branch delete <id|title>" });
14896
- }
14897
- const id = resolve15(ref);
14898
- if (!id) {
14899
- }
14900
- const ok = session.deleteBranch(id);
14901
- if (ok) {
14902
- await ctx.sessions.save();
14903
- ctx.send({ type: "info", message: `\u2713 Deleted branch "${id}"` });
14904
- ctx.sendStatus();
14905
- } else {
14906
- ctx.send({ type: "error", message: `Cannot delete "${id}" (active, or last remaining branch).` });
14907
- }
14908
- }
14909
- if (sub === "rename") {
14910
- const ref = args[1];
14911
- const title = args.slice(2).join(" ").trim();
14912
- if (!ref || !title) {
14913
- ctx.send({ type: "error", message: "Usage: /branch rename <id|title> <new title>" });
14914
- }
14915
- const id = resolve15(ref);
14916
- if (!id) {
14917
- }
14918
- const ok = session.renameBranch(id, title);
14919
- if (ok) {
14920
- await ctx.sessions.save();
14921
- ctx.send({ type: "info", message: `\u2713 Renamed branch "${id}" \u2192 "${title}"` });
14922
- ctx.sendStatus();
14923
- } else {
14924
- ctx.send({ type: "error", message: `Branch "${id}" not found.` });
14925
- }
14957
+ function buildDoctorReport(options) {
14958
+ const config = options.config;
14959
+ const cwd = options.cwd ?? process.cwd();
14960
+ const configDir = config.getConfigDir();
14961
+ const projectRoot = getGitRoot(cwd) ?? cwd;
14962
+ const contextCfg = config.get("context");
14963
+ const contextSetting = config.get("contextFile");
14964
+ const context = loadContextFiles({
14965
+ cwd,
14966
+ configDir,
14967
+ projectRoot,
14968
+ setting: contextSetting,
14969
+ maxBytes: contextCfg.projectDocMaxBytes,
14970
+ fallbackFilenames: contextCfg.projectDocFallbackFilenames
14971
+ });
14972
+ const hooksConfig = config.get("hooks");
14973
+ const hooks = listHooks(hooksConfig, configDir);
14974
+ const pendingHooks = getPendingHookTrust(hooksConfig, configDir);
14975
+ const plugins = listInstalledPlugins(configDir);
14976
+ const activeAssets = getActivePluginAssets(configDir);
14977
+ const network = config.get("networkPolicy");
14978
+ const mcpServers = config.get("mcpServers");
14979
+ const allStats = getStatsSnapshot();
14980
+ return {
14981
+ version: VERSION,
14982
+ npmCheck: options.npmCheck,
14983
+ node: process.version,
14984
+ platform: `${platform7()} ${release2()} (${arch2()})`,
14985
+ cwd,
14986
+ projectRoot,
14987
+ configDir,
14988
+ providers: options.providers.listAll().map((p) => ({ id: p.id, displayName: p.displayName, configured: p.configured })),
14989
+ files: [
14990
+ checkFile("config.json", join23(configDir, "config.json")),
14991
+ checkFile("memory.md", join23(configDir, MEMORY_FILE_NAME)),
14992
+ checkFile("memory.jsonl", join23(configDir, "memory.jsonl")),
14993
+ checkFile("dev-state.md", join23(configDir, DEV_STATE_FILE_NAME)),
14994
+ checkFile("hooks-trust.json", join23(configDir, "hooks-trust.json")),
14995
+ checkFile("plugin-state.json", join23(configDir, "plugins", "plugin-state.json"))
14996
+ ],
14997
+ context: {
14998
+ enabled: contextSetting !== false,
14999
+ setting: contextSetting,
15000
+ candidates: context.candidates,
15001
+ maxBytes: context.maxBytes,
15002
+ layers: context.layers.map((l) => ({ level: l.level, fileName: l.fileName, displayPath: l.displayPath, bytes: l.byteCount, chars: l.charCount, truncated: l.truncated })),
15003
+ skipped: context.skipped.map((s) => ({ level: s.level, fileName: s.fileName, displayPath: s.displayPath, reason: s.reason, message: s.message }))
15004
+ },
15005
+ permissions: {
15006
+ defaultProfile: config.get("defaultPermissionProfile"),
15007
+ allowedProfiles: config.get("allowedPermissionProfiles"),
15008
+ customProfileCount: Object.keys(config.get("permissionProfiles")).length,
15009
+ legacyDefaultAction: config.get("defaultPermission"),
15010
+ ruleCount: config.get("permissionRules").length
15011
+ },
15012
+ networkPolicy: {
15013
+ enabled: network.enabled,
15014
+ defaultAction: network.defaultAction,
15015
+ allowDomains: network.allowDomains.length,
15016
+ denyDomains: network.denyDomains.length,
15017
+ allowPorts: network.allowPorts.length,
15018
+ denyPorts: network.denyPorts.length,
15019
+ allowPrivateNetwork: network.allowPrivateNetwork,
15020
+ toolOverrides: Object.fromEntries(Object.entries(network.tools).filter(([, v]) => v !== void 0))
15021
+ },
15022
+ hooks: {
15023
+ enabled: hooksConfig?.enabled !== false,
15024
+ total: hooks.length,
15025
+ project: hooks.filter((h) => h.source === "project").length,
15026
+ pendingTrust: pendingHooks.length,
15027
+ disabled: hooks.filter((h) => h.disabled).length
15028
+ },
15029
+ plugins: {
15030
+ root: join23(configDir, "plugins"),
15031
+ installed: plugins.length,
15032
+ active: activeAssets.plugins.length,
15033
+ invalid: plugins.filter((p) => !p.valid).length,
15034
+ untrusted: plugins.filter((p) => p.valid && !p.trusted).length,
15035
+ disabled: plugins.filter((p) => p.valid && !p.enabled).length,
15036
+ mcpServers: Object.keys(activeAssets.mcpServers).length,
15037
+ skillDirs: activeAssets.skillDirs.length,
15038
+ commandDirs: activeAssets.commandDirs.length,
15039
+ agentDirs: activeAssets.agentDirs.length,
15040
+ entries: plugins.map((p) => ({ name: p.name, version: p.manifest.version, enabled: p.enabled, trusted: p.trusted, valid: p.valid, error: p.error }))
15041
+ },
15042
+ mcp: {
15043
+ enabled: config.get("mcpEnabled"),
15044
+ configured: Object.keys(mcpServers).length,
15045
+ projectConfigExists: existsSync28(join23(projectRoot, MCP_PROJECT_CONFIG_NAME)),
15046
+ statuses: options.mcpStatuses ?? []
15047
+ },
15048
+ recentCrashes: listRecentCrashes(5, configDir),
15049
+ diskUsage: getConfigDirUsage(configDir),
15050
+ toolStats: {
15051
+ totalCalls: allStats.reduce((a, b) => a + b.calls, 0),
15052
+ totalFailures: allStats.reduce((a, b) => a + b.failures, 0),
15053
+ topUsed: getTopUsedTools(5),
15054
+ topFailing: getTopFailingTools(5)
14926
15055
  }
14927
- if (sub === "diff") {
14928
- const ref = args[1];
14929
- if (!ref) {
14930
- ctx.send({ type: "error", message: "Usage: /branch diff <id|title>" });
14931
- }
14932
- const id = resolve15(ref);
14933
- if (!id) {
14934
- }
14935
- const d = session.diffBranches(id);
14936
- if (!d) {
14937
- ctx.send({ type: "error", message: `Branch "${id}" not found.` });
14938
- }
14939
- const preview = (m) => {
14940
- const text = typeof m.content === "string" ? m.content : "[complex content]";
14941
- return text.replace(/\s+/g, " ").trim().slice(0, 70);
14942
- };
14943
- const lines = [
14944
- `\u{1F500} diff ${id} (source) \u2194 ${session.activeBranchId} (active)`,
14945
- `Common prefix: ${d.commonPrefix} message(s)`
14946
- ];
14947
- if (d.sourceOnly.length === 0 && d.targetOnly.length === 0) {
14948
- lines.push("Branches are identical.");
14949
- } else {
14950
- if (d.sourceOnly.length > 0) {
14951
- lines.push("", `Only in ${id} (${d.sourceOnly.length}):`);
14952
- d.sourceOnly.forEach(
14953
- (m, i) => lines.push(` [${d.commonPrefix + i}] ${m.role}: ${preview(m)}`)
14954
- );
14955
- }
14956
- if (d.targetOnly.length > 0) {
14957
- lines.push("", `Only in ${session.activeBranchId} (active) (${d.targetOnly.length}):`);
14958
- d.targetOnly.forEach(
14959
- (m, i) => lines.push(` [${d.commonPrefix + i}] ${m.role}: ${preview(m)}`)
14960
- );
14961
- }
14962
- lines.push("", `Use /branch cherry-pick ${id} <index> to copy a message into the active branch.`);
14963
- }
14964
- ctx.send({ type: "info", message: lines.join("\n") });
15056
+ };
15057
+ }
15058
+
15059
+ // src/diagnostics/doctor-cli.ts
15060
+ function formatBytes(n) {
15061
+ if (n < 1024) return `${n} B`;
15062
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
15063
+ if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;
15064
+ return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
15065
+ }
15066
+ function formatDoctorReport(report, ansi = true) {
15067
+ const wrap = (code, s) => ansi ? `${code}${s}\x1B[0m` : s;
15068
+ const B = (s) => wrap("\x1B[1m", s);
15069
+ const D = (s) => wrap("\x1B[2m", s);
15070
+ const G = (s) => wrap("\x1B[32m", s);
15071
+ const Y = (s) => wrap("\x1B[33m", s);
15072
+ const R = (s) => wrap("\x1B[31m", s);
15073
+ const out = [];
15074
+ out.push("");
15075
+ out.push(B("\u{1FA7A} AI-CLI Health Check"));
15076
+ out.push("");
15077
+ const nc = report.npmCheck;
15078
+ let versionLine = ` version: ${report.version}`;
15079
+ if (nc?.status === "up-to-date") versionLine += ` ${D("(latest on npm)")}`;
15080
+ else if (nc?.status === "outdated" && nc.latest) versionLine += ` ${Y(`\u2192 ${nc.latest} available`)} ${D("(npm i -g jinzd-ai-cli@latest)")}`;
15081
+ else if (nc?.status === "skipped") versionLine += ` ${D(`(npm check skipped: ${nc.reason ?? "unknown"})`)}`;
15082
+ out.push(versionLine);
15083
+ out.push(` node: ${report.node}`);
15084
+ out.push(` platform: ${report.platform}`);
15085
+ out.push(` cwd: ${report.cwd}`);
15086
+ out.push(` project: ${report.projectRoot}`);
15087
+ out.push(` config: ${report.configDir}`);
15088
+ out.push("");
15089
+ out.push(B("API Keys:"));
15090
+ for (const p of report.providers) {
15091
+ out.push(` ${p.configured ? G("\u2713") : D("\u25CB")} ${p.id.padEnd(14)} ${p.configured ? G("configured") : D("not configured")}`);
15092
+ }
15093
+ out.push("");
15094
+ out.push(B("Context Files:"));
15095
+ out.push(` status: ${report.context.enabled ? G("enabled") : D("disabled")} \xB7 max ${formatBytes(report.context.maxBytes)}`);
15096
+ if (report.context.layers.length === 0) out.push(` ${D("(no context files loaded)")}`);
15097
+ for (const l of report.context.layers) {
15098
+ out.push(` ${G("\u2713")} ${l.level.padEnd(7)} ${l.displayPath} ${D(`(${formatBytes(l.bytes)}${l.truncated ? ", truncated" : ""})`)}`);
15099
+ }
15100
+ if (report.context.skipped.length > 0) {
15101
+ out.push(` ${D("skipped:")}`);
15102
+ for (const s of report.context.skipped.slice(0, 5)) {
15103
+ out.push(` ${Y("!")} ${s.level}:${s.fileName} ${s.reason}${s.message ? ` \xB7 ${s.message}` : ""}`);
14965
15104
  }
14966
- if (sub === "cherry-pick") {
14967
- const ref = args[1];
14968
- const idxStr = args[2];
14969
- if (!ref || !idxStr) {
14970
- ctx.send({ type: "error", message: "Usage: /branch cherry-pick <source-id|title> <msg-index>" });
14971
- }
14972
- const id = resolve15(ref);
14973
- if (!id) {
14974
- }
14975
- const idx = parseInt(idxStr, 10);
14976
- if (Number.isNaN(idx)) {
14977
- ctx.send({ type: "error", message: `Invalid message index: "${idxStr}"` });
14978
- }
14979
- const picked = session.cherryPickMessage(id, idx);
14980
- if (!picked) {
14981
- ctx.send({ type: "error", message: `Cherry-pick failed \u2014 index ${idx} out of range for branch "${id}".` });
14982
- }
14983
- await ctx.sessions.save();
14984
- const preview = typeof picked.content === "string" ? picked.content.replace(/\s+/g, " ").trim().slice(0, 60) : "[complex content]";
14985
- ctx.send({ type: "info", message: `\u2713 Cherry-picked ${picked.role} message from "${id}"[${idx}] \u2192 active branch (${session.messages.length} msgs): ${preview}` });
14986
- ctx.sendSessionMessages();
14987
- ctx.sendStatus();
15105
+ }
15106
+ out.push("");
15107
+ out.push(B("Permissions / Network:"));
15108
+ out.push(` profile: ${report.permissions.defaultProfile} \xB7 allowed ${report.permissions.allowedProfiles.join(", ")} \xB7 rules ${report.permissions.ruleCount} \xB7 custom profiles ${report.permissions.customProfileCount}`);
15109
+ const np = report.networkPolicy;
15110
+ out.push(` networkPolicy: ${np.enabled ? G("enabled") : D("disabled")} \xB7 default ${np.defaultAction} \xB7 allow ${np.allowDomains} domain(s)/${np.allowPorts} port(s) \xB7 deny ${np.denyDomains} domain(s)/${np.denyPorts} port(s) \xB7 private ${np.allowPrivateNetwork ? "allow" : "block/confirm"}`);
15111
+ const overrides = Object.entries(np.toolOverrides).map(([k, v]) => `${k}=${v}`).join(", ");
15112
+ if (overrides) out.push(` tool overrides: ${overrides}`);
15113
+ out.push("");
15114
+ out.push(B("Hooks / Plugins:"));
15115
+ out.push(` hooks: ${report.hooks.enabled ? G("enabled") : D("disabled")} \xB7 total ${report.hooks.total} \xB7 project ${report.hooks.project} \xB7 pending trust ${report.hooks.pendingTrust} \xB7 disabled ${report.hooks.disabled}`);
15116
+ out.push(` plugins: installed ${report.plugins.installed} \xB7 active ${report.plugins.active} \xB7 disabled ${report.plugins.disabled} \xB7 untrusted ${report.plugins.untrusted} \xB7 invalid ${report.plugins.invalid}`);
15117
+ out.push(` plugin assets: ${report.plugins.skillDirs} skill dir(s), ${report.plugins.commandDirs} command dir(s), ${report.plugins.agentDirs} agent dir(s), ${report.plugins.mcpServers} MCP server(s)`);
15118
+ if (report.plugins.entries.length > 0) {
15119
+ for (const p of report.plugins.entries.slice(0, 5)) {
15120
+ const state2 = p.valid ? `${p.enabled ? "enabled" : "disabled"} \xB7 ${p.trusted ? "trusted" : "untrusted"}` : `invalid \xB7 ${p.error ?? ""}`;
15121
+ out.push(` ${p.valid ? G("\u2713") : R("\u2717")} ${p.name}@${p.version} ${D(state2)}`);
14988
15122
  }
14989
- ctx.send({ type: "error", message: `Unknown subcommand: ${sub}. Use list/new/switch/delete/rename/diff/cherry-pick.` });
14990
15123
  }
14991
- }
14992
- async function handleFork(args, ctx) {
14993
- {
14994
- const session = ctx.sessions.current;
14995
- if (!session) {
14996
- ctx.send({ type: "info", message: "No active session to fork." });
14997
- }
14998
- const sub = args.join(" ").trim();
14999
- let messageCount = session.messages.length;
15000
- let fromLabel = "current position";
15001
- if (sub) {
15002
- const cp = session.checkpoints.find((c) => c.name === sub);
15003
- if (!cp) {
15004
- const available = session.checkpoints.map((c) => c.name);
15005
- ctx.send({ type: "error", message: available.length > 0 ? `Checkpoint "${sub}" not found. Available: ${available.join(", ")}` : `Checkpoint "${sub}" not found. No checkpoints saved.` });
15006
- }
15007
- messageCount = cp.messageIndex;
15008
- fromLabel = `checkpoint "${cp.name}" (${cp.messageIndex} messages)`;
15009
- }
15010
- try {
15011
- const originalId = session.id.slice(0, 8);
15012
- const forked = await ctx.sessions.forkSession(messageCount);
15013
- ctx.send({ type: "info", message: `\u{1F500} Session Forked
15014
- Original: ${originalId}
15015
- Forked: ${forked.id.slice(0, 8)} "${forked.title ?? "(untitled)"}"
15016
- From: ${fromLabel}
15017
- Messages: ${forked.messages.length} copied` });
15018
- ctx.sendSessionMessages();
15019
- ctx.sendStatus();
15020
- ctx.sendSessionList();
15021
- } catch (err) {
15022
- ctx.send({ type: "error", message: `Fork failed: ${err.message}` });
15023
- }
15124
+ out.push("");
15125
+ out.push(B("MCP Servers:"));
15126
+ out.push(` global configured: ${report.mcp.configured} \xB7 project .mcp.json: ${report.mcp.projectConfigExists ? "yes" : "no"} \xB7 mcpEnabled: ${report.mcp.enabled ? "true" : "false"}`);
15127
+ if (report.mcp.statuses.length === 0) out.push(` ${D("(no connected manager status in this mode)")}`);
15128
+ for (const s of report.mcp.statuses) {
15129
+ const state2 = s.connected ? `${G("connected")} \xB7 ${s.serverName} \xB7 ${s.toolCount} tools` : `${R("disconnected")}${s.error ? D(` \xB7 ${s.error}`) : ""}`;
15130
+ out.push(` ${s.connected ? G("\u2713") : R("\u2717")} ${s.serverId.padEnd(16)} ${state2}`);
15131
+ }
15132
+ out.push("");
15133
+ out.push(B("Config Files:"));
15134
+ for (const f of report.files) {
15135
+ const extra = f.exists && f.sizeBytes !== null ? ` ${D(`(${formatBytes(f.sizeBytes)})`)}` : "";
15136
+ out.push(` ${f.exists ? G("\u2713") : D("\u2013")} ${f.label.padEnd(16)} ${f.exists ? f.path : D("(not found)")}${extra}`);
15137
+ }
15138
+ out.push("");
15139
+ out.push(B("Recent Crashes (last 5):"));
15140
+ if (report.recentCrashes.length === 0) out.push(` ${D("(none - clean!)")}`);
15141
+ for (const c of report.recentCrashes) {
15142
+ out.push(` ${R("\u2717")} ${c.timestamp} ${Y(c.kind)}`);
15143
+ out.push(` ${D(c.message)}`);
15144
+ out.push(` ${D(c.file)}`);
15145
+ }
15146
+ out.push("");
15147
+ const ts = report.toolStats;
15148
+ out.push(B("Tool Usage:"));
15149
+ out.push(` total: ${ts.totalCalls} calls \xB7 ${ts.totalFailures} failures (${ts.totalCalls ? (ts.totalFailures * 100 / ts.totalCalls).toFixed(1) : "0.0"}%)`);
15150
+ if (ts.topUsed.length > 0) {
15151
+ out.push(` ${D("top used:")}`);
15152
+ for (const t of ts.topUsed) out.push(` ${t.name.padEnd(20)} ${String(t.calls).padStart(6)} calls \xB7 avg ${t.calls ? (t.totalDurationMs / t.calls).toFixed(0) : "0"}ms`);
15153
+ }
15154
+ if (ts.topFailing.length > 0) {
15155
+ out.push(` ${D("top failing:")}`);
15156
+ for (const t of ts.topFailing) out.push(` ${R(t.name.padEnd(20))} ${t.failures}/${t.calls} (${(t.failures * 100 / Math.max(1, t.calls)).toFixed(1)}%) ${D(t.lastFailureMessage ?? "")}`.slice(0, 200));
15157
+ }
15158
+ out.push("");
15159
+ const dirAnnotations = {
15160
+ models: "semantic search model -> /index semantic-clear",
15161
+ index: "symbol index -> /index clear",
15162
+ history: "conversation history",
15163
+ logs: "crash logs",
15164
+ "memory-index": "chat memory index -> /memory index-clear",
15165
+ plugins: "installed package plugins"
15166
+ };
15167
+ out.push(B("Disk Usage (~/.aicli):"));
15168
+ out.push(` total: ${formatBytes(report.diskUsage.totalBytes)}`);
15169
+ for (const e of report.diskUsage.entries.slice(0, 10)) {
15170
+ const note = dirAnnotations[e.name];
15171
+ out.push(` ${e.name.padEnd(18)} ${formatBytes(e.bytes)}${note ? ` ${D(`\xB7 ${note}`)}` : ""}`);
15024
15172
  }
15173
+ out.push("");
15174
+ out.push(G("\u2713 Health check complete"));
15175
+ out.push("");
15176
+ return out.join("\n");
15025
15177
  }
15026
- async function handleRewind(args, ctx) {
15027
- {
15028
- const session = ctx.sessions.current;
15029
- if (!session || session.messages.length === 0) {
15030
- ctx.send({ type: "info", message: "No messages to rewind." });
15031
- }
15032
- const rewindSub = args[0];
15033
- if (rewindSub === "list" || !rewindSub) {
15034
- const lines = [`Conversation messages (${session.messages.length} total):
15035
- `];
15036
- const cpIndices = (await import("./file-checkpoint-CGH6OJVI.js")).fileCheckpoints.getMessageIndices();
15037
- for (let i = 0; i < session.messages.length; i++) {
15038
- const m = session.messages[i];
15039
- const text = getContentText(m.content).replace(/\n/g, " ").slice(0, 60);
15040
- const pin = cpIndices.includes(i) ? " \u{1F4CC}" : "";
15041
- lines.push(` [${i + 1}] ${m.role.padEnd(10)} ${text}${pin}`);
15042
- }
15043
- lines.push("", "Usage: /rewind <n> \u2014 rewind to message N", "\u{1F4CC} = file checkpoint");
15044
- ctx.send({ type: "info", message: lines.join("\n") });
15045
- }
15046
- const rewindN = parseInt(rewindSub, 10);
15047
- if (isNaN(rewindN) || rewindN < 1 || rewindN > session.messages.length) {
15048
- ctx.send({ type: "error", message: `Invalid message number: ${rewindSub}. Range: 1-${session.messages.length}` });
15049
- }
15050
- const { fileCheckpoints: fc } = await import("./file-checkpoint-CGH6OJVI.js");
15051
- const rewindRemoved = session.messages.length - rewindN;
15052
- const rewindResult = fc.restoreToMessageIndex(rewindN);
15053
- session.messages = session.messages.slice(0, rewindN);
15054
- session.checkpoints = session.checkpoints.filter((c) => c.messageIndex <= rewindN);
15055
- session.updated = /* @__PURE__ */ new Date();
15056
- ctx.sessions.save();
15057
- const rewindLines = [`\u2713 Rewound to message ${rewindN}`, ` Messages removed: ${rewindRemoved}`];
15058
- if (rewindResult.restored > 0 || rewindResult.deleted > 0) {
15059
- rewindLines.push(` Files restored: ${rewindResult.restored}, deleted: ${rewindResult.deleted}`);
15060
- for (const f of rewindResult.files) rewindLines.push(` ${f}`);
15061
- } else {
15062
- rewindLines.push(" No file changes to revert.");
15178
+
15179
+ // src/web/commands/_shared.ts
15180
+ function lastAssistantText(messages) {
15181
+ for (let i = messages.length - 1; i >= 0; i--) {
15182
+ const m = messages[i];
15183
+ if (!m || m.role !== "assistant") continue;
15184
+ const c = m.content;
15185
+ if (typeof c === "string") return c;
15186
+ if (Array.isArray(c)) {
15187
+ const text = c.filter((p) => p && p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
15188
+ if (text) return text;
15063
15189
  }
15064
- ctx.send({ type: "info", message: rewindLines.join("\n") });
15065
- ctx.sendSessionMessages();
15066
- ctx.sendStatus();
15190
+ return void 0;
15067
15191
  }
15192
+ return void 0;
15068
15193
  }
15069
15194
 
15070
- // src/web/commands/tooling-commands.ts
15071
- import { existsSync as existsSync27, readdirSync as readdirSync12 } from "fs";
15072
- import { join as join22 } from "path";
15073
- async function handleTools(args, ctx) {
15074
- ctx.sendToolsList();
15195
+ // src/web/commands/session-commands.ts
15196
+ async function handleClear(_args, ctx) {
15197
+ ctx.saveIfNeeded();
15198
+ ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
15199
+ ctx.resetWebSessionUsage();
15200
+ ctx.send({ type: "info", message: "Conversation cleared." });
15201
+ ctx.sendStatus();
15202
+ ctx.sendSessionList();
15075
15203
  }
15076
- async function handlePlugins(args, ctx) {
15077
- {
15078
- const configDir = ctx.config.getConfigDir();
15079
- const pluginsDir = join22(configDir, PLUGINS_DIR_NAME);
15080
- const pluginTools = ctx.toolRegistry.listPluginTools();
15081
- const pluginPackages = listInstalledPlugins(configDir);
15082
- const lines = [`\u{1F50C} **Plugins:**`, `Dir: ${pluginsDir}`, ""];
15083
- if (pluginTools.length === 0 && pluginPackages.length === 0) {
15084
- lines.push("No plugins loaded.");
15085
- lines.push("Use /plugin install <path>, /plugin trust <name>, then /plugin enable <name>.");
15086
- } else {
15087
- if (pluginPackages.length > 0) {
15088
- lines.push(`Package plugins (${pluginPackages.length}):`);
15089
- for (const p of pluginPackages) lines.push(` \u2022 ${p.name}@${p.manifest.version} \u2014 ${p.enabled ? "enabled" : "disabled"} / ${p.trusted ? "trusted" : "untrusted"}`);
15090
- lines.push("");
15091
- }
15092
- if (pluginTools.length > 0) {
15093
- lines.push(`Legacy JS plugin tools (${pluginTools.length}):`);
15094
- for (const t of pluginTools) lines.push(` \u2022 ${t.definition.name} \u2014 ${t.definition.description}`);
15095
- }
15096
- }
15097
- ctx.send({ type: "info", message: lines.join("\n") });
15098
- }
15204
+ async function handleCompact(args, ctx) {
15205
+ await ctx.compactSession(args.join(" ") || void 0);
15099
15206
  }
15100
- async function handlePlugin(args, ctx) {
15101
- {
15102
- const configDir = ctx.config.getConfigDir();
15103
- const sub = args[0] ?? "list";
15104
- try {
15105
- if (sub === "list") {
15106
- const plugins = listInstalledPlugins(configDir);
15107
- const lines = [`\u{1F50C} **Package Plugins**`, `Dir: ${pluginRoot(configDir)}`, ""];
15108
- if (plugins.length === 0) lines.push("No package plugins installed.");
15109
- for (const p of plugins) lines.push(` \u2022 ${p.name}@${p.manifest.version} \u2014 ${p.enabled ? "enabled" : "disabled"} / ${p.trusted ? "trusted" : "untrusted"}`);
15110
- ctx.send({ type: "info", message: lines.join("\n") });
15111
- }
15112
- if (sub === "install") {
15113
- const input = args[1];
15114
- if (!input) throw new Error("Usage: /plugin install <path>");
15115
- const plugin = installPlugin(configDir, input);
15116
- ctx.send({ type: "info", message: `Installed plugin ${plugin.name}@${plugin.manifest.version}. Run /plugin trust ${plugin.name}, then /plugin enable ${plugin.name}.` });
15117
- }
15118
- if (sub === "trust") {
15119
- const name = args[1];
15120
- if (!name) throw new Error("Usage: /plugin trust <name>");
15121
- const state2 = trustPlugin(configDir, name);
15122
- ctx.send({ type: "info", message: `Trusted plugin ${state2.name}.` });
15123
- }
15124
- if (sub === "enable" || sub === "disable") {
15125
- const name = args[1];
15126
- if (!name) throw new Error(`Usage: /plugin ${sub} <name>`);
15127
- const state2 = setPluginEnabled(configDir, name, sub === "enable");
15128
- ctx.send({ type: "info", message: `${sub === "enable" ? "Enabled" : "Disabled"} plugin ${state2.name}. Restart Web UI for changes to take effect.` });
15129
- }
15130
- if (sub === "inspect") {
15131
- const name = args[1];
15132
- if (!name) throw new Error("Usage: /plugin inspect <name>");
15133
- ctx.send({ type: "info", message: describePlugin(getInstalledPlugin(configDir, name)).join("\n") });
15134
- }
15135
- throw new Error("Usage: /plugin [list|install|enable|disable|inspect|trust]");
15136
- } catch (err) {
15137
- ctx.send({ type: "error", message: err instanceof Error ? err.message : String(err) });
15138
- }
15139
- }
15207
+ async function handleStatus(_args, ctx) {
15208
+ const session = ctx.sessions.current;
15209
+ const cacheRead = ctx.sessionTokenUsage.cacheReadTokens;
15210
+ const cacheCreate = ctx.sessionTokenUsage.cacheCreationTokens;
15211
+ const cost = computeCost(ctx.currentProvider, ctx.currentModel, ctx.sessionTokenUsage);
15212
+ const cacheLine = cacheRead > 0 || cacheCreate > 0 ? `
15213
+ Cache: write=${cacheCreate} read=${cacheRead}` : "";
15214
+ const costLine = cost != null ? `
15215
+ Cost: ${formatCost(cost)}` : "";
15216
+ ctx.send({
15217
+ type: "info",
15218
+ message: `Provider: ${ctx.currentProvider}
15219
+ Model: ${ctx.currentModel}
15220
+ Session: ${session?.id ?? "none"} (${session?.messages.length ?? 0} messages)
15221
+ Tokens: in=${ctx.sessionTokenUsage.inputTokens} out=${ctx.sessionTokenUsage.outputTokens}${cacheLine}${costLine}`
15222
+ });
15140
15223
  }
15141
- async function handleMcp(args, ctx) {
15142
- {
15143
- if (!ctx.mcpManager) {
15144
- ctx.send({ type: "info", message: "MCP not configured. Add mcpServers to config.json." });
15224
+ async function handleSession(args, ctx) {
15225
+ const sub = args[0];
15226
+ if (sub === "new") {
15227
+ ctx.saveIfNeeded();
15228
+ const created = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
15229
+ ctx.unsavedSessions.set(created.id, created);
15230
+ ctx.resetWebSessionUsage();
15231
+ ctx.send({ type: "info", message: "New session created." });
15232
+ ctx.sendStatus();
15233
+ ctx.sendSessionList();
15234
+ return;
15235
+ }
15236
+ if (sub === "load" && args[1]) {
15237
+ const targetId = args[1];
15238
+ ctx.saveIfNeeded();
15239
+ const cachedExact = ctx.unsavedSessions.get(targetId);
15240
+ const cached = cachedExact ?? [...ctx.unsavedSessions.values()].find((s) => s.id.startsWith(targetId));
15241
+ if (cached) {
15242
+ ctx.sessions.setCurrent(cached);
15243
+ ctx.resetWebSessionUsage();
15244
+ ctx.send({
15245
+ type: "info",
15246
+ message: `Loaded session: ${cached.id.slice(0, 8)} "${cached.title ?? ""}" (${cached.messages.length} messages)`
15247
+ });
15248
+ ctx.sendSessionMessages();
15249
+ ctx.sendStatus();
15250
+ ctx.sendSessionList();
15251
+ return;
15145
15252
  }
15146
- const sub = args[0];
15147
- if (sub === "reconnect") {
15148
- ctx.send({ type: "info", message: "\u{1F504} Reconnecting MCP servers..." });
15149
- try {
15150
- await ctx.mcpManager.reconnectAll();
15151
- ctx.send({ type: "info", message: "\u2713 MCP reconnect complete." });
15152
- } catch (err) {
15153
- ctx.send({ type: "error", message: `Reconnect failed: ${err.message}` });
15253
+ const list = ctx.sessions.listSessions();
15254
+ const found = list.find((s) => s.id.startsWith(targetId));
15255
+ if (found) {
15256
+ ctx.sessions.loadSession(found.id);
15257
+ ctx.resetWebSessionUsage();
15258
+ ctx.send({ type: "info", message: `Loaded session: ${found.id.slice(0, 8)} "${found.title ?? ""}" (${found.messageCount} messages)` });
15259
+ const loadedSession = ctx.sessions.current;
15260
+ if (loadedSession && loadedSession.messages.length > 0) {
15261
+ const lastMsg = loadedSession.messages[loadedSession.messages.length - 1];
15262
+ const isIncomplete = lastMsg.role === "tool" || lastMsg.role === "assistant" && lastMsg.toolCalls && lastMsg.toolCalls.length > 0;
15263
+ if (isIncomplete) {
15264
+ ctx.send({
15265
+ type: "info",
15266
+ 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.'
15267
+ });
15268
+ }
15154
15269
  }
15155
- ctx.sendToolsList();
15156
- }
15157
- const statuses = ctx.mcpManager.getStatus();
15158
- if (statuses.length === 0) {
15159
- ctx.send({ type: "info", message: "No MCP servers configured." });
15160
- }
15161
- const lines = [`\u{1F50C} **MCP Servers (${statuses.length}):**`, ""];
15162
- for (const s of statuses) {
15163
- const state2 = s.connected ? `\u2713 connected \xB7 ${s.serverName} \xB7 ${s.toolCount} tools` : `\u2717 disconnected${s.error ? ` \xB7 ${s.error}` : ""}`;
15164
- lines.push(` ${s.serverId}: ${state2}`);
15270
+ ctx.sendSessionMessages();
15271
+ ctx.sendStatus();
15272
+ ctx.sendSessionList();
15273
+ return;
15165
15274
  }
15166
- lines.push("");
15167
- lines.push("Use /mcp reconnect to reconnect.");
15168
- ctx.send({ type: "info", message: lines.join("\n") });
15275
+ const recreated = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
15276
+ ctx.unsavedSessions.set(recreated.id, recreated);
15277
+ ctx.resetWebSessionUsage();
15278
+ ctx.send({
15279
+ type: "info",
15280
+ message: `Previous session (${targetId.slice(0, 8)}) is no longer available \u2014 started a new one.`
15281
+ });
15282
+ ctx.sendStatus();
15283
+ ctx.sendSessionList();
15284
+ return;
15169
15285
  }
15170
- }
15171
- async function handleSkill(args, ctx) {
15172
- {
15173
- const sub = args[0];
15174
- if (!ctx.skillManager) {
15175
- ctx.send({ type: "error", message: "Skill system not available." });
15176
- }
15177
- if (!sub || sub === "list") {
15178
- const skills = ctx.skillManager.listSkills();
15179
- const active = ctx.skillManager.getActive();
15180
- if (skills.length === 0) {
15181
- ctx.send({ type: "info", message: "No skills available. Add .md files to ~/.aicli/skills/" });
15182
- } else {
15183
- const lines = skills.map((s) => {
15184
- const isActive = active?.meta.name === s.meta.name;
15185
- const marker = isActive ? " \u2705 (active)" : "";
15186
- return ` ${s.meta.name}${marker} \u2014 ${s.meta.description || "(no description)"}`;
15187
- });
15188
- ctx.send({ type: "info", message: `\u{1F4DA} Skills:
15189
- ${lines.join("\n")}` });
15286
+ if (sub === "list") {
15287
+ ctx.sendSessionList();
15288
+ return;
15289
+ }
15290
+ if (sub === "delete" && args[1]) {
15291
+ const targetId = args[1];
15292
+ const cachedKey = ctx.unsavedSessions.has(targetId) ? targetId : [...ctx.unsavedSessions.keys()].find((k) => k.startsWith(targetId));
15293
+ if (cachedKey) {
15294
+ ctx.unsavedSessions.delete(cachedKey);
15295
+ if (ctx.sessions.current?.id === cachedKey) {
15296
+ ctx.sessions.clearCurrent();
15190
15297
  }
15191
- } else if (sub === "off") {
15192
- ctx.skillManager.deactivate();
15193
- ctx.send({ type: "info", message: "\u{1F50C} Skill deactivated." });
15194
- ctx.sendToolsList();
15195
- ctx.sendStatus();
15196
- } else if (sub === "reload") {
15197
- ctx.skillManager.loadSkills(getActivePluginAssets(ctx.config.getConfigDir()).skillDirs);
15198
- ctx.send({ type: "info", message: `\u{1F504} Reloaded ${ctx.skillManager.listSkills().length} skill(s).` });
15199
- ctx.sendToolsList();
15298
+ ctx.send({ type: "info", message: `Deleted session: ${cachedKey.slice(0, 8)}` });
15299
+ ctx.sendSessionList();
15300
+ return;
15301
+ }
15302
+ const list = ctx.sessions.listSessions();
15303
+ const found = list.find((s) => s.id.startsWith(targetId));
15304
+ if (found) {
15305
+ ctx.sessions.deleteSession(found.id);
15306
+ ctx.send({ type: "info", message: `Deleted session: ${found.id.slice(0, 8)}` });
15307
+ ctx.sendSessionList();
15200
15308
  } else {
15201
- const activated = ctx.skillManager.activate(sub);
15202
- if (activated) {
15203
- ctx.send({ type: "info", message: `\u2705 Skill activated: ${activated.meta.name}
15204
- ${activated.meta.description || ""}` });
15205
- ctx.sendToolsList();
15206
- ctx.sendStatus();
15207
- } else {
15208
- const available = ctx.skillManager.listSkills().map((s) => s.meta.name).join(", ");
15209
- ctx.send({ type: "error", message: `Skill "${sub}" not found. Available: ${available}` });
15210
- }
15309
+ ctx.send({ type: "error", message: `Session not found: ${targetId}` });
15211
15310
  }
15311
+ return;
15212
15312
  }
15213
- }
15214
- async function handleCommands(args, ctx) {
15215
- {
15216
- const configDir = ctx.config.getConfigDir();
15217
- const commandsDir = join22(configDir, CUSTOM_COMMANDS_DIR_NAME);
15218
- if (!existsSync27(commandsDir)) {
15219
- ctx.send({ type: "info", message: `No custom commands directory.
15220
- Create: ${commandsDir}/ with .md files.` });
15313
+ if (sub === "batch-delete") {
15314
+ const ids = args.slice(1).filter(Boolean);
15315
+ if (ids.length === 0) {
15316
+ ctx.send({ type: "error", message: "No session IDs provided." });
15317
+ return;
15221
15318
  }
15222
- try {
15223
- const files = readdirSync12(commandsDir).filter((f) => f.endsWith(".md"));
15224
- if (files.length === 0) {
15225
- ctx.send({ type: "info", message: `No custom commands found in ${commandsDir}
15226
- Add .md files to create commands.` });
15227
- } else {
15228
- const lines = [`\u{1F4CB} **Custom Commands (${files.length}):**`, `Dir: ${commandsDir}`, ""];
15229
- for (const f of files) {
15230
- const name = f.replace(/\.md$/, "");
15231
- lines.push(` /${name}`);
15232
- }
15233
- ctx.send({ type: "info", message: lines.join("\n") });
15319
+ const list = ctx.sessions.listSessions();
15320
+ let deleted = 0;
15321
+ for (const targetId of ids) {
15322
+ const cachedKey = ctx.unsavedSessions.has(targetId) ? targetId : [...ctx.unsavedSessions.keys()].find((k) => k.startsWith(targetId));
15323
+ if (cachedKey) {
15324
+ ctx.unsavedSessions.delete(cachedKey);
15325
+ deleted++;
15326
+ continue;
15327
+ }
15328
+ const found = list.find((s) => s.id.startsWith(targetId));
15329
+ if (found) {
15330
+ ctx.sessions.deleteSession(found.id);
15331
+ deleted++;
15234
15332
  }
15235
- } catch {
15236
- ctx.send({ type: "info", message: `Cannot read: ${commandsDir}` });
15237
15333
  }
15334
+ ctx.send({ type: "info", message: `Deleted ${deleted} session(s).` });
15335
+ ctx.sendSessionList();
15336
+ return;
15238
15337
  }
15338
+ if (sub === "rename" && args[1] && args.length >= 3) {
15339
+ const targetId = args[1];
15340
+ const newTitle = args.slice(2).join(" ").trim();
15341
+ if (!newTitle) {
15342
+ ctx.send({ type: "error", message: "Title cannot be empty." });
15343
+ return;
15344
+ }
15345
+ const cachedKey = ctx.unsavedSessions.has(targetId) ? targetId : [...ctx.unsavedSessions.keys()].find((k) => k.startsWith(targetId));
15346
+ if (cachedKey) {
15347
+ const session = ctx.unsavedSessions.get(cachedKey);
15348
+ session.title = newTitle;
15349
+ ctx.send({ type: "info", message: `Renamed session: "${newTitle}"` });
15350
+ ctx.sendSessionList();
15351
+ return;
15352
+ }
15353
+ const list = ctx.sessions.listSessions();
15354
+ const found = list.find((s) => s.id.startsWith(targetId));
15355
+ if (found) {
15356
+ const session = ctx.sessions.loadSession(found.id);
15357
+ session.title = newTitle;
15358
+ ctx.sessions.save();
15359
+ ctx.send({ type: "info", message: `Renamed session: "${newTitle}"` });
15360
+ ctx.sendSessionList();
15361
+ } else {
15362
+ ctx.send({ type: "error", message: `Session not found: ${targetId}` });
15363
+ }
15364
+ return;
15365
+ }
15366
+ ctx.send({ type: "info", message: "Usage: /session new | list | load <id> | delete <id> | rename <id> <title>" });
15239
15367
  }
15240
-
15241
- // src/web/commands/review-commands.ts
15242
- import { existsSync as existsSync28, readFileSync as readFileSync22, writeFileSync as writeFileSync3 } from "fs";
15243
- import { join as join23 } from "path";
15244
-
15245
- // src/cli/review-prompts.ts
15246
- function buildReviewPrompt(diff, gitContextStr, detailed) {
15247
- const level = detailed ? "Please perform a detailed in-depth review covering: security, performance, maintainability, error handling, naming conventions, and code duplication." : "Please perform a concise code review focusing on bugs, security issues, and key improvement suggestions.";
15248
- return `# Code Review Request
15249
-
15250
- ${level}
15251
-
15252
- ## Git Status
15253
- ${gitContextStr}
15254
-
15255
- ## Code Changes (diff)
15256
- \`\`\`diff
15257
- ${diff}
15258
- \`\`\`
15259
-
15260
- ## Output Format
15261
- Please structure your review as follows:
15262
- 1. **Overall Assessment**: One-sentence summary of the change quality
15263
- 2. **Issues** (if any): Each issue with [Severity] file:line \u2014 description + suggested fix
15264
- 3. **Improvement Suggestions** (if any): Non-critical but recommended optimizations
15265
- 4. **Highlights** (if any): Good practices worth acknowledging
15266
-
15267
- Severity levels: \u{1F534} Critical / \u{1F7E1} Warning / \u{1F535} Info`;
15268
- }
15269
- function buildSecurityReviewPrompt(diff, gitContextStr) {
15270
- return `# Security Vulnerability Review
15271
-
15272
- Analyze the following code changes **exclusively for security vulnerabilities**.
15273
-
15274
- ## Categories to check:
15275
- 1. **Injection** \u2014 SQL, command, path traversal, XSS, template injection
15276
- 2. **Authentication & Authorization** \u2014 hardcoded credentials, missing auth checks, privilege escalation
15277
- 3. **Secrets & Sensitive Data** \u2014 API keys, tokens, passwords in code, logging sensitive data
15278
- 4. **Input Validation** \u2014 missing validation, unsafe deserialization, buffer issues
15279
- 5. **Cryptography** \u2014 weak algorithms, improper random, hardcoded IVs/salts
15280
- 6. **Dependencies** \u2014 known vulnerable packages, unsafe dynamic imports
15281
- 7. **File System** \u2014 path traversal, unsafe file permissions, symlink attacks
15282
- 8. **Network** \u2014 SSRF, insecure protocols, missing TLS validation
15283
-
15284
- ## Git Status
15285
- ${gitContextStr}
15286
-
15287
- ## Code Changes (diff)
15288
- \`\`\`diff
15289
- ${diff}
15290
- \`\`\`
15291
-
15292
- ## Output Format
15293
- For each finding:
15294
- - **Severity**: \u{1F534} CRITICAL / \u{1F7E0} HIGH / \u{1F7E1} MEDIUM / \u{1F535} LOW / \u2139\uFE0F INFO
15295
- - **Category**: (from list above)
15296
- - **File & location**: file:line
15297
- - **Description**: what the vulnerability is and how it could be exploited
15298
- - **Recommended fix**: specific code change to resolve
15299
-
15300
- If no security issues found, state "\u2705 No security vulnerabilities detected" with a brief explanation of what was checked.`;
15301
- }
15302
-
15303
- // src/core/git-diff.ts
15304
- import { execFileSync as execFileSync3 } from "child_process";
15305
- function readGitDiff(options = {}) {
15306
- const args = ["diff"];
15307
- if (options.staged) args.push("--staged");
15308
- return execFileSync3("git", args, {
15309
- encoding: "utf-8",
15310
- timeout: options.timeoutMs ?? 1e4
15311
- }).trim();
15368
+ async function handleExport(args, ctx) {
15369
+ const format = args[0] === "json" ? "json" : "md";
15370
+ ctx.exportConversation(format);
15371
+ }
15372
+ async function handleCost(_args, ctx) {
15373
+ const u = ctx.sessionTokenUsage;
15374
+ const total = u.inputTokens + u.outputTokens + u.cacheCreationTokens + u.cacheReadTokens;
15375
+ const cost = computeCost(ctx.currentProvider, ctx.currentModel, u);
15376
+ let savings = null;
15377
+ if (cost != null && u.cacheReadTokens > 0) {
15378
+ const withoutCache = computeCost(ctx.currentProvider, ctx.currentModel, {
15379
+ inputTokens: u.inputTokens + u.cacheReadTokens,
15380
+ outputTokens: u.outputTokens,
15381
+ cacheCreationTokens: u.cacheCreationTokens,
15382
+ cacheReadTokens: 0
15383
+ });
15384
+ if (withoutCache != null) savings = withoutCache - cost;
15385
+ }
15386
+ const lines = [
15387
+ "\u{1F4B0} Session Cost & Token Usage",
15388
+ ` Provider : ${ctx.currentProvider}`,
15389
+ ` Model : ${ctx.currentModel}`,
15390
+ ` Input (uncached): ${u.inputTokens.toLocaleString()}`,
15391
+ ` Output : ${u.outputTokens.toLocaleString()}`
15392
+ ];
15393
+ if (u.cacheCreationTokens > 0) lines.push(` Cache write : ${u.cacheCreationTokens.toLocaleString()}`);
15394
+ if (u.cacheReadTokens > 0) {
15395
+ const pct = Math.round(u.cacheReadTokens / (u.cacheReadTokens + u.inputTokens) * 100);
15396
+ lines.push(` Cache read : ${u.cacheReadTokens.toLocaleString()} (${pct}% hit rate)`);
15397
+ }
15398
+ lines.push(` Total tokens : ${total.toLocaleString()}`);
15399
+ if (cost != null) {
15400
+ lines.push(` Cost : ${formatCost(cost)}`);
15401
+ if (savings != null && savings > 0) {
15402
+ lines.push(` Cache savings : -${formatCost(savings)} (vs no cache)`);
15403
+ }
15404
+ } else {
15405
+ lines.push(` Cost : \u2014 (pricing unknown for this model)`);
15406
+ }
15407
+ ctx.send({ type: "info", message: lines.join("\n") });
15312
15408
  }
15313
-
15314
- // src/web/commands/review-commands.ts
15315
- async function handleReview(args, ctx) {
15316
- {
15317
- const gitCtx = getGitContext();
15318
- if (!gitCtx) {
15319
- ctx.send({ type: "error", message: "Not a git repository." });
15409
+ async function handleCheckpoint(args, ctx) {
15410
+ const session = ctx.sessions.current;
15411
+ if (!session) {
15412
+ ctx.send({ type: "error", message: "No active session." });
15413
+ return;
15414
+ }
15415
+ const sub = args[0]?.toLowerCase();
15416
+ if (!sub || sub === "list") {
15417
+ const cps = session.checkpoints;
15418
+ if (cps.length === 0) {
15419
+ ctx.send({ type: "info", message: "No checkpoints. Use /checkpoint save <name> to create one." });
15420
+ } else {
15421
+ const lines = [`\u{1F4CC} Checkpoints (${cps.length}):`, ""];
15422
+ for (const cp of cps) {
15423
+ const ts = cp.timestamp.toLocaleString();
15424
+ lines.push(` ${cp.name.padEnd(20)} msg #${cp.messageIndex} ${ts}`);
15425
+ }
15426
+ ctx.send({ type: "info", message: lines.join("\n") });
15320
15427
  }
15321
- const staged = args.includes("--staged");
15322
- const detailed = args.includes("--detailed");
15323
- let diff;
15324
- try {
15325
- diff = readGitDiff({ staged });
15326
- } catch {
15327
- ctx.send({ type: "error", message: "Failed to run git diff." });
15428
+ return;
15429
+ }
15430
+ const cpName = args.slice(1).join(" ").trim();
15431
+ if (sub === "save") {
15432
+ if (!cpName) {
15433
+ ctx.send({ type: "error", message: "Usage: /checkpoint save <name>" });
15434
+ return;
15328
15435
  }
15329
- if (!diff) {
15330
- ctx.send({ type: "info", message: "No changes to review." + (staged ? "" : " Try --staged for staged changes.") });
15436
+ session.createCheckpoint(cpName);
15437
+ ctx.send({ type: "info", message: `\u2713 Checkpoint "${cpName}" saved at message #${session.messages.length}` });
15438
+ } else if (sub === "restore") {
15439
+ if (!cpName) {
15440
+ ctx.send({ type: "error", message: "Usage: /checkpoint restore <name>" });
15441
+ return;
15331
15442
  }
15332
- const MAX_DIFF = 8e3;
15333
- let truncated = false;
15334
- if (diff.length > MAX_DIFF) {
15335
- const head = diff.slice(0, Math.floor(MAX_DIFF * 0.7));
15336
- const tail = diff.slice(diff.length - Math.floor(MAX_DIFF * 0.2));
15337
- diff = head + "\n\n... [diff truncated, " + diff.length + " chars total] ...\n\n" + tail;
15338
- truncated = true;
15443
+ const ok = session.restoreCheckpoint(cpName);
15444
+ if (ok) {
15445
+ ctx.send({ type: "info", message: `\u2713 Restored to checkpoint "${cpName}" (${session.messages.length} messages)` });
15446
+ ctx.sendSessionMessages();
15447
+ ctx.sendStatus();
15448
+ } else {
15449
+ ctx.send({ type: "error", message: `Checkpoint "${cpName}" not found.` });
15339
15450
  }
15340
- const reviewPrompt = buildReviewPrompt(diff, formatGitContextForPrompt(gitCtx), detailed);
15341
- ctx.send({ type: "info", message: "\u{1F50D} Analyzing changes..." });
15342
- try {
15343
- const review = await ctx.chatOnce(reviewPrompt, { temperature: 0.3, maxTokens: 8192 });
15344
- const msg = truncated ? review + "\n\n\u26A0 Diff was truncated. Consider reviewing smaller changesets." : review;
15345
- ctx.send({ type: "info", message: msg });
15346
- } catch (err) {
15347
- ctx.send({ type: "error", message: `Review failed: ${err.message}` });
15451
+ } else if (sub === "delete") {
15452
+ if (!cpName) {
15453
+ ctx.send({ type: "error", message: "Usage: /checkpoint delete <name>" });
15454
+ return;
15455
+ }
15456
+ const ok = session.deleteCheckpoint(cpName);
15457
+ if (ok) {
15458
+ ctx.send({ type: "info", message: `\u2713 Deleted checkpoint "${cpName}"` });
15459
+ } else {
15460
+ ctx.send({ type: "error", message: `Checkpoint "${cpName}" not found.` });
15348
15461
  }
15462
+ } else {
15463
+ ctx.send({ type: "error", message: `Unknown subcommand: ${sub}. Use save/restore/list/delete.` });
15349
15464
  }
15350
15465
  }
15351
- async function handleSecurityReview(args, ctx) {
15352
- {
15353
- const gitCtx = getGitContext();
15354
- if (!gitCtx) {
15355
- ctx.send({ type: "error", message: "Not a git repository." });
15466
+ async function handleBranch(args, ctx) {
15467
+ const session = ctx.sessions.current;
15468
+ if (!session) {
15469
+ ctx.send({ type: "error", message: "No active session." });
15470
+ return;
15471
+ }
15472
+ const sub = args[0]?.toLowerCase();
15473
+ const resolve14 = (ref) => {
15474
+ const r = session.resolveBranchRef(ref);
15475
+ if (r.ok) return r.id;
15476
+ if (r.reason === "ambiguous") {
15477
+ const list = r.matches.map((m) => `${m.id} (${m.title})`).join(", ");
15478
+ ctx.send({ type: "error", message: `Ambiguous branch reference "${ref}". Matches: ${list}. Use the 6-char id.` });
15479
+ } else {
15480
+ ctx.send({ type: "error", message: `Branch "${ref}" not found. Use /branch list to see all branches.` });
15356
15481
  }
15357
- const secStaged = args.includes("--staged");
15358
- let secDiff;
15359
- try {
15360
- secDiff = readGitDiff({ staged: secStaged });
15361
- } catch {
15362
- ctx.send({ type: "error", message: "Failed to run git diff." });
15482
+ return null;
15483
+ };
15484
+ if (!sub || sub === "list") {
15485
+ const branches = session.listBranches();
15486
+ const lines = [`\u{1F33F} Branches (${branches.length}):`, ""];
15487
+ for (const b of branches) {
15488
+ const marker = b.id === session.activeBranchId ? "\u25CF " : " ";
15489
+ const count = b.id === session.activeBranchId ? session.messages.length : session.getBranchMessages(b.id)?.length ?? 0;
15490
+ const parent = b.parentBranchId ? ` \u2190 ${b.parentBranchId}@${b.parentMessageIndex}` : "";
15491
+ lines.push(` ${marker}${b.id.padEnd(10)} ${b.title.padEnd(20)} (${count} msgs)${parent}`);
15363
15492
  }
15364
- if (!secDiff) {
15365
- ctx.send({ type: "info", message: "No changes to review." + (secStaged ? "" : " Try --staged for staged changes.") });
15493
+ lines.push("");
15494
+ 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>");
15495
+ ctx.send({ type: "info", message: lines.join("\n") });
15496
+ return;
15497
+ }
15498
+ if (sub === "new") {
15499
+ const idxArg = args[1];
15500
+ if (!idxArg) {
15501
+ ctx.send({ type: "error", message: "Usage: /branch new <msgIndex> [title]" });
15502
+ return;
15366
15503
  }
15367
- const SEC_MAX_DIFF = 8e3;
15368
- let secTruncated = false;
15369
- if (secDiff.length > SEC_MAX_DIFF) {
15370
- const head = secDiff.slice(0, Math.floor(SEC_MAX_DIFF * 0.7));
15371
- const tail = secDiff.slice(secDiff.length - Math.floor(SEC_MAX_DIFF * 0.2));
15372
- secDiff = head + "\n\n... [diff truncated, " + secDiff.length + " chars total] ...\n\n" + tail;
15373
- secTruncated = true;
15504
+ const fromIndex = parseInt(idxArg, 10);
15505
+ if (isNaN(fromIndex) || fromIndex < 0 || fromIndex > session.messages.length) {
15506
+ ctx.send({ type: "error", message: `Invalid msgIndex: ${idxArg}. Range: 0-${session.messages.length}` });
15507
+ return;
15374
15508
  }
15375
- const secPrompt = buildSecurityReviewPrompt(secDiff, formatGitContextForPrompt(gitCtx));
15376
- ctx.send({ type: "info", message: "\u{1F512} Scanning for security vulnerabilities..." });
15509
+ const title = args.slice(2).join(" ").trim() || void 0;
15377
15510
  try {
15378
- const secReview = await ctx.chatOnce(secPrompt, { temperature: 0.2, maxTokens: 8192 });
15379
- const secMsg = secTruncated ? secReview + "\n\n\u26A0 Diff was truncated. Consider reviewing smaller changesets." : secReview;
15380
- ctx.send({ type: "info", message: secMsg });
15511
+ const newId = session.createBranch(fromIndex, title);
15512
+ await ctx.sessions.save();
15513
+ ctx.send({ type: "info", message: `\u2713 Created branch "${newId}" from message #${fromIndex}. Now active (${session.messages.length} messages).` });
15514
+ ctx.sendSessionMessages();
15515
+ ctx.sendStatus();
15381
15516
  } catch (err) {
15382
- ctx.send({ type: "error", message: `Security review failed: ${err.message}` });
15517
+ ctx.send({ type: "error", message: err.message });
15383
15518
  }
15519
+ return;
15384
15520
  }
15385
- }
15386
- async function handleTest(args, ctx) {
15387
- {
15388
- ctx.send({ type: "info", message: "\u{1F9EA} Running tests..." });
15389
- try {
15390
- const { executeTests } = await import("./run-tests-NIY24YDP.js");
15391
- const argStr = args.join(" ").trim();
15392
- let testArgs = {};
15393
- if (argStr) {
15394
- const isCommand = argStr.includes(" ") || /^(mvn|gradle|npm|pytest|cargo|go)\b/.test(argStr);
15395
- testArgs = isCommand ? { command: argStr } : { filter: argStr };
15396
- }
15397
- const report = await executeTests(testArgs);
15398
- ctx.send({ type: "info", message: report });
15399
- } catch (err) {
15400
- ctx.send({ type: "error", message: `Test failed: ${err.message}` });
15521
+ if (sub === "switch") {
15522
+ const ref = args[1];
15523
+ if (!ref) {
15524
+ ctx.send({ type: "error", message: "Usage: /branch switch <id|title>" });
15525
+ return;
15526
+ }
15527
+ const id = resolve14(ref);
15528
+ if (!id) return;
15529
+ const ok = session.switchBranch(id);
15530
+ if (ok) {
15531
+ await ctx.sessions.save();
15532
+ const b = session.getActiveBranch();
15533
+ ctx.send({ type: "info", message: `\u2713 Switched to branch "${b.id}" \u2014 ${b.title} (${session.messages.length} messages)` });
15534
+ ctx.sendSessionMessages();
15535
+ ctx.sendStatus();
15536
+ } else {
15537
+ ctx.send({ type: "error", message: `Cannot switch to "${id}" (already active).` });
15401
15538
  }
15539
+ return;
15402
15540
  }
15403
- }
15404
- async function handleDiff(args, ctx) {
15405
- {
15406
- const history = undoStack.getHistory();
15407
- if (history.length === 0) {
15408
- ctx.send({ type: "info", message: "No file modifications in this session." });
15541
+ if (sub === "delete") {
15542
+ const ref = args[1];
15543
+ if (!ref) {
15544
+ ctx.send({ type: "error", message: "Usage: /branch delete <id|title>" });
15545
+ return;
15409
15546
  }
15410
- const fileMap = /* @__PURE__ */ new Map();
15411
- for (const entry of history) {
15412
- if (!fileMap.has(entry.filePath)) {
15413
- fileMap.set(entry.filePath, { earliest: entry.previousContent });
15414
- }
15547
+ const id = resolve14(ref);
15548
+ if (!id) return;
15549
+ const ok = session.deleteBranch(id);
15550
+ if (ok) {
15551
+ await ctx.sessions.save();
15552
+ ctx.send({ type: "info", message: `\u2713 Deleted branch "${id}"` });
15553
+ ctx.sendStatus();
15554
+ } else {
15555
+ ctx.send({ type: "error", message: `Cannot delete "${id}" (active, or last remaining branch).` });
15415
15556
  }
15416
- const statsOnly = args[0]?.toLowerCase() === "--stats";
15417
- let newFiles = 0;
15418
- let modifiedFiles = 0;
15419
- const diffLines2 = [];
15420
- for (const [filePath, { earliest }] of fileMap) {
15421
- const currentContent = existsSync28(filePath) ? (() => {
15422
- try {
15423
- return readFileSync22(filePath, "utf-8");
15424
- } catch {
15425
- return null;
15426
- }
15427
- })() : null;
15428
- const isNew = earliest === null;
15429
- if (isNew) newFiles++;
15430
- else modifiedFiles++;
15431
- if (statsOnly) continue;
15432
- if (isNew) {
15433
- diffLines2.push(`
15434
- \u{1F4C4} [NEW] ${filePath}`);
15435
- if (currentContent !== null) {
15436
- diffLines2.push(` ${currentContent.split("\n").length} lines`);
15437
- } else {
15438
- diffLines2.push(" (file was deleted after creation)");
15439
- }
15440
- } else {
15441
- diffLines2.push(`
15442
- \u{1F4DD} [MODIFIED] ${filePath}`);
15443
- const oldText = earliest ?? "";
15444
- const newText = currentContent ?? "";
15445
- if (oldText === newText) {
15446
- diffLines2.push(" (no net change)");
15447
- } else {
15448
- diffLines2.push(renderDiff(oldText, newText, { filePath, maxLines: 80 }));
15449
- }
15557
+ return;
15558
+ }
15559
+ if (sub === "rename") {
15560
+ const ref = args[1];
15561
+ const title = args.slice(2).join(" ").trim();
15562
+ if (!ref || !title) {
15563
+ ctx.send({ type: "error", message: "Usage: /branch rename <id|title> <new title>" });
15564
+ return;
15565
+ }
15566
+ const id = resolve14(ref);
15567
+ if (!id) return;
15568
+ const ok = session.renameBranch(id, title);
15569
+ if (ok) {
15570
+ await ctx.sessions.save();
15571
+ ctx.send({ type: "info", message: `\u2713 Renamed branch "${id}" \u2192 "${title}"` });
15572
+ ctx.sendStatus();
15573
+ } else {
15574
+ ctx.send({ type: "error", message: `Branch "${id}" not found.` });
15575
+ }
15576
+ return;
15577
+ }
15578
+ if (sub === "diff") {
15579
+ const ref = args[1];
15580
+ if (!ref) {
15581
+ ctx.send({ type: "error", message: "Usage: /branch diff <id|title>" });
15582
+ return;
15583
+ }
15584
+ const id = resolve14(ref);
15585
+ if (!id) return;
15586
+ const d = session.diffBranches(id);
15587
+ if (!d) {
15588
+ ctx.send({ type: "error", message: `Branch "${id}" not found.` });
15589
+ return;
15590
+ }
15591
+ const preview = (m) => {
15592
+ const text = typeof m.content === "string" ? m.content : "[complex content]";
15593
+ return text.replace(/\s+/g, " ").trim().slice(0, 70);
15594
+ };
15595
+ const lines = [
15596
+ `\u{1F500} diff ${id} (source) \u2194 ${session.activeBranchId} (active)`,
15597
+ `Common prefix: ${d.commonPrefix} message(s)`
15598
+ ];
15599
+ if (d.sourceOnly.length === 0 && d.targetOnly.length === 0) {
15600
+ lines.push("Branches are identical.");
15601
+ } else {
15602
+ if (d.sourceOnly.length > 0) {
15603
+ lines.push("", `Only in ${id} (${d.sourceOnly.length}):`);
15604
+ d.sourceOnly.forEach(
15605
+ (m, i) => lines.push(` [${d.commonPrefix + i}] ${m.role}: ${preview(m)}`)
15606
+ );
15450
15607
  }
15608
+ if (d.targetOnly.length > 0) {
15609
+ lines.push("", `Only in ${session.activeBranchId} (active) (${d.targetOnly.length}):`);
15610
+ d.targetOnly.forEach(
15611
+ (m, i) => lines.push(` [${d.commonPrefix + i}] ${m.role}: ${preview(m)}`)
15612
+ );
15613
+ }
15614
+ lines.push("", `Use /branch cherry-pick ${id} <index> to copy a message into the active branch.`);
15451
15615
  }
15452
- diffLines2.push(`
15453
- **Summary:** ${fileMap.size} file(s) \u2014 ${newFiles} new, ${modifiedFiles} modified`);
15454
- ctx.send({ type: "info", message: diffLines2.join("\n") });
15616
+ ctx.send({ type: "info", message: lines.join("\n") });
15617
+ return;
15455
15618
  }
15456
- }
15457
- async function handleInit(args, ctx) {
15458
- {
15459
- const cwd = process.cwd();
15460
- const targetPath = join23(cwd, "AICLI.md");
15461
- const force = args.includes("--force");
15462
- if (existsSync28(targetPath) && !force) {
15463
- ctx.send({ type: "info", message: `AICLI.md already exists at ${targetPath}
15464
- Use /init --force to overwrite.` });
15619
+ if (sub === "cherry-pick") {
15620
+ const ref = args[1];
15621
+ const idxStr = args[2];
15622
+ if (!ref || !idxStr) {
15623
+ ctx.send({ type: "error", message: "Usage: /branch cherry-pick <source-id|title> <msg-index>" });
15624
+ return;
15465
15625
  }
15466
- ctx.send({ type: "info", message: "\u{1F4DD} Scanning project structure..." });
15467
- try {
15468
- const projectInfo = scanProject(cwd);
15469
- const prompt = buildInitPrompt(projectInfo, cwd);
15470
- const content = await ctx.chatOnce(prompt, { temperature: 0.3, maxTokens: 4096 });
15471
- writeFileSync3(targetPath, content, "utf-8");
15472
- ctx.send({ type: "info", message: `\u2713 Generated: ${targetPath} (${content.length} chars)
15473
- Use /context reload to load it.` });
15474
- } catch (err) {
15475
- ctx.send({ type: "error", message: `Failed to generate AICLI.md: ${err.message}` });
15626
+ const id = resolve14(ref);
15627
+ if (!id) return;
15628
+ const idx = parseInt(idxStr, 10);
15629
+ if (Number.isNaN(idx)) {
15630
+ ctx.send({ type: "error", message: `Invalid message index: "${idxStr}"` });
15631
+ return;
15632
+ }
15633
+ const picked = session.cherryPickMessage(id, idx);
15634
+ if (!picked) {
15635
+ ctx.send({ type: "error", message: `Cherry-pick failed \u2014 index ${idx} out of range for branch "${id}".` });
15636
+ return;
15476
15637
  }
15638
+ await ctx.sessions.save();
15639
+ const pickPreview = typeof picked.content === "string" ? picked.content.replace(/\s+/g, " ").trim().slice(0, 60) : "[complex content]";
15640
+ ctx.send({ type: "info", message: `\u2713 Cherry-picked ${picked.role} message from "${id}"[${idx}] \u2192 active branch (${session.messages.length} msgs): ${pickPreview}` });
15641
+ ctx.sendSessionMessages();
15642
+ ctx.sendStatus();
15643
+ return;
15477
15644
  }
15645
+ ctx.send({ type: "error", message: `Unknown subcommand: ${sub}. Use list/new/switch/delete/rename/diff/cherry-pick.` });
15478
15646
  }
15479
- async function handleScaffold(args, ctx) {
15480
- {
15481
- const description = args.join(" ").trim();
15482
- if (!description) {
15483
- ctx.send({ type: "error", message: "Usage: /scaffold <project description>" });
15647
+ async function handleFork(args, ctx) {
15648
+ const session = ctx.sessions.current;
15649
+ if (!session) {
15650
+ ctx.send({ type: "info", message: "No active session to fork." });
15651
+ return;
15652
+ }
15653
+ const sub = args.join(" ").trim();
15654
+ let messageCount = session.messages.length;
15655
+ let fromLabel = "current position";
15656
+ if (sub) {
15657
+ const cp = session.checkpoints.find((c) => c.name === sub);
15658
+ if (!cp) {
15659
+ const available = session.checkpoints.map((c) => c.name);
15660
+ ctx.send({ type: "error", message: available.length > 0 ? `Checkpoint "${sub}" not found. Available: ${available.join(", ")}` : `Checkpoint "${sub}" not found. No checkpoints saved.` });
15661
+ return;
15484
15662
  }
15485
- ctx.send({ type: "info", message: "\u{1F3D7}\uFE0F Generating project scaffold..." });
15486
- const scaffoldPrompt = `Please scaffold a project based on this description: ${description}
15487
-
15488
- Create the necessary files and directory structure. Use write_file and bash tools to set up the project.`;
15489
- await ctx.handleChat(scaffoldPrompt);
15663
+ messageCount = cp.messageIndex;
15664
+ fromLabel = `checkpoint "${cp.name}" (${cp.messageIndex} messages)`;
15665
+ }
15666
+ try {
15667
+ const originalId = session.id.slice(0, 8);
15668
+ const forked = await ctx.sessions.forkSession(messageCount);
15669
+ ctx.send({ type: "info", message: `\u{1F500} Session Forked
15670
+ Original: ${originalId}
15671
+ Forked: ${forked.id.slice(0, 8)} "${forked.title ?? "(untitled)"}"
15672
+ From: ${fromLabel}
15673
+ Messages: ${forked.messages.length} copied` });
15674
+ ctx.sendSessionMessages();
15675
+ ctx.sendStatus();
15676
+ ctx.sendSessionList();
15677
+ } catch (err) {
15678
+ ctx.send({ type: "error", message: `Fork failed: ${err.message}` });
15490
15679
  }
15491
15680
  }
15681
+ async function handleRewind(args, ctx) {
15682
+ const session = ctx.sessions.current;
15683
+ if (!session || session.messages.length === 0) {
15684
+ ctx.send({ type: "info", message: "No messages to rewind." });
15685
+ return;
15686
+ }
15687
+ const rewindSub = args[0];
15688
+ if (rewindSub === "list" || !rewindSub) {
15689
+ const lines = [`Conversation messages (${session.messages.length} total):
15690
+ `];
15691
+ const cpIndices = (await import("./file-checkpoint-CGH6OJVI.js")).fileCheckpoints.getMessageIndices();
15692
+ for (let i = 0; i < session.messages.length; i++) {
15693
+ const m = session.messages[i];
15694
+ const text = getContentText(m.content).replace(/\n/g, " ").slice(0, 60);
15695
+ const pin = cpIndices.includes(i) ? " \u{1F4CC}" : "";
15696
+ lines.push(` [${i + 1}] ${m.role.padEnd(10)} ${text}${pin}`);
15697
+ }
15698
+ lines.push("", "Usage: /rewind <n> \u2014 rewind to message N", "\u{1F4CC} = file checkpoint");
15699
+ ctx.send({ type: "info", message: lines.join("\n") });
15700
+ return;
15701
+ }
15702
+ const rewindN = parseInt(rewindSub, 10);
15703
+ if (isNaN(rewindN) || rewindN < 1 || rewindN > session.messages.length) {
15704
+ ctx.send({ type: "error", message: `Invalid message number: ${rewindSub}. Range: 1-${session.messages.length}` });
15705
+ return;
15706
+ }
15707
+ const { fileCheckpoints: fc } = await import("./file-checkpoint-CGH6OJVI.js");
15708
+ const rewindRemoved = session.messages.length - rewindN;
15709
+ const rewindResult = fc.restoreToMessageIndex(rewindN);
15710
+ session.messages = session.messages.slice(0, rewindN);
15711
+ session.checkpoints = session.checkpoints.filter((c) => c.messageIndex <= rewindN);
15712
+ session.updated = /* @__PURE__ */ new Date();
15713
+ ctx.sessions.save();
15714
+ const rewindLines = [`\u2713 Rewound to message ${rewindN}`, ` Messages removed: ${rewindRemoved}`];
15715
+ if (rewindResult.restored > 0 || rewindResult.deleted > 0) {
15716
+ rewindLines.push(` Files restored: ${rewindResult.restored}, deleted: ${rewindResult.deleted}`);
15717
+ for (const f of rewindResult.files) rewindLines.push(` ${f}`);
15718
+ } else {
15719
+ rewindLines.push(" No file changes to revert.");
15720
+ }
15721
+ ctx.send({ type: "info", message: rewindLines.join("\n") });
15722
+ ctx.sendSessionMessages();
15723
+ ctx.sendStatus();
15724
+ }
15492
15725
 
15493
- // src/web/commands/memory-commands.ts
15494
- import { existsSync as existsSync29, statSync as statSync12 } from "fs";
15495
- import { resolve as resolve12 } from "path";
15496
- async function handleSystem(args, ctx) {
15497
- {
15498
- const text = args.join(" ").trim();
15499
- if (!text) {
15500
- const current = ctx.activeSystemPrompt;
15501
- if (current) {
15502
- const preview = current.length > 500 ? current.slice(0, 500) + "..." : current;
15503
- ctx.send({ type: "info", message: `\u{1F4CB} Current system prompt (${current.length} chars):
15504
-
15505
- ${preview}` });
15506
- } else {
15507
- ctx.send({ type: "info", message: "No custom system prompt set. Using default.\nUsage: /system <prompt>" });
15508
- }
15726
+ // src/web/commands/tooling-commands.ts
15727
+ async function handleTools(_args, ctx) {
15728
+ ctx.sendToolsList();
15729
+ }
15730
+ async function handlePlugins(_args, ctx) {
15731
+ const configDir = ctx.config.getConfigDir();
15732
+ const pluginsDir = join24(configDir, PLUGINS_DIR_NAME);
15733
+ const pluginTools = ctx.toolRegistry.listPluginTools();
15734
+ const pluginPackages = listInstalledPlugins(configDir);
15735
+ const lines = [`\u{1F50C} **Plugins:**`, `Dir: ${pluginsDir}`, ""];
15736
+ if (pluginTools.length === 0 && pluginPackages.length === 0) {
15737
+ lines.push("No plugins loaded.");
15738
+ lines.push("Use /plugin install <path>, /plugin trust <name>, then /plugin enable <name>.");
15739
+ } else {
15740
+ if (pluginPackages.length > 0) {
15741
+ lines.push(`Package plugins (${pluginPackages.length}):`);
15742
+ for (const p of pluginPackages) lines.push(` \u2022 ${p.name}@${p.manifest.version} \u2014 ${p.enabled ? "enabled" : "disabled"} / ${p.trusted ? "trusted" : "untrusted"}`);
15743
+ lines.push("");
15509
15744
  }
15510
- if (text === "clear" || text === "reset") {
15511
- ctx.activeSystemPrompt = ctx.loadContextFiles();
15512
- ctx.send({ type: "info", message: "\u2713 System prompt reset to default (context files)." });
15513
- } else {
15514
- ctx.activeSystemPrompt = text;
15515
- ctx.send({ type: "info", message: `\u2713 System prompt set (${text.length} chars).` });
15745
+ if (pluginTools.length > 0) {
15746
+ lines.push(`Legacy JS plugin tools (${pluginTools.length}):`);
15747
+ for (const t of pluginTools) lines.push(` \u2022 ${t.definition.name} \u2014 ${t.definition.description}`);
15516
15748
  }
15517
15749
  }
15750
+ ctx.send({ type: "info", message: lines.join("\n") });
15518
15751
  }
15519
- async function handleContext(args, ctx) {
15520
- {
15521
- const sub = args[0];
15522
- if (sub === "reload") {
15523
- ctx.activeSystemPrompt = ctx.loadContextFiles();
15524
- ctx.send({ type: "info", message: ctx.activeSystemPrompt ? `\u2713 Context reloaded (${ctx.activeSystemPrompt.length} chars).` : "\u2713 No context files found." });
15525
- }
15526
- const result = ctx.contextLoadResult;
15527
- const layers = ["\u{1F4DA} **Context Layers:**", ""];
15528
- if (result && result.layers.length > 0) {
15529
- const labels = { global: "Global ", project: "Project", plugin: "Plugin ", local: "Local " };
15530
- for (const layer of result.layers) {
15531
- layers.push(` \u2713 ${labels[layer.level] ?? layer.level}: ${layer.displayPath} (${layer.charCount} chars${layer.truncated ? ", truncated" : ""})`);
15532
- }
15533
- layers.push(` Total: ${result.totalChars} chars (${result.layers.length} layer${result.layers.length > 1 ? "s" : ""})`);
15534
- } else {
15535
- layers.push(" No context files loaded.");
15536
- layers.push(" Search order per layer: AICLI.override.md \u2192 AGENTS.override.md \u2192 AICLI.md \u2192 CLAUDE.md \u2192 AGENTS.md");
15752
+ async function handlePlugin(args, ctx) {
15753
+ const configDir = ctx.config.getConfigDir();
15754
+ const sub = args[0] ?? "list";
15755
+ try {
15756
+ if (sub === "list") {
15757
+ const plugins = listInstalledPlugins(configDir);
15758
+ const lines = [`\u{1F50C} **Package Plugins**`, `Dir: ${pluginRoot(configDir)}`, ""];
15759
+ if (plugins.length === 0) lines.push("No package plugins installed.");
15760
+ for (const p of plugins) lines.push(` \u2022 ${p.name}@${p.manifest.version} \u2014 ${p.enabled ? "enabled" : "disabled"} / ${p.trusted ? "trusted" : "untrusted"}`);
15761
+ ctx.send({ type: "info", message: lines.join("\n") });
15762
+ return;
15537
15763
  }
15538
- if (result && result.skipped.length > 0) {
15539
- layers.push("");
15540
- layers.push("Skipped:");
15541
- for (const skipped of result.skipped.slice(0, 5)) {
15542
- layers.push(` - ${skipped.displayPath}: ${skipped.reason}${skipped.message ? ` (${skipped.message})` : ""}`);
15543
- }
15764
+ if (sub === "install") {
15765
+ const input = args[1];
15766
+ if (!input) throw new Error("Usage: /plugin install <path>");
15767
+ const plugin = installPlugin(configDir, input);
15768
+ ctx.send({ type: "info", message: `Installed plugin ${plugin.name}@${plugin.manifest.version}. Run /plugin trust ${plugin.name}, then /plugin enable ${plugin.name}.` });
15769
+ return;
15544
15770
  }
15545
- layers.push("");
15546
- layers.push(`Total prompt length: ${ctx.activeSystemPrompt?.length ?? 0} chars`);
15547
- layers.push("Use /context reload to refresh.");
15548
- ctx.send({ type: "info", message: layers.join("\n") });
15771
+ if (sub === "trust") {
15772
+ const name = args[1];
15773
+ if (!name) throw new Error("Usage: /plugin trust <name>");
15774
+ const state2 = trustPlugin(configDir, name);
15775
+ ctx.send({ type: "info", message: `Trusted plugin ${state2.name}.` });
15776
+ return;
15777
+ }
15778
+ if (sub === "enable" || sub === "disable") {
15779
+ const name = args[1];
15780
+ if (!name) throw new Error(`Usage: /plugin ${sub} <name>`);
15781
+ const state2 = setPluginEnabled(configDir, name, sub === "enable");
15782
+ ctx.send({ type: "info", message: `${sub === "enable" ? "Enabled" : "Disabled"} plugin ${state2.name}. Restart Web UI for changes to take effect.` });
15783
+ return;
15784
+ }
15785
+ if (sub === "inspect") {
15786
+ const name = args[1];
15787
+ if (!name) throw new Error("Usage: /plugin inspect <name>");
15788
+ ctx.send({ type: "info", message: describePlugin(getInstalledPlugin(configDir, name)).join("\n") });
15789
+ return;
15790
+ }
15791
+ throw new Error("Usage: /plugin [list|install|enable|disable|inspect|trust]");
15792
+ } catch (err) {
15793
+ ctx.send({ type: "error", message: err instanceof Error ? err.message : String(err) });
15549
15794
  }
15550
15795
  }
15551
- async function handleIndex(args, ctx) {
15552
- {
15553
- const sub = (args[0] ?? "status").toLowerCase();
15554
- const root = process.cwd();
15555
- const { loadIndex: loadIndex2, clearIndex } = await import("./store-VO37H6LS.js");
15556
- const { indexProject: indexProject2 } = await import("./indexer-4WWS3VIL.js");
15557
- const { loadVectorStore, clearVectorStore } = await import("./vector-store-JBAE6PS4.js");
15558
- if (sub === "status") {
15559
- const idx = loadIndex2(root);
15560
- const vec = loadVectorStore(root);
15561
- if (!idx) {
15562
- ctx.send({ type: "info", message: `No symbol index for ${root}. Run /index rebuild.` });
15563
- } else {
15564
- const semantic = vec ? `${vec.count} vectors \xD7 ${vec.dim}-dim` : "not built \u2014 run /index semantic-rebuild to enable search_code";
15565
- ctx.send({
15566
- type: "info",
15567
- message: [
15568
- "\u{1F50E} Symbol index:",
15569
- ` Root: ${idx.root}`,
15570
- ` Generated: ${idx.generated}`,
15571
- ` Files: ${idx.fileCount}`,
15572
- ` Symbols: ${idx.symbolCount}`,
15573
- ` Semantic: ${semantic}`
15574
- ].join("\n")
15575
- });
15576
- }
15577
- } else if (sub === "rebuild") {
15578
- ctx.send({ type: "info", message: `Indexing ${root}\u2026` });
15579
- const { stats } = await indexProject2(root, { force: true });
15580
- ctx.send({
15581
- type: "info",
15582
- message: `\u2713 Indexed ${stats.filesParsed} files (${stats.symbols} symbols) in ${stats.durationMs}ms`
15583
- });
15584
- } else if (sub === "clear") {
15585
- clearIndex(root);
15586
- clearVectorStore(root);
15587
- ctx.send({ type: "info", message: `\u2713 Cleared symbol + semantic index for ${root}` });
15588
- } else if (sub === "semantic-rebuild") {
15589
- const idx = loadIndex2(root);
15590
- if (!idx) {
15591
- ctx.send({ type: "error", message: "No symbol index yet. Run /index rebuild first." });
15592
- }
15593
- if (idx.symbolCount === 0) {
15594
- ctx.send({
15595
- type: "error",
15596
- 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."
15597
- });
15598
- }
15599
- ctx.send({
15600
- type: "info",
15601
- message: `Building semantic index for ${idx.symbolCount} symbols\u2026 (first run downloads ~117 MB model)`
15602
- });
15603
- try {
15604
- const { rebuildSemanticIndex } = await import("./semantic-FF6DDJI6.js");
15605
- const stats = await rebuildSemanticIndex(root);
15606
- const first = stats.modelFirstLoadMs ? ` (model load+first batch ${stats.modelFirstLoadMs}ms)` : "";
15607
- ctx.send({
15608
- type: "info",
15609
- message: `\u2713 Embedded ${stats.symbolsEmbedded} symbols in ${stats.durationMs}ms${first}`
15610
- });
15611
- } catch (err) {
15612
- const msg = err instanceof Error ? err.message : String(err);
15613
- ctx.send({ type: "error", message: `Semantic rebuild failed: ${msg}` });
15614
- }
15615
- } else if (sub === "semantic-clear") {
15616
- clearVectorStore(root);
15617
- ctx.send({ type: "info", message: "\u2713 Cleared semantic index (symbol index preserved)" });
15796
+ async function handleMcp(_args, ctx) {
15797
+ if (!ctx.mcpManager) {
15798
+ ctx.send({ type: "info", message: "MCP not configured. Add mcpServers to config.json." });
15799
+ return;
15800
+ }
15801
+ const sub = _args[0];
15802
+ if (sub === "reconnect") {
15803
+ ctx.send({ type: "info", message: "\u{1F504} Reconnecting MCP servers..." });
15804
+ try {
15805
+ await ctx.mcpManager.reconnectAll();
15806
+ ctx.send({ type: "info", message: "\u2713 MCP reconnect complete." });
15807
+ } catch (err) {
15808
+ ctx.send({ type: "error", message: `Reconnect failed: ${err.message}` });
15809
+ }
15810
+ ctx.sendToolsList();
15811
+ return;
15812
+ }
15813
+ const statuses = ctx.mcpManager.getStatus();
15814
+ if (statuses.length === 0) {
15815
+ ctx.send({ type: "info", message: "No MCP servers configured." });
15816
+ return;
15817
+ }
15818
+ const lines = [`\u{1F50C} **MCP Servers (${statuses.length}):**`, ""];
15819
+ for (const s of statuses) {
15820
+ const state2 = s.connected ? `\u2713 connected \xB7 ${s.serverName} \xB7 ${s.toolCount} tools` : `\u2717 disconnected${s.error ? ` \xB7 ${s.error}` : ""}`;
15821
+ lines.push(` ${s.serverId}: ${state2}`);
15822
+ }
15823
+ lines.push("");
15824
+ lines.push("Use /mcp reconnect to reconnect.");
15825
+ ctx.send({ type: "info", message: lines.join("\n") });
15826
+ }
15827
+ async function handleSkill(args, ctx) {
15828
+ const sub = args[0];
15829
+ if (!ctx.skillManager) {
15830
+ ctx.send({ type: "error", message: "Skill system not available." });
15831
+ return;
15832
+ }
15833
+ if (!sub || sub === "list") {
15834
+ const skills = ctx.skillManager.listSkills();
15835
+ const active = ctx.skillManager.getActive();
15836
+ if (skills.length === 0) {
15837
+ ctx.send({ type: "info", message: "No skills available. Add .md files to ~/.aicli/skills/" });
15618
15838
  } else {
15619
- ctx.send({
15620
- type: "error",
15621
- message: `Unknown subcommand: ${sub}. Use status/rebuild/clear/semantic-rebuild/semantic-clear.`
15839
+ const lines = skills.map((s) => {
15840
+ const isActive = active?.meta.name === s.meta.name;
15841
+ const marker = isActive ? " \u2705 (active)" : "";
15842
+ return ` ${s.meta.name}${marker} \u2014 ${s.meta.description || "(no description)"}`;
15622
15843
  });
15844
+ ctx.send({ type: "info", message: `\u{1F4DA} Skills:
15845
+ ${lines.join("\n")}` });
15623
15846
  }
15847
+ return;
15848
+ }
15849
+ if (sub === "off") {
15850
+ ctx.skillManager.deactivate();
15851
+ ctx.send({ type: "info", message: "\u{1F50C} Skill deactivated." });
15852
+ ctx.sendToolsList();
15853
+ ctx.sendStatus();
15854
+ return;
15855
+ }
15856
+ if (sub === "reload") {
15857
+ ctx.skillManager.loadSkills(getActivePluginAssets(ctx.config.getConfigDir()).skillDirs);
15858
+ ctx.send({ type: "info", message: `\u{1F504} Reloaded ${ctx.skillManager.listSkills().length} skill(s).` });
15859
+ ctx.sendToolsList();
15860
+ return;
15861
+ }
15862
+ const activated = ctx.skillManager.activate(sub);
15863
+ if (activated) {
15864
+ ctx.send({ type: "info", message: `\u2705 Skill activated: ${activated.meta.name}
15865
+ ${activated.meta.description || ""}` });
15866
+ ctx.sendToolsList();
15867
+ ctx.sendStatus();
15868
+ } else {
15869
+ const available = ctx.skillManager.listSkills().map((s) => s.meta.name).join(", ");
15870
+ ctx.send({ type: "error", message: `Skill "${sub}" not found. Available: ${available}` });
15624
15871
  }
15625
15872
  }
15626
- async function handleMemory(args, ctx) {
15627
- {
15628
- const sub = args[0];
15629
- if (sub === "add" && args.length > 1) {
15630
- ctx.memoryAdd(args.slice(1).join(" "));
15631
- } else if (sub === "clear") {
15632
- ctx.memoryClear();
15873
+ async function handleCommands(_args, ctx) {
15874
+ const configDir = ctx.config.getConfigDir();
15875
+ const commandsDir = join24(configDir, CUSTOM_COMMANDS_DIR_NAME);
15876
+ if (!existsSync29(commandsDir)) {
15877
+ ctx.send({ type: "info", message: `No custom commands directory.
15878
+ Create: ${commandsDir}/ with .md files.` });
15879
+ return;
15880
+ }
15881
+ try {
15882
+ const files = readdirSync13(commandsDir).filter((f) => f.endsWith(".md"));
15883
+ if (files.length === 0) {
15884
+ ctx.send({ type: "info", message: `No custom commands found in ${commandsDir}
15885
+ Add .md files to create commands.` });
15633
15886
  } else {
15634
- ctx.memoryShow();
15887
+ const lines = [`\u{1F4CB} **Custom Commands (${files.length}):**`, `Dir: ${commandsDir}`, ""];
15888
+ for (const f of files) {
15889
+ const name = f.replace(/\.md$/, "");
15890
+ lines.push(` /${name}`);
15891
+ }
15892
+ ctx.send({ type: "info", message: lines.join("\n") });
15635
15893
  }
15894
+ } catch {
15895
+ ctx.send({ type: "info", message: `Cannot read: ${commandsDir}` });
15636
15896
  }
15637
15897
  }
15638
- async function handleAddDir(args, ctx) {
15639
- {
15640
- const sub = args[0]?.trim();
15641
- if (!sub) {
15642
- ctx.send({ type: "info", message: ctx.addedDirs.size > 0 ? `\u{1F4C1} Added directories:
15643
- ${[...ctx.addedDirs].map((d) => ` \u2022 ${d}`).join("\n")}
15644
15898
 
15645
- Use /add-dir remove to clear.` : "No directories added.\nUsage: /add-dir <path> | /add-dir remove" });
15646
- }
15647
- if (sub === "remove" || sub === "clear") {
15648
- ctx.addedDirs.clear();
15649
- ctx.send({ type: "info", message: "\u2713 All added directories removed from context." });
15650
- }
15651
- const dirPath = resolve12(sub);
15652
- if (!existsSync29(dirPath)) {
15653
- ctx.send({ type: "error", message: `Directory not found: ${dirPath}` });
15654
- }
15655
- if (!statSync12(dirPath).isDirectory()) {
15656
- ctx.send({ type: "error", message: `Not a directory: ${dirPath}` });
15657
- }
15658
- ctx.addedDirs.add(dirPath);
15659
- ctx.send({ type: "info", message: `\u2713 Added directory: ${dirPath}
15660
- It will be included in AI context for subsequent messages.` });
15899
+ // src/web/commands/review-commands.ts
15900
+ async function handleReview(args, ctx) {
15901
+ const gitCtx = getGitContext();
15902
+ if (!gitCtx) {
15903
+ ctx.send({ type: "error", message: "Not a git repository." });
15904
+ return;
15661
15905
  }
15662
- }
15663
-
15664
- // src/diagnostics/doctor-report.ts
15665
- import { existsSync as existsSync31, statSync as statSync14 } from "fs";
15666
- import { join as join26 } from "path";
15667
- import { arch as arch2, platform as platform6, release as release2 } from "os";
15668
-
15669
- // src/diagnostics/crash-log.ts
15670
- import {
15671
- existsSync as existsSync30,
15672
- mkdirSync as mkdirSync16,
15673
- readdirSync as readdirSync15,
15674
- readFileSync as readFileSync24,
15675
- statSync as statSync13,
15676
- unlinkSync as unlinkSync6,
15677
- writeFileSync as writeFileSync5
15678
- } from "fs";
15679
- import { join as join25 } from "path";
15680
- import { homedir as homedir8, platform as platform5, release, arch } from "os";
15681
- var LOGS_DIR_NAME = "logs";
15682
- var CRASH_LOG_PREFIX = "crash-";
15683
- function getLogsDir(configDir) {
15684
- const base = configDir ?? join25(homedir8(), CONFIG_DIR_NAME);
15685
- return join25(base, LOGS_DIR_NAME);
15686
- }
15687
- function truncate(s, n) {
15688
- return s.length <= n ? s : s.slice(0, n) + "\u2026";
15689
- }
15690
- function listRecentCrashes(limit = 10, configDir) {
15691
- const dir = getLogsDir(configDir);
15692
- if (!existsSync30(dir)) return [];
15693
- const files = [];
15694
- let names;
15906
+ const staged = args.includes("--staged");
15907
+ const detailed = args.includes("--detailed");
15908
+ let diff;
15695
15909
  try {
15696
- names = readdirSync15(dir);
15910
+ diff = readGitDiff({ staged });
15697
15911
  } catch {
15698
- return [];
15912
+ ctx.send({ type: "error", message: "Failed to run git diff." });
15913
+ return;
15699
15914
  }
15700
- for (const name of names) {
15701
- if (!name.startsWith(CRASH_LOG_PREFIX) || !name.endsWith(".log")) continue;
15702
- const full = join25(dir, name);
15703
- try {
15704
- const st = statSync13(full);
15705
- const head = readFileSync24(full, "utf-8").split("\n").slice(0, 20);
15706
- const tsLine = head.find((l) => l.startsWith("timestamp:"));
15707
- const kindLine = head.find((l) => l.startsWith("kind:"));
15708
- const msgIdx = head.findIndex((l) => l.trim() === "## message");
15709
- const message = msgIdx >= 0 && head[msgIdx + 1] ? head[msgIdx + 1] : "";
15710
- files.push({
15711
- file: full,
15712
- timestamp: tsLine ? tsLine.replace("timestamp:", "").trim() : st.mtime.toISOString(),
15713
- kind: kindLine ? kindLine.replace("kind:", "").trim() : "unknown",
15714
- message: truncate(message, 200),
15715
- sizeBytes: st.size
15716
- });
15717
- } catch {
15718
- }
15915
+ if (!diff) {
15916
+ ctx.send({ type: "info", message: "No changes to review." + (staged ? "" : " Try --staged for staged changes.") });
15917
+ return;
15918
+ }
15919
+ const MAX_DIFF = 8e3;
15920
+ let truncated = false;
15921
+ if (diff.length > MAX_DIFF) {
15922
+ const head = diff.slice(0, Math.floor(MAX_DIFF * 0.7));
15923
+ const tail = diff.slice(diff.length - Math.floor(MAX_DIFF * 0.2));
15924
+ diff = head + "\n\n... [diff truncated, " + diff.length + " chars total] ...\n\n" + tail;
15925
+ truncated = true;
15926
+ }
15927
+ const reviewPrompt = buildReviewPrompt(diff, formatGitContextForPrompt(gitCtx), detailed);
15928
+ ctx.send({ type: "info", message: "\u{1F50D} Analyzing changes..." });
15929
+ try {
15930
+ const review = await ctx.chatOnce(reviewPrompt, { temperature: 0.3, maxTokens: 8192 });
15931
+ const msg = truncated ? review + "\n\n\u26A0 Diff was truncated. Consider reviewing smaller changesets." : review;
15932
+ ctx.send({ type: "info", message: msg });
15933
+ } catch (err) {
15934
+ ctx.send({ type: "error", message: `Review failed: ${err.message}` });
15719
15935
  }
15720
- files.sort((a, b) => a.timestamp < b.timestamp ? 1 : -1);
15721
- return files.slice(0, limit);
15722
15936
  }
15723
- function getConfigDirUsage(configDir) {
15724
- const base = configDir ?? join25(homedir8(), CONFIG_DIR_NAME);
15725
- if (!existsSync30(base)) return { totalBytes: 0, entries: [] };
15726
- const entries = [];
15727
- let total = 0;
15728
- let names;
15937
+ async function handleSecurityReview(args, ctx) {
15938
+ const gitCtx = getGitContext();
15939
+ if (!gitCtx) {
15940
+ ctx.send({ type: "error", message: "Not a git repository." });
15941
+ return;
15942
+ }
15943
+ const secStaged = args.includes("--staged");
15944
+ let secDiff;
15729
15945
  try {
15730
- names = readdirSync15(base);
15946
+ secDiff = readGitDiff({ staged: secStaged });
15731
15947
  } catch {
15732
- return { totalBytes: 0, entries: [] };
15948
+ ctx.send({ type: "error", message: "Failed to run git diff." });
15949
+ return;
15733
15950
  }
15734
- for (const name of names) {
15735
- const full = join25(base, name);
15736
- const bytes = dirSize(full);
15737
- entries.push({ name, bytes });
15738
- total += bytes;
15951
+ if (!secDiff) {
15952
+ ctx.send({ type: "info", message: "No changes to review." + (secStaged ? "" : " Try --staged for staged changes.") });
15953
+ return;
15954
+ }
15955
+ const SEC_MAX_DIFF = 8e3;
15956
+ let secTruncated = false;
15957
+ if (secDiff.length > SEC_MAX_DIFF) {
15958
+ const head = secDiff.slice(0, Math.floor(SEC_MAX_DIFF * 0.7));
15959
+ const tail = secDiff.slice(secDiff.length - Math.floor(SEC_MAX_DIFF * 0.2));
15960
+ secDiff = head + "\n\n... [diff truncated, " + secDiff.length + " chars total] ...\n\n" + tail;
15961
+ secTruncated = true;
15962
+ }
15963
+ const secPrompt = buildSecurityReviewPrompt(secDiff, formatGitContextForPrompt(gitCtx));
15964
+ ctx.send({ type: "info", message: "\u{1F512} Scanning for security vulnerabilities..." });
15965
+ try {
15966
+ const secReview = await ctx.chatOnce(secPrompt, { temperature: 0.2, maxTokens: 8192 });
15967
+ const secMsg = secTruncated ? secReview + "\n\n\u26A0 Diff was truncated. Consider reviewing smaller changesets." : secReview;
15968
+ ctx.send({ type: "info", message: secMsg });
15969
+ } catch (err) {
15970
+ ctx.send({ type: "error", message: `Security review failed: ${err.message}` });
15739
15971
  }
15740
- entries.sort((a, b) => b.bytes - a.bytes);
15741
- return { totalBytes: total, entries };
15742
15972
  }
15743
- function dirSize(path3) {
15973
+ async function handleTest(args, ctx) {
15974
+ ctx.send({ type: "info", message: "\u{1F9EA} Running tests..." });
15744
15975
  try {
15745
- const st = statSync13(path3);
15746
- if (st.isFile()) return st.size;
15747
- if (!st.isDirectory()) return 0;
15748
- let sum = 0;
15749
- for (const name of readdirSync15(path3)) {
15750
- sum += dirSize(join25(path3, name));
15751
- }
15752
- return sum;
15753
- } catch {
15754
- return 0;
15976
+ const { executeTests } = await import("./run-tests-S36TE2W2.js");
15977
+ const argStr = args.join(" ").trim();
15978
+ let testArgs = {};
15979
+ if (argStr) {
15980
+ const isCommand = argStr.includes(" ") || /^(mvn|gradle|npm|pytest|cargo|go)\b/.test(argStr);
15981
+ testArgs = isCommand ? { command: argStr } : { filter: argStr };
15982
+ }
15983
+ const report = await executeTests(testArgs);
15984
+ ctx.send({ type: "info", message: report });
15985
+ } catch (err) {
15986
+ ctx.send({ type: "error", message: `Test failed: ${err.message}` });
15755
15987
  }
15756
15988
  }
15757
-
15758
- // src/diagnostics/doctor-report.ts
15759
- function checkFile(label, path3) {
15760
- const exists = existsSync31(path3);
15761
- let sizeBytes = null;
15762
- if (exists) {
15763
- try {
15764
- sizeBytes = statSync14(path3).size;
15765
- } catch {
15989
+ async function handleDiff(args, ctx) {
15990
+ const history = undoStack.getHistory();
15991
+ if (history.length === 0) {
15992
+ ctx.send({ type: "info", message: "No file modifications in this session." });
15993
+ return;
15994
+ }
15995
+ const fileMap = /* @__PURE__ */ new Map();
15996
+ for (const entry of history) {
15997
+ if (!fileMap.has(entry.filePath)) {
15998
+ fileMap.set(entry.filePath, { earliest: entry.previousContent });
15766
15999
  }
15767
16000
  }
15768
- return { label, path: path3, exists, sizeBytes };
15769
- }
15770
- function buildDoctorReport(options) {
15771
- const config = options.config;
15772
- const cwd = options.cwd ?? process.cwd();
15773
- const configDir = config.getConfigDir();
15774
- const projectRoot = getGitRoot(cwd) ?? cwd;
15775
- const contextCfg = config.get("context");
15776
- const contextSetting = config.get("contextFile");
15777
- const context = loadContextFiles({
15778
- cwd,
15779
- configDir,
15780
- projectRoot,
15781
- setting: contextSetting,
15782
- maxBytes: contextCfg.projectDocMaxBytes,
15783
- fallbackFilenames: contextCfg.projectDocFallbackFilenames
15784
- });
15785
- const hooksConfig = config.get("hooks");
15786
- const hooks = listHooks(hooksConfig, configDir);
15787
- const pendingHooks = getPendingHookTrust(hooksConfig, configDir);
15788
- const plugins = listInstalledPlugins(configDir);
15789
- const activeAssets = getActivePluginAssets(configDir);
15790
- const network = config.get("networkPolicy");
15791
- const mcpServers = config.get("mcpServers");
15792
- const allStats = getStatsSnapshot();
15793
- return {
15794
- version: VERSION,
15795
- npmCheck: options.npmCheck,
15796
- node: process.version,
15797
- platform: `${platform6()} ${release2()} (${arch2()})`,
15798
- cwd,
15799
- projectRoot,
15800
- configDir,
15801
- providers: options.providers.listAll().map((p) => ({ id: p.id, displayName: p.displayName, configured: p.configured })),
15802
- files: [
15803
- checkFile("config.json", join26(configDir, "config.json")),
15804
- checkFile("memory.md", join26(configDir, MEMORY_FILE_NAME)),
15805
- checkFile("memory.jsonl", join26(configDir, "memory.jsonl")),
15806
- checkFile("dev-state.md", join26(configDir, DEV_STATE_FILE_NAME)),
15807
- checkFile("hooks-trust.json", join26(configDir, "hooks-trust.json")),
15808
- checkFile("plugin-state.json", join26(configDir, "plugins", "plugin-state.json"))
15809
- ],
15810
- context: {
15811
- enabled: contextSetting !== false,
15812
- setting: contextSetting,
15813
- candidates: context.candidates,
15814
- maxBytes: context.maxBytes,
15815
- layers: context.layers.map((l) => ({ level: l.level, fileName: l.fileName, displayPath: l.displayPath, bytes: l.byteCount, chars: l.charCount, truncated: l.truncated })),
15816
- skipped: context.skipped.map((s) => ({ level: s.level, fileName: s.fileName, displayPath: s.displayPath, reason: s.reason, message: s.message }))
15817
- },
15818
- permissions: {
15819
- defaultProfile: config.get("defaultPermissionProfile"),
15820
- allowedProfiles: config.get("allowedPermissionProfiles"),
15821
- customProfileCount: Object.keys(config.get("permissionProfiles")).length,
15822
- legacyDefaultAction: config.get("defaultPermission"),
15823
- ruleCount: config.get("permissionRules").length
15824
- },
15825
- networkPolicy: {
15826
- enabled: network.enabled,
15827
- defaultAction: network.defaultAction,
15828
- allowDomains: network.allowDomains.length,
15829
- denyDomains: network.denyDomains.length,
15830
- allowPorts: network.allowPorts.length,
15831
- denyPorts: network.denyPorts.length,
15832
- allowPrivateNetwork: network.allowPrivateNetwork,
15833
- toolOverrides: Object.fromEntries(Object.entries(network.tools).filter(([, v]) => v !== void 0))
15834
- },
15835
- hooks: {
15836
- enabled: hooksConfig?.enabled !== false,
15837
- total: hooks.length,
15838
- project: hooks.filter((h) => h.source === "project").length,
15839
- pendingTrust: pendingHooks.length,
15840
- disabled: hooks.filter((h) => h.disabled).length
15841
- },
15842
- plugins: {
15843
- root: join26(configDir, "plugins"),
15844
- installed: plugins.length,
15845
- active: activeAssets.plugins.length,
15846
- invalid: plugins.filter((p) => !p.valid).length,
15847
- untrusted: plugins.filter((p) => p.valid && !p.trusted).length,
15848
- disabled: plugins.filter((p) => p.valid && !p.enabled).length,
15849
- mcpServers: Object.keys(activeAssets.mcpServers).length,
15850
- skillDirs: activeAssets.skillDirs.length,
15851
- commandDirs: activeAssets.commandDirs.length,
15852
- agentDirs: activeAssets.agentDirs.length,
15853
- entries: plugins.map((p) => ({ name: p.name, version: p.manifest.version, enabled: p.enabled, trusted: p.trusted, valid: p.valid, error: p.error }))
15854
- },
15855
- mcp: {
15856
- enabled: config.get("mcpEnabled"),
15857
- configured: Object.keys(mcpServers).length,
15858
- projectConfigExists: existsSync31(join26(projectRoot, MCP_PROJECT_CONFIG_NAME)),
15859
- statuses: options.mcpStatuses ?? []
15860
- },
15861
- recentCrashes: listRecentCrashes(5, configDir),
15862
- diskUsage: getConfigDirUsage(configDir),
15863
- toolStats: {
15864
- totalCalls: allStats.reduce((a, b) => a + b.calls, 0),
15865
- totalFailures: allStats.reduce((a, b) => a + b.failures, 0),
15866
- topUsed: getTopUsedTools(5),
15867
- topFailing: getTopFailingTools(5)
16001
+ const statsOnly = args[0]?.toLowerCase() === "--stats";
16002
+ let newFiles = 0;
16003
+ let modifiedFiles = 0;
16004
+ const diffLines2 = [];
16005
+ for (const [filePath, { earliest }] of fileMap) {
16006
+ const currentContent = existsSync29(filePath) ? (() => {
16007
+ try {
16008
+ return readFileSync22(filePath, "utf-8");
16009
+ } catch {
16010
+ return null;
16011
+ }
16012
+ })() : null;
16013
+ const isNew = earliest === null;
16014
+ if (isNew) newFiles++;
16015
+ else modifiedFiles++;
16016
+ if (statsOnly) continue;
16017
+ if (isNew) {
16018
+ diffLines2.push(`
16019
+ \u{1F4C4} [NEW] ${filePath}`);
16020
+ if (currentContent !== null) {
16021
+ diffLines2.push(` ${currentContent.split("\n").length} lines`);
16022
+ } else {
16023
+ diffLines2.push(" (file was deleted after creation)");
16024
+ }
16025
+ } else {
16026
+ diffLines2.push(`
16027
+ \u{1F4DD} [MODIFIED] ${filePath}`);
16028
+ const oldText = earliest ?? "";
16029
+ const newText = currentContent ?? "";
16030
+ if (oldText === newText) {
16031
+ diffLines2.push(" (no net change)");
16032
+ } else {
16033
+ diffLines2.push(renderDiff(oldText, newText, { filePath, maxLines: 80 }));
16034
+ }
15868
16035
  }
15869
- };
16036
+ }
16037
+ diffLines2.push(`
16038
+ **Summary:** ${fileMap.size} file(s) \u2014 ${newFiles} new, ${modifiedFiles} modified`);
16039
+ ctx.send({ type: "info", message: diffLines2.join("\n") });
16040
+ }
16041
+ async function handleInit(args, ctx) {
16042
+ const cwd = process.cwd();
16043
+ const targetPath = join24(cwd, "AICLI.md");
16044
+ const force = args.includes("--force");
16045
+ if (existsSync29(targetPath) && !force) {
16046
+ ctx.send({ type: "info", message: `AICLI.md already exists at ${targetPath}
16047
+ Use /init --force to overwrite.` });
16048
+ return;
16049
+ }
16050
+ ctx.send({ type: "info", message: "\u{1F4DD} Scanning project structure..." });
16051
+ try {
16052
+ const projectInfo = scanProject(cwd);
16053
+ const prompt = buildInitPrompt(projectInfo, cwd);
16054
+ const content = await ctx.chatOnce(prompt, { temperature: 0.3, maxTokens: 4096 });
16055
+ writeFileSync3(targetPath, content, "utf-8");
16056
+ ctx.send({ type: "info", message: `\u2713 Generated: ${targetPath} (${content.length} chars)
16057
+ Use /context reload to load it.` });
16058
+ } catch (err) {
16059
+ ctx.send({ type: "error", message: `Failed to generate AICLI.md: ${err.message}` });
16060
+ }
15870
16061
  }
16062
+ async function handleScaffold(args, ctx) {
16063
+ const description = args.join(" ").trim();
16064
+ if (!description) {
16065
+ ctx.send({ type: "error", message: "Usage: /scaffold <project description>" });
16066
+ return;
16067
+ }
16068
+ ctx.send({ type: "info", message: "\u{1F3D7}\uFE0F Generating project scaffold..." });
16069
+ const scaffoldPrompt = `Please scaffold a project based on this description: ${description}
15871
16070
 
15872
- // src/diagnostics/doctor-cli.ts
15873
- function formatBytes(n) {
15874
- if (n < 1024) return `${n} B`;
15875
- if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
15876
- if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;
15877
- return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
16071
+ Create the necessary files and directory structure. Use write_file and bash tools to set up the project.`;
16072
+ await ctx.handleChat(scaffoldPrompt);
15878
16073
  }
15879
- function formatDoctorReport(report, ansi = true) {
15880
- const wrap = (code, s) => ansi ? `${code}${s}\x1B[0m` : s;
15881
- const B = (s) => wrap("\x1B[1m", s);
15882
- const D = (s) => wrap("\x1B[2m", s);
15883
- const G = (s) => wrap("\x1B[32m", s);
15884
- const Y = (s) => wrap("\x1B[33m", s);
15885
- const R = (s) => wrap("\x1B[31m", s);
15886
- const out = [];
15887
- out.push("");
15888
- out.push(B("\u{1FA7A} AI-CLI Health Check"));
15889
- out.push("");
15890
- const nc = report.npmCheck;
15891
- let versionLine = ` version: ${report.version}`;
15892
- if (nc?.status === "up-to-date") versionLine += ` ${D("(latest on npm)")}`;
15893
- else if (nc?.status === "outdated" && nc.latest) versionLine += ` ${Y(`\u2192 ${nc.latest} available`)} ${D("(npm i -g jinzd-ai-cli@latest)")}`;
15894
- else if (nc?.status === "skipped") versionLine += ` ${D(`(npm check skipped: ${nc.reason ?? "unknown"})`)}`;
15895
- out.push(versionLine);
15896
- out.push(` node: ${report.node}`);
15897
- out.push(` platform: ${report.platform}`);
15898
- out.push(` cwd: ${report.cwd}`);
15899
- out.push(` project: ${report.projectRoot}`);
15900
- out.push(` config: ${report.configDir}`);
15901
- out.push("");
15902
- out.push(B("API Keys:"));
15903
- for (const p of report.providers) {
15904
- out.push(` ${p.configured ? G("\u2713") : D("\u25CB")} ${p.id.padEnd(14)} ${p.configured ? G("configured") : D("not configured")}`);
16074
+
16075
+ // src/web/commands/memory-commands.ts
16076
+ async function handleSystem(args, ctx) {
16077
+ const text = args.join(" ").trim();
16078
+ if (!text) {
16079
+ const current = ctx.activeSystemPrompt;
16080
+ if (current) {
16081
+ const preview = current.length > 500 ? current.slice(0, 500) + "..." : current;
16082
+ ctx.send({ type: "info", message: `\u{1F4CB} Current system prompt (${current.length} chars):
16083
+
16084
+ ${preview}` });
16085
+ } else {
16086
+ ctx.send({ type: "info", message: "No custom system prompt set. Using default.\nUsage: /system <prompt>" });
16087
+ }
16088
+ return;
15905
16089
  }
15906
- out.push("");
15907
- out.push(B("Context Files:"));
15908
- out.push(` status: ${report.context.enabled ? G("enabled") : D("disabled")} \xB7 max ${formatBytes(report.context.maxBytes)}`);
15909
- if (report.context.layers.length === 0) out.push(` ${D("(no context files loaded)")}`);
15910
- for (const l of report.context.layers) {
15911
- out.push(` ${G("\u2713")} ${l.level.padEnd(7)} ${l.displayPath} ${D(`(${formatBytes(l.bytes)}${l.truncated ? ", truncated" : ""})`)}`);
16090
+ if (text === "clear" || text === "reset") {
16091
+ ctx.activeSystemPrompt = ctx.loadContextFiles();
16092
+ ctx.send({ type: "info", message: "\u2713 System prompt reset to default (context files)." });
16093
+ } else {
16094
+ ctx.activeSystemPrompt = text;
16095
+ ctx.send({ type: "info", message: `\u2713 System prompt set (${text.length} chars).` });
15912
16096
  }
15913
- if (report.context.skipped.length > 0) {
15914
- out.push(` ${D("skipped:")}`);
15915
- for (const s of report.context.skipped.slice(0, 5)) {
15916
- out.push(` ${Y("!")} ${s.level}:${s.fileName} ${s.reason}${s.message ? ` \xB7 ${s.message}` : ""}`);
16097
+ }
16098
+ async function handleContext(_args, ctx) {
16099
+ const sub = _args[0];
16100
+ if (sub === "reload") {
16101
+ ctx.activeSystemPrompt = ctx.loadContextFiles();
16102
+ ctx.send({ type: "info", message: ctx.activeSystemPrompt ? `\u2713 Context reloaded (${ctx.activeSystemPrompt.length} chars).` : "\u2713 No context files found." });
16103
+ return;
16104
+ }
16105
+ const result = ctx.contextLoadResult;
16106
+ const layers = ["\u{1F4DA} **Context Layers:**", ""];
16107
+ if (result && result.layers.length > 0) {
16108
+ const labels = { global: "Global ", project: "Project", plugin: "Plugin ", local: "Local " };
16109
+ for (const layer of result.layers) {
16110
+ layers.push(` \u2713 ${labels[layer.level] ?? layer.level}: ${layer.displayPath} (${layer.charCount} chars${layer.truncated ? ", truncated" : ""})`);
15917
16111
  }
16112
+ layers.push(` Total: ${result.totalChars} chars (${result.layers.length} layer${result.layers.length > 1 ? "s" : ""})`);
16113
+ } else {
16114
+ layers.push(" No context files loaded.");
16115
+ layers.push(" Search order per layer: AICLI.override.md \u2192 AGENTS.override.md \u2192 AICLI.md \u2192 CLAUDE.md \u2192 AGENTS.md");
15918
16116
  }
15919
- out.push("");
15920
- out.push(B("Permissions / Network:"));
15921
- out.push(` profile: ${report.permissions.defaultProfile} \xB7 allowed ${report.permissions.allowedProfiles.join(", ")} \xB7 rules ${report.permissions.ruleCount} \xB7 custom profiles ${report.permissions.customProfileCount}`);
15922
- const np = report.networkPolicy;
15923
- out.push(` networkPolicy: ${np.enabled ? G("enabled") : D("disabled")} \xB7 default ${np.defaultAction} \xB7 allow ${np.allowDomains} domain(s)/${np.allowPorts} port(s) \xB7 deny ${np.denyDomains} domain(s)/${np.denyPorts} port(s) \xB7 private ${np.allowPrivateNetwork ? "allow" : "block/confirm"}`);
15924
- const overrides = Object.entries(np.toolOverrides).map(([k, v]) => `${k}=${v}`).join(", ");
15925
- if (overrides) out.push(` tool overrides: ${overrides}`);
15926
- out.push("");
15927
- out.push(B("Hooks / Plugins:"));
15928
- out.push(` hooks: ${report.hooks.enabled ? G("enabled") : D("disabled")} \xB7 total ${report.hooks.total} \xB7 project ${report.hooks.project} \xB7 pending trust ${report.hooks.pendingTrust} \xB7 disabled ${report.hooks.disabled}`);
15929
- out.push(` plugins: installed ${report.plugins.installed} \xB7 active ${report.plugins.active} \xB7 disabled ${report.plugins.disabled} \xB7 untrusted ${report.plugins.untrusted} \xB7 invalid ${report.plugins.invalid}`);
15930
- out.push(` plugin assets: ${report.plugins.skillDirs} skill dir(s), ${report.plugins.commandDirs} command dir(s), ${report.plugins.agentDirs} agent dir(s), ${report.plugins.mcpServers} MCP server(s)`);
15931
- if (report.plugins.entries.length > 0) {
15932
- for (const p of report.plugins.entries.slice(0, 5)) {
15933
- const state2 = p.valid ? `${p.enabled ? "enabled" : "disabled"} \xB7 ${p.trusted ? "trusted" : "untrusted"}` : `invalid \xB7 ${p.error ?? ""}`;
15934
- out.push(` ${p.valid ? G("\u2713") : R("\u2717")} ${p.name}@${p.version} ${D(state2)}`);
16117
+ if (result && result.skipped.length > 0) {
16118
+ layers.push("");
16119
+ layers.push("Skipped:");
16120
+ for (const skipped of result.skipped.slice(0, 5)) {
16121
+ layers.push(` - ${skipped.displayPath}: ${skipped.reason}${skipped.message ? ` (${skipped.message})` : ""}`);
15935
16122
  }
15936
16123
  }
15937
- out.push("");
15938
- out.push(B("MCP Servers:"));
15939
- out.push(` global configured: ${report.mcp.configured} \xB7 project .mcp.json: ${report.mcp.projectConfigExists ? "yes" : "no"} \xB7 mcpEnabled: ${report.mcp.enabled ? "true" : "false"}`);
15940
- if (report.mcp.statuses.length === 0) out.push(` ${D("(no connected manager status in this mode)")}`);
15941
- for (const s of report.mcp.statuses) {
15942
- const state2 = s.connected ? `${G("connected")} \xB7 ${s.serverName} \xB7 ${s.toolCount} tools` : `${R("disconnected")}${s.error ? D(` \xB7 ${s.error}`) : ""}`;
15943
- out.push(` ${s.connected ? G("\u2713") : R("\u2717")} ${s.serverId.padEnd(16)} ${state2}`);
16124
+ layers.push("");
16125
+ layers.push(`Total prompt length: ${ctx.activeSystemPrompt?.length ?? 0} chars`);
16126
+ layers.push("Use /context reload to refresh.");
16127
+ ctx.send({ type: "info", message: layers.join("\n") });
16128
+ }
16129
+ async function handleIndex(args, ctx) {
16130
+ const sub = (args[0] ?? "status").toLowerCase();
16131
+ const root = process.cwd();
16132
+ const { loadIndex: loadIndex2, clearIndex } = await import("./store-VO37H6LS.js");
16133
+ const { indexProject: indexProject2 } = await import("./indexer-4WWS3VIL.js");
16134
+ const { loadVectorStore, clearVectorStore } = await import("./vector-store-JBAE6PS4.js");
16135
+ if (sub === "status") {
16136
+ const idx = loadIndex2(root);
16137
+ const vec = loadVectorStore(root);
16138
+ if (!idx) {
16139
+ ctx.send({ type: "info", message: `No symbol index for ${root}. Run /index rebuild.` });
16140
+ return;
16141
+ }
16142
+ const semantic = vec ? `${vec.count} vectors \xD7 ${vec.dim}-dim` : "not built \u2014 run /index semantic-rebuild to enable search_code";
16143
+ ctx.send({
16144
+ type: "info",
16145
+ message: [
16146
+ "\u{1F50E} Symbol index:",
16147
+ ` Root: ${idx.root}`,
16148
+ ` Generated: ${idx.generated}`,
16149
+ ` Files: ${idx.fileCount}`,
16150
+ ` Symbols: ${idx.symbolCount}`,
16151
+ ` Semantic: ${semantic}`
16152
+ ].join("\n")
16153
+ });
16154
+ } else if (sub === "rebuild") {
16155
+ ctx.send({ type: "info", message: `Indexing ${root}\u2026` });
16156
+ const { stats } = await indexProject2(root, { force: true });
16157
+ ctx.send({
16158
+ type: "info",
16159
+ message: `\u2713 Indexed ${stats.filesParsed} files (${stats.symbols} symbols) in ${stats.durationMs}ms`
16160
+ });
16161
+ } else if (sub === "clear") {
16162
+ clearIndex(root);
16163
+ clearVectorStore(root);
16164
+ ctx.send({ type: "info", message: `\u2713 Cleared symbol + semantic index for ${root}` });
16165
+ } else if (sub === "semantic-rebuild") {
16166
+ const idx = loadIndex2(root);
16167
+ if (!idx) {
16168
+ ctx.send({ type: "error", message: "No symbol index yet. Run /index rebuild first." });
16169
+ return;
16170
+ }
16171
+ if (idx.symbolCount === 0) {
16172
+ ctx.send({
16173
+ type: "error",
16174
+ 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."
16175
+ });
16176
+ return;
16177
+ }
16178
+ ctx.send({
16179
+ type: "info",
16180
+ message: `Building semantic index for ${idx.symbolCount} symbols\u2026 (first run downloads ~117 MB model)`
16181
+ });
16182
+ try {
16183
+ const { rebuildSemanticIndex } = await import("./semantic-FF6DDJI6.js");
16184
+ const stats = await rebuildSemanticIndex(root);
16185
+ const first = stats.modelFirstLoadMs ? ` (model load+first batch ${stats.modelFirstLoadMs}ms)` : "";
16186
+ ctx.send({
16187
+ type: "info",
16188
+ message: `\u2713 Embedded ${stats.symbolsEmbedded} symbols in ${stats.durationMs}ms${first}`
16189
+ });
16190
+ } catch (err) {
16191
+ const msg = err instanceof Error ? err.message : String(err);
16192
+ ctx.send({ type: "error", message: `Semantic rebuild failed: ${msg}` });
16193
+ }
16194
+ } else if (sub === "semantic-clear") {
16195
+ clearVectorStore(root);
16196
+ ctx.send({ type: "info", message: "\u2713 Cleared semantic index (symbol index preserved)" });
16197
+ } else {
16198
+ ctx.send({
16199
+ type: "error",
16200
+ message: `Unknown subcommand: ${sub}. Use status/rebuild/clear/semantic-rebuild/semantic-clear.`
16201
+ });
15944
16202
  }
15945
- out.push("");
15946
- out.push(B("Config Files:"));
15947
- for (const f of report.files) {
15948
- const extra = f.exists && f.sizeBytes !== null ? ` ${D(`(${formatBytes(f.sizeBytes)})`)}` : "";
15949
- out.push(` ${f.exists ? G("\u2713") : D("\u2013")} ${f.label.padEnd(16)} ${f.exists ? f.path : D("(not found)")}${extra}`);
16203
+ }
16204
+ async function handleMemory(args, ctx) {
16205
+ const sub = args[0];
16206
+ if (sub === "add" && args.length > 1) {
16207
+ ctx.memoryAdd(args.slice(1).join(" "));
16208
+ } else if (sub === "clear") {
16209
+ ctx.memoryClear();
16210
+ } else {
16211
+ ctx.memoryShow();
15950
16212
  }
15951
- out.push("");
15952
- out.push(B("Recent Crashes (last 5):"));
15953
- if (report.recentCrashes.length === 0) out.push(` ${D("(none - clean!)")}`);
15954
- for (const c of report.recentCrashes) {
15955
- out.push(` ${R("\u2717")} ${c.timestamp} ${Y(c.kind)}`);
15956
- out.push(` ${D(c.message)}`);
15957
- out.push(` ${D(c.file)}`);
16213
+ }
16214
+ async function handleAddDir(args, ctx) {
16215
+ const sub = args[0]?.trim();
16216
+ if (!sub) {
16217
+ ctx.send({ type: "info", message: ctx.addedDirs.size > 0 ? `\u{1F4C1} Added directories:
16218
+ ${[...ctx.addedDirs].map((d) => ` \u2022 ${d}`).join("\n")}
16219
+
16220
+ Use /add-dir remove to clear.` : "No directories added.\nUsage: /add-dir <path> | /add-dir remove" });
16221
+ return;
15958
16222
  }
15959
- out.push("");
15960
- const ts = report.toolStats;
15961
- out.push(B("Tool Usage:"));
15962
- out.push(` total: ${ts.totalCalls} calls \xB7 ${ts.totalFailures} failures (${ts.totalCalls ? (ts.totalFailures * 100 / ts.totalCalls).toFixed(1) : "0.0"}%)`);
15963
- if (ts.topUsed.length > 0) {
15964
- out.push(` ${D("top used:")}`);
15965
- for (const t of ts.topUsed) out.push(` ${t.name.padEnd(20)} ${String(t.calls).padStart(6)} calls \xB7 avg ${t.calls ? (t.totalDurationMs / t.calls).toFixed(0) : "0"}ms`);
16223
+ if (sub === "remove" || sub === "clear") {
16224
+ ctx.addedDirs.clear();
16225
+ ctx.send({ type: "info", message: "\u2713 All added directories removed from context." });
16226
+ return;
15966
16227
  }
15967
- if (ts.topFailing.length > 0) {
15968
- out.push(` ${D("top failing:")}`);
15969
- for (const t of ts.topFailing) out.push(` ${R(t.name.padEnd(20))} ${t.failures}/${t.calls} (${(t.failures * 100 / Math.max(1, t.calls)).toFixed(1)}%) ${D(t.lastFailureMessage ?? "")}`.slice(0, 200));
16228
+ const dirPath = resolve10(sub);
16229
+ if (!existsSync29(dirPath)) {
16230
+ ctx.send({ type: "error", message: `Directory not found: ${dirPath}` });
16231
+ return;
15970
16232
  }
15971
- out.push("");
15972
- const dirAnnotations = {
15973
- models: "semantic search model -> /index semantic-clear",
15974
- index: "symbol index -> /index clear",
15975
- history: "conversation history",
15976
- logs: "crash logs",
15977
- "memory-index": "chat memory index -> /memory index-clear",
15978
- plugins: "installed package plugins"
15979
- };
15980
- out.push(B("Disk Usage (~/.aicli):"));
15981
- out.push(` total: ${formatBytes(report.diskUsage.totalBytes)}`);
15982
- for (const e of report.diskUsage.entries.slice(0, 10)) {
15983
- const note = dirAnnotations[e.name];
15984
- out.push(` ${e.name.padEnd(18)} ${formatBytes(e.bytes)}${note ? ` ${D(`\xB7 ${note}`)}` : ""}`);
16233
+ if (!statSync12(dirPath).isDirectory()) {
16234
+ ctx.send({ type: "error", message: `Not a directory: ${dirPath}` });
16235
+ return;
15985
16236
  }
15986
- out.push("");
15987
- out.push(G("\u2713 Health check complete"));
15988
- out.push("");
15989
- return out.join("\n");
16237
+ ctx.addedDirs.add(dirPath);
16238
+ ctx.send({ type: "info", message: `\u2713 Added directory: ${dirPath}
16239
+ It will be included in AI context for subsequent messages.` });
15990
16240
  }
15991
16241
 
15992
16242
  // src/web/commands/security-commands.ts
15993
16243
  async function handleConfig(args, ctx) {
15994
- {
15995
- const sub = args[0];
15996
- if (sub === "show" || !sub) {
15997
- ctx.send({ type: "info", message: `\u2699\uFE0F Configuration:
16244
+ const sub = args[0];
16245
+ if (sub === "show" || !sub) {
16246
+ ctx.send({ type: "info", message: `\u2699\uFE0F Configuration:
15998
16247
  \`\`\`json
15999
16248
  ${ctx.config.toFormattedJSON()}
16000
16249
  \`\`\`` });
16001
- } else if (sub === "get" && args[1]) {
16002
- const val = ctx.config.getByPath(args[1]);
16003
- const display = typeof val === "object" ? JSON.stringify(val, null, 2) : String(val ?? "undefined");
16004
- ctx.send({ type: "info", message: `${args[1]} = ${display}` });
16005
- } else if (sub === "set" && args[1] && args[2] !== void 0) {
16006
- try {
16007
- ctx.config.setByPath(args[1], args.slice(2).join(" "));
16008
- ctx.send({ type: "info", message: `\u2713 ${args[1]} = ${args.slice(2).join(" ")}` });
16009
- } catch (err) {
16010
- ctx.send({ type: "error", message: `Config error: ${err.message}` });
16011
- }
16012
- } else {
16013
- ctx.send({ type: "info", message: "Usage: /config [show] | /config get <key> | /config set <key> <value>" });
16250
+ } else if (sub === "get" && args[1]) {
16251
+ const val = ctx.config.getByPath(args[1]);
16252
+ const display = typeof val === "object" ? JSON.stringify(val, null, 2) : String(val ?? "undefined");
16253
+ ctx.send({ type: "info", message: `${args[1]} = ${display}` });
16254
+ } else if (sub === "set" && args[1] && args[2] !== void 0) {
16255
+ try {
16256
+ ctx.config.setByPath(args[1], args.slice(2).join(" "));
16257
+ ctx.send({ type: "info", message: `\u2713 ${args[1]} = ${args.slice(2).join(" ")}` });
16258
+ } catch (err) {
16259
+ ctx.send({ type: "error", message: `Config error: ${err.message}` });
16014
16260
  }
16261
+ } else {
16262
+ ctx.send({ type: "info", message: "Usage: /config [show] | /config get <key> | /config set <key> <value>" });
16015
16263
  }
16016
16264
  }
16017
- async function handleDoctor(args, ctx) {
16018
- {
16019
- const report = buildDoctorReport({
16020
- config: ctx.config,
16021
- providers: ctx.providers,
16022
- cwd: process.cwd(),
16023
- mcpStatuses: ctx.mcpManager?.getStatus() ?? []
16024
- });
16025
- ctx.send({ type: "info", message: formatDoctorReport(report, false) });
16026
- }
16027
- }
16028
- async function handleBug(args, ctx) {
16029
- {
16030
- const session = ctx.sessions.current;
16031
- const lines = [
16032
- "\u{1F41B} **Bug Report Template**",
16033
- "",
16034
- "**Environment:**",
16035
- ` ai-cli version: ${VERSION}`,
16036
- ` Node.js: ${process.version}`,
16037
- ` OS: ${process.platform} ${process.arch}`,
16038
- ` Provider: ${ctx.currentProvider}`,
16039
- ` Model: ${ctx.currentModel}`,
16040
- ` Session: ${session?.id.slice(0, 8) ?? "none"} (${session?.messages.length ?? 0} messages)`,
16041
- "",
16042
- "**Description:**",
16043
- "(Describe the bug)",
16044
- "",
16045
- "**Steps to reproduce:**",
16046
- "1. ...",
16047
- "",
16048
- "**Expected behavior:**",
16049
- "...",
16050
- "",
16051
- "**Actual behavior:**",
16052
- "...",
16053
- "",
16054
- "Report at: https://github.com/jinzhengdong/ai-cli/issues"
16055
- ];
16056
- ctx.send({ type: "info", message: lines.join("\n") });
16057
- }
16265
+ async function handleDoctor(_args, ctx) {
16266
+ const report = buildDoctorReport({
16267
+ config: ctx.config,
16268
+ providers: ctx.providers,
16269
+ cwd: process.cwd(),
16270
+ mcpStatuses: ctx.mcpManager?.getStatus() ?? []
16271
+ });
16272
+ ctx.send({ type: "info", message: formatDoctorReport(report, false) });
16273
+ }
16274
+ async function handleBug(_args, ctx) {
16275
+ const session = ctx.sessions.current;
16276
+ const lines = [
16277
+ "\u{1F41B} **Bug Report Template**",
16278
+ "",
16279
+ "**Environment:**",
16280
+ ` ai-cli version: ${VERSION}`,
16281
+ ` Node.js: ${process.version}`,
16282
+ ` OS: ${process.platform} ${process.arch}`,
16283
+ ` Provider: ${ctx.currentProvider}`,
16284
+ ` Model: ${ctx.currentModel}`,
16285
+ ` Session: ${session?.id.slice(0, 8) ?? "none"} (${session?.messages.length ?? 0} messages)`,
16286
+ "",
16287
+ "**Description:**",
16288
+ "(Describe the bug)",
16289
+ "",
16290
+ "**Steps to reproduce:**",
16291
+ "1. ...",
16292
+ "",
16293
+ "**Expected behavior:**",
16294
+ "...",
16295
+ "",
16296
+ "**Actual behavior:**",
16297
+ "...",
16298
+ "",
16299
+ "Report at: https://github.com/jinzhengdong/ai-cli/issues"
16300
+ ];
16301
+ ctx.send({ type: "info", message: lines.join("\n") });
16058
16302
  }
16059
16303
 
16060
16304
  // src/web/commands/agent-commands.ts
16061
16305
  async function handleAuto(args, ctx) {
16062
- {
16063
- const sub = args[0]?.toLowerCase() ?? "status";
16064
- if (sub === "on") {
16065
- ctx.toolExecutor.sessionAutoMode = true;
16066
- 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." });
16067
- } else if (sub === "off") {
16068
- ctx.toolExecutor.sessionAutoMode = false;
16069
- ctx.send({ type: "info", message: "Auto Mode disabled." });
16070
- } else if (sub === "status") {
16071
- ctx.send({ type: "info", message: `Auto Mode: ${ctx.toolExecutor.sessionAutoMode ? "on" : "off"}
16306
+ const sub = args[0]?.toLowerCase() ?? "status";
16307
+ if (sub === "on") {
16308
+ ctx.toolExecutor.sessionAutoMode = true;
16309
+ 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." });
16310
+ } else if (sub === "off") {
16311
+ ctx.toolExecutor.sessionAutoMode = false;
16312
+ ctx.send({ type: "info", message: "Auto Mode disabled." });
16313
+ } else if (sub === "status") {
16314
+ ctx.send({ type: "info", message: `Auto Mode: ${ctx.toolExecutor.sessionAutoMode ? "on" : "off"}
16072
16315
  Scope: current session only
16073
16316
  Denied: /permissions recently-denied` });
16074
- } else {
16075
- ctx.send({ type: "error", message: "Usage: /auto [on|off|status]" });
16076
- }
16077
- ctx.sendStatus();
16317
+ } else {
16318
+ ctx.send({ type: "error", message: "Usage: /auto [on|off|status]" });
16078
16319
  }
16320
+ ctx.sendStatus();
16079
16321
  }
16080
16322
  async function handlePermissions(args, ctx) {
16081
- {
16082
- const sub = args[0]?.toLowerCase() ?? "recently-denied";
16083
- if (sub === "recently-denied") {
16084
- const rows = getRecentlyDeniedAutoActions();
16085
- if (rows.length === 0) {
16086
- ctx.send({ type: "info", message: "No recently denied Auto Mode actions." });
16087
- } else {
16088
- const lines = [`Recently denied Auto Mode actions (${rows.length})`, ""];
16089
- for (const r of rows.slice(0, 20)) {
16090
- lines.push(`- ${r.timestamp} ${r.tool} ${r.ruleId}`);
16091
- lines.push(` ${r.reason}`);
16092
- lines.push(` ${r.argsPreview}`);
16093
- }
16094
- ctx.send({ type: "info", message: lines.join("\n") });
16095
- }
16096
- } else if (sub === "clear-denied") {
16097
- clearRecentlyDeniedAutoActions();
16098
- ctx.send({ type: "info", message: "Cleared recently denied Auto Mode actions." });
16323
+ const sub = args[0]?.toLowerCase() ?? "recently-denied";
16324
+ if (sub === "recently-denied") {
16325
+ const rows = getRecentlyDeniedAutoActions();
16326
+ if (rows.length === 0) {
16327
+ ctx.send({ type: "info", message: "No recently denied Auto Mode actions." });
16099
16328
  } else {
16100
- ctx.send({ type: "error", message: "Usage: /permissions recently-denied|clear-denied" });
16329
+ const lines = [`Recently denied Auto Mode actions (${rows.length})`, ""];
16330
+ for (const r of rows.slice(0, 20)) {
16331
+ lines.push(`- ${r.timestamp} ${r.tool} ${r.ruleId}`);
16332
+ lines.push(` ${r.reason}`);
16333
+ lines.push(` ${r.argsPreview}`);
16334
+ }
16335
+ ctx.send({ type: "info", message: lines.join("\n") });
16101
16336
  }
16337
+ } else if (sub === "clear-denied") {
16338
+ clearRecentlyDeniedAutoActions();
16339
+ ctx.send({ type: "info", message: "Cleared recently denied Auto Mode actions." });
16340
+ } else {
16341
+ ctx.send({ type: "error", message: "Usage: /permissions recently-denied|clear-denied" });
16102
16342
  }
16103
16343
  }
16104
16344
  async function handleYolo(args, ctx) {
16105
- {
16106
- const sub = args[0]?.toLowerCase();
16107
- if (sub === "off") {
16108
- ctx.toolExecutor.sessionAutoApprove = false;
16109
- ctx.send({ type: "info", message: "\u{1F512} Auto-approve disabled \u2014 confirmations restored for this session." });
16110
- } else {
16111
- ctx.toolExecutor.sessionAutoApprove = true;
16112
- 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." });
16113
- }
16345
+ const sub = args[0]?.toLowerCase();
16346
+ if (sub === "off") {
16347
+ ctx.toolExecutor.sessionAutoApprove = false;
16348
+ ctx.send({ type: "info", message: "\u{1F512} Auto-approve disabled \u2014 confirmations restored for this session." });
16349
+ } else {
16350
+ ctx.toolExecutor.sessionAutoApprove = true;
16351
+ 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." });
16114
16352
  }
16115
16353
  }
16116
16354
  async function handleAgent(args, ctx) {
16117
- {
16118
- const sub = args[0]?.toLowerCase() ?? "summary";
16119
- if (sub === "list") {
16120
- const agents = listAgentConfigs(ctx.config.getConfigDir());
16121
- const lines = [`Agents (${agents.length})`, ""];
16122
- for (const agent of agents) {
16123
- const active = agent.name === getPreferredAgentName() ? " *" : " ";
16124
- const model = [agent.provider, agent.model].filter(Boolean).join("/") || "inherit";
16125
- lines.push(`${active} ${agent.name} [${agent.source ?? "builtin"}] \u2014 ${agent.description ?? ""}`);
16126
- lines.push(` model: ${model} \xB7 permission: ${agent.permissionProfile ?? "workspace-write"} \xB7 maxRounds: ${agent.maxToolRounds ?? "inherit"}`);
16127
- }
16128
- lines.push("", "Config dirs: ~/.aicli/agents/ and .aicli/agents/");
16129
- ctx.send({ type: "info", message: lines.join("\n") });
16355
+ const sub = args[0]?.toLowerCase() ?? "summary";
16356
+ if (sub === "list") {
16357
+ const agents = listAgentConfigs(ctx.config.getConfigDir());
16358
+ const lines = [`Agents (${agents.length})`, ""];
16359
+ for (const agent of agents) {
16360
+ const active = agent.name === getPreferredAgentName() ? " *" : " ";
16361
+ const model = [agent.provider, agent.model].filter(Boolean).join("/") || "inherit";
16362
+ lines.push(`${active} ${agent.name} [${agent.source ?? "builtin"}] \u2014 ${agent.description ?? ""}`);
16363
+ lines.push(` model: ${model} \xB7 permission: ${agent.permissionProfile ?? "workspace-write"} \xB7 maxRounds: ${agent.maxToolRounds ?? "inherit"}`);
16364
+ }
16365
+ lines.push("", "Config dirs: ~/.aicli/agents/ and .aicli/agents/");
16366
+ ctx.send({ type: "info", message: lines.join("\n") });
16367
+ return;
16368
+ }
16369
+ if (sub === "switch") {
16370
+ const name = args[1];
16371
+ if (!name) {
16372
+ ctx.send({ type: "error", message: "Usage: /agent switch <name>" });
16373
+ return;
16130
16374
  }
16131
- if (sub === "switch") {
16132
- const name = args[1];
16133
- if (!name) {
16134
- ctx.send({ type: "error", message: "Usage: /agent switch <name>" });
16135
- }
16136
- const agent = resolveAgentConfig(name, ctx.config.getConfigDir());
16137
- if (!agent) {
16138
- const available = listAgentConfigs(ctx.config.getConfigDir()).map((a) => a.name).join(", ");
16139
- ctx.send({ type: "error", message: `Unknown agent: ${name}
16375
+ const agent = resolveAgentConfig(name, ctx.config.getConfigDir());
16376
+ if (!agent) {
16377
+ const available = listAgentConfigs(ctx.config.getConfigDir()).map((a) => a.name).join(", ");
16378
+ ctx.send({ type: "error", message: `Unknown agent: ${name}
16140
16379
  Available: ${available}` });
16141
- }
16142
- setPreferredAgentName(agent.name);
16143
- ctx.send({ type: "info", message: `Preferred spawn_agent role: ${agent.name}` });
16144
- ctx.sendStatus();
16380
+ return;
16145
16381
  }
16146
- if (sub === "stop") {
16147
- const target = args[1] ?? "all";
16148
- const count = requestAgentStop(target);
16149
- ctx.send({ type: "info", message: count > 0 ? `Stop requested for ${count} running agent(s).` : "No matching running agents." });
16150
- ctx.sendStatus();
16382
+ setPreferredAgentName(agent.name);
16383
+ ctx.send({ type: "info", message: `Preferred spawn_agent role: ${agent.name}` });
16384
+ ctx.sendStatus();
16385
+ return;
16386
+ }
16387
+ if (sub === "stop") {
16388
+ const target = args[1] ?? "all";
16389
+ const count = requestAgentStop(target);
16390
+ ctx.send({ type: "info", message: count > 0 ? `Stop requested for ${count} running agent(s).` : "No matching running agents." });
16391
+ ctx.sendStatus();
16392
+ return;
16393
+ }
16394
+ if (sub === "summary") {
16395
+ const runs2 = listAgentRuns();
16396
+ if (runs2.length === 0) {
16397
+ ctx.send({ type: "info", message: "No sub-agent runs yet." });
16398
+ return;
16151
16399
  }
16152
- if (sub === "summary") {
16153
- const runs2 = listAgentRuns();
16154
- if (runs2.length === 0) {
16155
- ctx.send({ type: "info", message: "No sub-agent runs yet." });
16156
- }
16157
- const lines = [`Recent agent runs (${runs2.length})`, ""];
16158
- for (const run of runs2.slice(0, 20)) {
16159
- lines.push(`${run.id} ${run.status} ${run.agentName} ${run.startedAt}`);
16160
- lines.push(` task: ${run.task.slice(0, 120)}${run.task.length > 120 ? "..." : ""}`);
16161
- if (run.toolNames.length > 0) lines.push(` tools: ${run.toolNames.join(", ")}`);
16162
- if (run.summary) lines.push(` summary: ${run.summary.slice(0, 180)}${run.summary.length > 180 ? "..." : ""}`);
16163
- if (run.error) lines.push(` error: ${run.error}`);
16164
- }
16165
- ctx.send({ type: "info", message: lines.join("\n") });
16400
+ const lines = [`Recent agent runs (${runs2.length})`, ""];
16401
+ for (const run of runs2.slice(0, 20)) {
16402
+ lines.push(`${run.id} ${run.status} ${run.agentName} ${run.startedAt}`);
16403
+ lines.push(` task: ${run.task.slice(0, 120)}${run.task.length > 120 ? "..." : ""}`);
16404
+ if (run.toolNames.length > 0) lines.push(` tools: ${run.toolNames.join(", ")}`);
16405
+ if (run.summary) lines.push(` summary: ${run.summary.slice(0, 180)}${run.summary.length > 180 ? "..." : ""}`);
16406
+ if (run.error) lines.push(` error: ${run.error}`);
16166
16407
  }
16167
- ctx.send({ type: "error", message: "Usage: /agent list|switch <name>|stop <id|all>|summary" });
16408
+ ctx.send({ type: "info", message: lines.join("\n") });
16409
+ return;
16168
16410
  }
16411
+ ctx.send({ type: "error", message: "Usage: /agent list|switch <name>|stop <id|all>|summary" });
16169
16412
  }
16170
16413
 
16171
16414
  // src/web/commands/misc-commands.ts
16172
- async function handleHelp(args, ctx) {
16415
+ async function handleHelp(_args, ctx) {
16173
16416
  ctx.send({
16174
16417
  type: "info",
16175
16418
  message: [
@@ -16227,90 +16470,87 @@ async function handleHelp(args, ctx) {
16227
16470
  ].join("\n")
16228
16471
  });
16229
16472
  }
16230
- async function handleAbout(args, ctx) {
16231
- {
16232
- const providerList = ctx.providers.listAll();
16233
- const toolCount = ctx.toolRegistry.getDefinitions().length;
16234
- const mcpTools = ctx.mcpManager?.getTotalToolCount() ?? 0;
16235
- const lines = [
16236
- `\u{1F916} **ai-cli** v${VERSION}`,
16237
- "",
16238
- `${DESCRIPTION}`,
16239
- `Author: ${AUTHOR} <${AUTHOR_EMAIL}>`,
16240
- "",
16241
- `**Providers:** ${providerList.length} (${providerList.map((p) => p.id).join(", ")})`,
16242
- `**Tools:** ${toolCount + mcpTools} (${toolCount} built-in${mcpTools > 0 ? ` + ${mcpTools} MCP` : ""})`,
16243
- `**REPL Commands:** 37`,
16244
- "",
16245
- `GitHub: https://github.com/jinzhengdong/ai-cli`
16246
- ];
16247
- ctx.send({ type: "info", message: lines.join("\n") });
16248
- }
16473
+ async function handleAbout(_args, ctx) {
16474
+ const providerList = ctx.providers.listAll();
16475
+ const toolCount = ctx.toolRegistry.getDefinitions().length;
16476
+ const mcpTools = ctx.mcpManager?.getTotalToolCount() ?? 0;
16477
+ const lines = [
16478
+ `\u{1F916} **ai-cli** v${VERSION}`,
16479
+ "",
16480
+ `${DESCRIPTION}`,
16481
+ `Author: ${AUTHOR} <${AUTHOR_EMAIL}>`,
16482
+ "",
16483
+ `**Providers:** ${providerList.length} (${providerList.map((p) => p.id).join(", ")})`,
16484
+ `**Tools:** ${toolCount + mcpTools} (${toolCount} built-in${mcpTools > 0 ? ` + ${mcpTools} MCP` : ""})`,
16485
+ `**REPL Commands:** 37`,
16486
+ "",
16487
+ `GitHub: https://github.com/jinzhengdong/ai-cli`
16488
+ ];
16489
+ ctx.send({ type: "info", message: lines.join("\n") });
16249
16490
  }
16250
16491
  async function handleSearch(args, ctx) {
16251
- {
16252
- const query = args.join(" ").trim();
16253
- if (!query) {
16254
- ctx.send({ type: "error", message: "Usage: /search <keyword>" });
16255
- }
16256
- const results = ctx.sessions.searchMessages(query);
16257
- if (results.length === 0) {
16258
- ctx.send({ type: "info", message: `No sessions found containing "${query}".` });
16259
- }
16260
- const lines = [`\u{1F50D} Found ${results.length} session(s) containing "${query}"`, ""];
16261
- for (const r of results) {
16262
- const { sessionMeta, matches } = r;
16263
- const dateStr = sessionMeta.updated instanceof Date ? sessionMeta.updated.toLocaleDateString() : String(sessionMeta.updated);
16264
- lines.push(`**${sessionMeta.id.slice(0, 8)}** [${dateStr}] ${sessionMeta.provider} / ${sessionMeta.model}`);
16265
- if (sessionMeta.title) lines.push(` ${sessionMeta.title}`);
16266
- for (const m of matches) {
16267
- const icon = m.role === "user" ? "\u{1F464}" : "\u{1F916}";
16268
- lines.push(` ${icon} ${m.snippet}`);
16269
- }
16270
- lines.push("");
16492
+ const query = args.join(" ").trim();
16493
+ if (!query) {
16494
+ ctx.send({ type: "error", message: "Usage: /search <keyword>" });
16495
+ return;
16496
+ }
16497
+ const results = ctx.sessions.searchMessages(query);
16498
+ if (results.length === 0) {
16499
+ ctx.send({ type: "info", message: `No sessions found containing "${query}".` });
16500
+ return;
16501
+ }
16502
+ const lines = [`\u{1F50D} Found ${results.length} session(s) containing "${query}"`, ""];
16503
+ for (const r of results) {
16504
+ const { sessionMeta, matches } = r;
16505
+ const dateStr = sessionMeta.updated instanceof Date ? sessionMeta.updated.toLocaleDateString() : String(sessionMeta.updated);
16506
+ lines.push(`**${sessionMeta.id.slice(0, 8)}** [${dateStr}] ${sessionMeta.provider} / ${sessionMeta.model}`);
16507
+ if (sessionMeta.title) lines.push(` ${sessionMeta.title}`);
16508
+ for (const m of matches) {
16509
+ const icon = m.role === "user" ? "\u{1F464}" : "\u{1F916}";
16510
+ lines.push(` ${icon} ${m.snippet}`);
16271
16511
  }
16272
- lines.push("Use /session load <id> to resume a session.");
16273
- ctx.send({ type: "info", message: lines.join("\n") });
16512
+ lines.push("");
16274
16513
  }
16514
+ lines.push("Use /session load <id> to resume a session.");
16515
+ ctx.send({ type: "info", message: lines.join("\n") });
16275
16516
  }
16276
16517
  async function handleUndo(args, ctx) {
16277
- {
16278
- const sub = args.join(" ").trim();
16279
- if (sub === "list") {
16280
- const history = undoStack.getHistory();
16281
- if (history.length === 0) {
16282
- ctx.send({ type: "info", message: "Undo stack is empty." });
16283
- }
16284
- const lines = [`\u{1F4CB} Undo Stack (${history.length} entries, newest last):`, ""];
16285
- history.forEach((entry, i) => {
16286
- const timeStr = entry.timestamp.toLocaleTimeString();
16287
- const typeTag = entry.isDirectory ? "[dir]" : entry.previousContent === null ? "[new]" : "[mod]";
16288
- lines.push(` ${String(i + 1).padStart(3)} ${typeTag} ${entry.description} ${timeStr}`);
16289
- });
16290
- ctx.send({ type: "info", message: lines.join("\n") });
16291
- }
16292
- const n = sub ? parseInt(sub, 10) : 1;
16293
- if (isNaN(n) || n < 1) {
16294
- ctx.send({ type: "info", message: "Usage: /undo [list | <n>]" });
16295
- }
16296
- if (undoStack.depth === 0) {
16297
- ctx.send({ type: "info", message: "Nothing to undo." });
16298
- }
16299
- const count = Math.min(n, undoStack.depth);
16300
- const undoResults = [];
16301
- for (let i = 0; i < count; i++) {
16302
- const top = undoStack.peek();
16303
- if (!top) {
16304
- }
16305
- const undoResult = undoStack.undo();
16306
- if (undoResult) undoResults.push(undoResult.result);
16518
+ const sub = args.join(" ").trim();
16519
+ if (sub === "list") {
16520
+ const history = undoStack.getHistory();
16521
+ if (history.length === 0) {
16522
+ ctx.send({ type: "info", message: "Undo stack is empty." });
16523
+ return;
16307
16524
  }
16308
- if (undoResults.length === 0) {
16309
- ctx.send({ type: "info", message: "Nothing to undo." });
16310
- } else {
16311
- ctx.send({ type: "info", message: `\u2713 ${undoResults.length} operation(s) undone:
16525
+ const lines = [`\u{1F4CB} Undo Stack (${history.length} entries, newest last):`, ""];
16526
+ history.forEach((entry, i) => {
16527
+ const timeStr = entry.timestamp.toLocaleTimeString();
16528
+ const typeTag = entry.isDirectory ? "[dir]" : entry.previousContent === null ? "[new]" : "[mod]";
16529
+ lines.push(` ${String(i + 1).padStart(3)} ${typeTag} ${entry.description} ${timeStr}`);
16530
+ });
16531
+ ctx.send({ type: "info", message: lines.join("\n") });
16532
+ return;
16533
+ }
16534
+ const n = sub ? parseInt(sub, 10) : 1;
16535
+ if (isNaN(n) || n < 1) {
16536
+ ctx.send({ type: "info", message: "Usage: /undo [list | <n>]" });
16537
+ return;
16538
+ }
16539
+ if (undoStack.depth === 0) {
16540
+ ctx.send({ type: "info", message: "Nothing to undo." });
16541
+ return;
16542
+ }
16543
+ const count = Math.min(n, undoStack.depth);
16544
+ const undoResults = [];
16545
+ for (let i = 0; i < count; i++) {
16546
+ const undoResult = undoStack.undo();
16547
+ if (undoResult) undoResults.push(undoResult.result);
16548
+ }
16549
+ if (undoResults.length === 0) {
16550
+ ctx.send({ type: "info", message: "Nothing to undo." });
16551
+ } else {
16552
+ ctx.send({ type: "info", message: `\u2713 ${undoResults.length} operation(s) undone:
16312
16553
  ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
16313
- }
16314
16554
  }
16315
16555
  }
16316
16556
 
@@ -16359,20 +16599,6 @@ var webCommandHandlers = {
16359
16599
  search: handleSearch,
16360
16600
  undo: handleUndo
16361
16601
  };
16362
- function lastAssistantText(messages) {
16363
- for (let i = messages.length - 1; i >= 0; i--) {
16364
- const m = messages[i];
16365
- if (!m || m.role !== "assistant") continue;
16366
- const c = m.content;
16367
- if (typeof c === "string") return c;
16368
- if (Array.isArray(c)) {
16369
- const text = c.filter((p) => p && p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
16370
- if (text) return text;
16371
- }
16372
- return void 0;
16373
- }
16374
- return void 0;
16375
- }
16376
16602
  var SessionHandler = class {
16377
16603
  ws;
16378
16604
  config;
@@ -16415,6 +16641,8 @@ var SessionHandler = class {
16415
16641
  addedDirs = /* @__PURE__ */ new Set();
16416
16642
  /** Track MCP tool names used across this session (for MCP budget prioritization) */
16417
16643
  usedMcpToolNames = /* @__PURE__ */ new Set();
16644
+ /** P2-PERF-05: bash session key for cleanup on disconnect */
16645
+ _bashSessionKey = "";
16418
16646
  /**
16419
16647
  * 未保存会话的内存缓存(per-handler)。
16420
16648
  * 当客户端通过 `session new` 创建新会话时,会话仅存在于 SessionManager._current 中,
@@ -16433,7 +16661,9 @@ var SessionHandler = class {
16433
16661
  this.mcpManager = shared.mcpManager;
16434
16662
  this.skillManager = shared.skillManager;
16435
16663
  this.toolExecutor = new ToolExecutorWeb(shared.toolRegistry, ws);
16436
- this.toolExecutor.setSessionKey(`web-${Math.random().toString(36).slice(2)}-${Date.now()}`);
16664
+ const bashSessionKey = `web-${Math.random().toString(36).slice(2)}-${Date.now()}`;
16665
+ this.toolExecutor.setSessionKey(bashSessionKey);
16666
+ this._bashSessionKey = bashSessionKey;
16437
16667
  this.currentProvider = this.config.get("defaultProvider");
16438
16668
  const allDefaultModels = this.config.get("defaultModels");
16439
16669
  try {
@@ -16551,10 +16781,10 @@ var SessionHandler = class {
16551
16781
  return;
16552
16782
  }
16553
16783
  case "ask_user_response": {
16554
- const resolve15 = this.pendingAskUser.get(msg.requestId);
16555
- if (resolve15) {
16784
+ const resolve14 = this.pendingAskUser.get(msg.requestId);
16785
+ if (resolve14) {
16556
16786
  this.pendingAskUser.delete(msg.requestId);
16557
- resolve15(msg.answer);
16787
+ resolve14(msg.answer);
16558
16788
  }
16559
16789
  return;
16560
16790
  }
@@ -16567,10 +16797,10 @@ var SessionHandler = class {
16567
16797
  case "memory_manage":
16568
16798
  return this.handleMemoryManage(msg.action, msg.id, msg.expiresAt);
16569
16799
  case "auto_pause_response": {
16570
- const resolve15 = this.pendingAutoPause.get(msg.requestId);
16571
- if (resolve15) {
16800
+ const resolve14 = this.pendingAutoPause.get(msg.requestId);
16801
+ if (resolve14) {
16572
16802
  this.pendingAutoPause.delete(msg.requestId);
16573
- resolve15({ action: msg.action, message: msg.message });
16803
+ resolve14({ action: msg.action, message: msg.message });
16574
16804
  }
16575
16805
  return;
16576
16806
  }
@@ -16585,10 +16815,10 @@ var SessionHandler = class {
16585
16815
  this.hubOrchestrator?.abort();
16586
16816
  return;
16587
16817
  case "hub_steer": {
16588
- const resolve15 = this.pendingHubReview.get(msg.requestId);
16589
- if (resolve15) {
16818
+ const resolve14 = this.pendingHubReview.get(msg.requestId);
16819
+ if (resolve14) {
16590
16820
  this.pendingHubReview.delete(msg.requestId);
16591
- resolve15({ action: msg.action, message: msg.message });
16821
+ resolve14({ action: msg.action, message: msg.message });
16592
16822
  }
16593
16823
  return;
16594
16824
  }
@@ -16601,11 +16831,12 @@ var SessionHandler = class {
16601
16831
  runLifecycleHooks(mergePluginHooks(this.config.get("hooks") ?? void 0, this.config.getConfigDir()), "Stop", { sessionId: this.sessions.current?.id }, { configDir: this.config.getConfigDir() });
16602
16832
  this.toolExecutor.cancelAll();
16603
16833
  if (this.abortController) this.abortController.abort();
16604
- for (const resolve15 of this.pendingAskUser.values()) resolve15(null);
16834
+ for (const resolve14 of this.pendingAskUser.values()) resolve14(null);
16605
16835
  this.pendingAskUser.clear();
16606
- for (const resolve15 of this.pendingAutoPause.values()) resolve15({ action: "stop" });
16836
+ for (const resolve14 of this.pendingAutoPause.values()) resolve14({ action: "stop" });
16607
16837
  this.pendingAutoPause.clear();
16608
16838
  this.saveIfNeeded();
16839
+ if (this._bashSessionKey) clearBashSession(this._bashSessionKey);
16609
16840
  }
16610
16841
  /** 根据当前模型 context window 更新工具输出截断上限 */
16611
16842
  updateContextWindow() {
@@ -16737,9 +16968,9 @@ var SessionHandler = class {
16737
16968
  this.hubOrchestrator = orchestrator;
16738
16969
  orchestrator.onEvent = (event) => this.send({ type: "hub_event", event });
16739
16970
  if (config.humanSteer) {
16740
- orchestrator.onRoundReview = ({ round, maxRounds }) => new Promise((resolve15) => {
16971
+ orchestrator.onRoundReview = ({ round, maxRounds }) => new Promise((resolve14) => {
16741
16972
  const requestId = `hubrev_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
16742
- this.pendingHubReview.set(requestId, resolve15);
16973
+ this.pendingHubReview.set(requestId, resolve14);
16743
16974
  this.send({ type: "hub_review", requestId, round, maxRounds });
16744
16975
  });
16745
16976
  }
@@ -17072,7 +17303,7 @@ Try: /compact to reduce context, /clear to reset, or switch to a larger-context
17072
17303
  this.send({ type: "response_done", content, usage });
17073
17304
  if (pendingTeeSave && isCleanDocumentBody(content)) {
17074
17305
  try {
17075
- mkdirSync17(dirname11(pendingTeeSave), { recursive: true });
17306
+ mkdirSync16(dirname10(pendingTeeSave), { recursive: true });
17076
17307
  const bodyToSave = stripOuterCodeFence(content);
17077
17308
  atomicWriteFileSync(pendingTeeSave, bodyToSave);
17078
17309
  undoStack.push(pendingTeeSave, `save_last_response (deferred): ${pendingTeeSave}`);
@@ -17097,8 +17328,8 @@ Try: /compact to reduce context, /clear to reset, or switch to a larger-context
17097
17328
  onMcpToolUsed: (name) => this.usedMcpToolNames.add(name),
17098
17329
  requestAutoPause: async ({ effectiveRound, maxToolRounds: totalRounds, toolSummary }) => {
17099
17330
  const requestId = `pause_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
17100
- const pauseResp = await new Promise((resolve15) => {
17101
- this.pendingAutoPause.set(requestId, resolve15);
17331
+ const pauseResp = await new Promise((resolve14) => {
17332
+ this.pendingAutoPause.set(requestId, resolve14);
17102
17333
  this.send({
17103
17334
  type: "auto_pause_request",
17104
17335
  requestId,
@@ -17248,8 +17479,8 @@ ${summaryContent}`,
17248
17479
  let isError = false;
17249
17480
  let summary;
17250
17481
  try {
17251
- mkdirSync17(dirname11(saveToFile), { recursive: true });
17252
- fileStream = createWriteStream4(saveToFile);
17482
+ mkdirSync16(dirname10(saveToFile), { recursive: true });
17483
+ fileStream = createWriteStream3(saveToFile);
17253
17484
  const teeSystemPrompt = stripToolCallReminder(systemPrompt ?? "") + CONTENT_ONLY_STREAM_REMINDER;
17254
17485
  const teeExtraMessages = extraMessages.length > 0 ? [...extraMessages, { role: "user", content: TEE_FINAL_USER_NUDGE }] : [{ role: "user", content: TEE_FINAL_USER_NUDGE }];
17255
17486
  const chatRequest = {
@@ -17277,8 +17508,8 @@ ${summaryContent}`,
17277
17508
  }
17278
17509
  if (chunk.done) break;
17279
17510
  }
17280
- await new Promise((resolve15, reject) => {
17281
- fileStream.end((err) => err ? reject(err) : resolve15());
17511
+ await new Promise((resolve14, reject) => {
17512
+ fileStream.end((err) => err ? reject(err) : resolve14());
17282
17513
  });
17283
17514
  const verdict = evaluateTeeContent(fullContent, saveToFile, priorContent);
17284
17515
  if (verdict.kind === "reject") {
@@ -17304,7 +17535,7 @@ ${summaryContent}`,
17304
17535
  } catch (err) {
17305
17536
  if (fileStream) {
17306
17537
  try {
17307
- await new Promise((resolve15) => fileStream.end(() => resolve15()));
17538
+ await new Promise((resolve14) => fileStream.end(() => resolve14()));
17308
17539
  } catch {
17309
17540
  }
17310
17541
  }
@@ -17541,7 +17772,7 @@ ${entry.content}`;
17541
17772
  const configDir = this.config.getConfigDir();
17542
17773
  try {
17543
17774
  atomicWriteFileSync(memoryStorePath(configDir), "");
17544
- atomicWriteFileSync(join27(configDir, MEMORY_FILE_NAME), "");
17775
+ atomicWriteFileSync(join26(configDir, MEMORY_FILE_NAME), "");
17545
17776
  this.send({ type: "info", message: "\u{1F5D1}\uFE0F Persistent memory cleared." });
17546
17777
  this.sendMemoryEntries();
17547
17778
  } catch (err) {
@@ -17607,7 +17838,7 @@ ${entry.content}`;
17607
17838
  return;
17608
17839
  }
17609
17840
  try {
17610
- const { searchChatMemory: searchChatMemory2, loadChatIndex: loadChatIndex2 } = await import("./chat-index-PS274XM7.js");
17841
+ const { searchChatMemory: searchChatMemory2, loadChatIndex: loadChatIndex2 } = await import("./chat-index-WSI7ULRQ.js");
17611
17842
  const loaded = loadChatIndex2();
17612
17843
  if (!loaded || loaded.idx.chunks.length === 0) {
17613
17844
  this.send({ type: "memory_hits", query: q, hits: [], indexMissing: true });
@@ -17643,7 +17874,7 @@ ${entry.content}`;
17643
17874
  }
17644
17875
  async handleMemoryStatus() {
17645
17876
  try {
17646
- const { getChatIndexStatus } = await import("./chat-index-PS274XM7.js");
17877
+ const { getChatIndexStatus } = await import("./chat-index-WSI7ULRQ.js");
17647
17878
  const s = getChatIndexStatus();
17648
17879
  this.send({
17649
17880
  type: "memory_status",
@@ -17668,7 +17899,7 @@ ${entry.content}`;
17668
17899
  type: "info",
17669
17900
  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"
17670
17901
  });
17671
- const { buildChatIndex } = await import("./chat-index-PS274XM7.js");
17902
+ const { buildChatIndex } = await import("./chat-index-WSI7ULRQ.js");
17672
17903
  const stats = await buildChatIndex({
17673
17904
  full,
17674
17905
  onProgress: (p) => {
@@ -17823,8 +18054,8 @@ async function setupProxy(configProxy) {
17823
18054
  }
17824
18055
 
17825
18056
  // src/web/auth.ts
17826
- import { existsSync as existsSync33, readFileSync as readFileSync26, writeFileSync as writeFileSync7, mkdirSync as mkdirSync18, readdirSync as readdirSync17, copyFileSync } from "fs";
17827
- import { join as join28 } from "path";
18057
+ import { existsSync as existsSync32, readFileSync as readFileSync25, writeFileSync as writeFileSync6, mkdirSync as mkdirSync17, readdirSync as readdirSync16, copyFileSync } from "fs";
18058
+ import { join as join27 } from "path";
17828
18059
  import { createHmac, randomBytes, timingSafeEqual, pbkdf2Sync } from "crypto";
17829
18060
  var USERS_FILE = "users.json";
17830
18061
  var TOKEN_EXPIRY_HOURS = 24;
@@ -17839,7 +18070,7 @@ var AuthManager = class {
17839
18070
  db;
17840
18071
  constructor(baseDir) {
17841
18072
  this.baseDir = baseDir;
17842
- this.usersFile = join28(baseDir, USERS_FILE);
18073
+ this.usersFile = join27(baseDir, USERS_FILE);
17843
18074
  this.db = this.loadOrCreate();
17844
18075
  }
17845
18076
  // ── Public API ─────────────────────────────────────────────────
@@ -17868,9 +18099,9 @@ var AuthManager = class {
17868
18099
  }
17869
18100
  const salt = randomBytes(16).toString("hex");
17870
18101
  const passwordHash = this.hashPassword(password, salt);
17871
- const dataDir = join28(USERS_DIR, username);
17872
- const fullDataDir = join28(this.baseDir, dataDir);
17873
- mkdirSync18(join28(fullDataDir, "history"), { recursive: true });
18102
+ const dataDir = join27(USERS_DIR, username);
18103
+ const fullDataDir = join27(this.baseDir, dataDir);
18104
+ mkdirSync17(join27(fullDataDir, "history"), { recursive: true });
17874
18105
  const user = {
17875
18106
  username,
17876
18107
  passwordHash,
@@ -17985,7 +18216,7 @@ var AuthManager = class {
17985
18216
  getUserDataDir(username) {
17986
18217
  const user = this.db.users.find((u) => u.username === username);
17987
18218
  if (!user) throw new Error(`User not found: ${username}`);
17988
- return join28(this.baseDir, user.dataDir);
18219
+ return join27(this.baseDir, user.dataDir);
17989
18220
  }
17990
18221
  /** List all usernames */
17991
18222
  listUsers() {
@@ -18021,30 +18252,30 @@ var AuthManager = class {
18021
18252
  const err = this.register(username, password);
18022
18253
  if (err) return err;
18023
18254
  const userDir = this.getUserDataDir(username);
18024
- const globalConfig = join28(this.baseDir, "config.json");
18025
- if (existsSync33(globalConfig)) {
18255
+ const globalConfig = join27(this.baseDir, "config.json");
18256
+ if (existsSync32(globalConfig)) {
18026
18257
  try {
18027
- const content = readFileSync26(globalConfig, "utf-8");
18028
- writeFileSync7(join28(userDir, "config.json"), content, "utf-8");
18258
+ const content = readFileSync25(globalConfig, "utf-8");
18259
+ writeFileSync6(join27(userDir, "config.json"), content, "utf-8");
18029
18260
  } catch {
18030
18261
  }
18031
18262
  }
18032
- const globalMemory = join28(this.baseDir, "memory.md");
18033
- if (existsSync33(globalMemory)) {
18263
+ const globalMemory = join27(this.baseDir, "memory.md");
18264
+ if (existsSync32(globalMemory)) {
18034
18265
  try {
18035
- const content = readFileSync26(globalMemory, "utf-8");
18036
- writeFileSync7(join28(userDir, "memory.md"), content, "utf-8");
18266
+ const content = readFileSync25(globalMemory, "utf-8");
18267
+ writeFileSync6(join27(userDir, "memory.md"), content, "utf-8");
18037
18268
  } catch {
18038
18269
  }
18039
18270
  }
18040
- const globalHistory = join28(this.baseDir, "history");
18041
- if (existsSync33(globalHistory)) {
18271
+ const globalHistory = join27(this.baseDir, "history");
18272
+ if (existsSync32(globalHistory)) {
18042
18273
  try {
18043
- const files = readdirSync17(globalHistory).filter((f) => f.endsWith(".json"));
18044
- const userHistory = join28(userDir, "history");
18274
+ const files = readdirSync16(globalHistory).filter((f) => f.endsWith(".json"));
18275
+ const userHistory = join27(userDir, "history");
18045
18276
  for (const f of files) {
18046
18277
  try {
18047
- copyFileSync(join28(globalHistory, f), join28(userHistory, f));
18278
+ copyFileSync(join27(globalHistory, f), join27(userHistory, f));
18048
18279
  } catch {
18049
18280
  }
18050
18281
  }
@@ -18055,9 +18286,9 @@ var AuthManager = class {
18055
18286
  }
18056
18287
  // ── Private methods ────────────────────────────────────────────
18057
18288
  loadOrCreate() {
18058
- if (existsSync33(this.usersFile)) {
18289
+ if (existsSync32(this.usersFile)) {
18059
18290
  try {
18060
- return JSON.parse(readFileSync26(this.usersFile, "utf-8"));
18291
+ return JSON.parse(readFileSync25(this.usersFile, "utf-8"));
18061
18292
  } catch {
18062
18293
  }
18063
18294
  }
@@ -18073,7 +18304,7 @@ var AuthManager = class {
18073
18304
  this.saveDB(this.db);
18074
18305
  }
18075
18306
  saveDB(db) {
18076
- mkdirSync18(this.baseDir, { recursive: true });
18307
+ mkdirSync17(this.baseDir, { recursive: true });
18077
18308
  atomicWriteFileSync(this.usersFile, JSON.stringify(db, null, 2));
18078
18309
  }
18079
18310
  /** Legacy hash — kept only for migrating old users (v0.2.x) */
@@ -18105,11 +18336,11 @@ function getModuleDir() {
18105
18336
  if (typeof import.meta?.url === "string") {
18106
18337
  const url = new URL(import.meta.url);
18107
18338
  const filePath = url.pathname.replace(/^\/([A-Z]:)/i, "$1");
18108
- return dirname12(filePath);
18339
+ return dirname11(filePath);
18109
18340
  }
18110
18341
  } catch {
18111
18342
  }
18112
- return resolve14(".");
18343
+ return resolve13(".");
18113
18344
  }
18114
18345
  async function startWebServer(options = {}) {
18115
18346
  const port = options.port ?? 3e3;
@@ -18177,8 +18408,8 @@ async function startWebServer(options = {}) {
18177
18408
  }
18178
18409
  }
18179
18410
  let skillManager = null;
18180
- const skillsDir = join29(config.getConfigDir(), SKILLS_DIR_NAME);
18181
- if (existsSync34(skillsDir)) {
18411
+ const skillsDir = join28(config.getConfigDir(), SKILLS_DIR_NAME);
18412
+ if (existsSync33(skillsDir)) {
18182
18413
  skillManager = new SkillManager(skillsDir, config.get("ui").skillSizeWarn);
18183
18414
  skillManager.loadSkills(pluginAssets.skillDirs);
18184
18415
  const count = skillManager.listSkills().length;
@@ -18249,18 +18480,18 @@ async function startWebServer(options = {}) {
18249
18480
  next();
18250
18481
  };
18251
18482
  const moduleDir = getModuleDir();
18252
- let clientDir = join29(moduleDir, "web", "client");
18253
- if (!existsSync34(clientDir)) {
18254
- clientDir = join29(moduleDir, "client");
18483
+ let clientDir = join28(moduleDir, "web", "client");
18484
+ if (!existsSync33(clientDir)) {
18485
+ clientDir = join28(moduleDir, "client");
18255
18486
  }
18256
- if (!existsSync34(clientDir)) {
18257
- clientDir = join29(moduleDir, "..", "..", "src", "web", "client");
18487
+ if (!existsSync33(clientDir)) {
18488
+ clientDir = join28(moduleDir, "..", "..", "src", "web", "client");
18258
18489
  }
18259
- if (!existsSync34(clientDir)) {
18260
- clientDir = join29(process.cwd(), "src", "web", "client");
18490
+ if (!existsSync33(clientDir)) {
18491
+ clientDir = join28(process.cwd(), "src", "web", "client");
18261
18492
  }
18262
18493
  console.log(` Static files: ${clientDir}`);
18263
- app.use("/vendor", express.static(join29(clientDir, "vendor"), { maxAge: "1y", immutable: true }));
18494
+ app.use("/vendor", express.static(join28(clientDir, "vendor"), { maxAge: "1y", immutable: true }));
18264
18495
  app.use(express.static(clientDir));
18265
18496
  app.get("/api/status", (_req, res) => {
18266
18497
  res.json({
@@ -18327,10 +18558,10 @@ async function startWebServer(options = {}) {
18327
18558
  app.get("/api/files", requireAuth, (req, res) => {
18328
18559
  const cwd = process.cwd();
18329
18560
  const prefix = req.query.prefix || "";
18330
- const targetDir = join29(cwd, prefix);
18561
+ const targetDir = join28(cwd, prefix);
18331
18562
  try {
18332
- const canonicalTarget = realpathSync(resolve14(targetDir));
18333
- const canonicalCwd = realpathSync(resolve14(cwd));
18563
+ const canonicalTarget = realpathSync(resolve13(targetDir));
18564
+ const canonicalCwd = realpathSync(resolve13(cwd));
18334
18565
  if (!canonicalTarget.startsWith(canonicalCwd + sep3) && canonicalTarget !== canonicalCwd) {
18335
18566
  res.json({ files: [] });
18336
18567
  return;
@@ -18341,10 +18572,10 @@ async function startWebServer(options = {}) {
18341
18572
  }
18342
18573
  try {
18343
18574
  const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "dist-cjs", "release", "__pycache__", ".next", ".nuxt", "coverage", ".cache"]);
18344
- const entries = readdirSync18(targetDir, { withFileTypes: true });
18575
+ const entries = readdirSync17(targetDir, { withFileTypes: true });
18345
18576
  const files = entries.filter((e) => !SKIP.has(e.name) && !e.name.startsWith(".")).slice(0, 50).map((e) => ({
18346
18577
  name: e.name,
18347
- path: relative4(cwd, join29(targetDir, e.name)).replace(/\\/g, "/"),
18578
+ path: relative4(cwd, join28(targetDir, e.name)).replace(/\\/g, "/"),
18348
18579
  isDir: e.isDirectory()
18349
18580
  }));
18350
18581
  res.json({ files });
@@ -18384,8 +18615,8 @@ async function startWebServer(options = {}) {
18384
18615
  try {
18385
18616
  const authUser = req._authUser;
18386
18617
  const histDir = authUser ? getUserShared(authUser).config.getHistoryDir() : config.getHistoryDir();
18387
- const filePath = join29(histDir, `${id}.json`);
18388
- if (!existsSync34(filePath)) {
18618
+ const filePath = join28(histDir, `${id}.json`);
18619
+ if (!existsSync33(filePath)) {
18389
18620
  res.status(404).json({ error: "Session not found" });
18390
18621
  return;
18391
18622
  }
@@ -18400,7 +18631,7 @@ async function startWebServer(options = {}) {
18400
18631
  res.status(404).json({ error: "Session not found" });
18401
18632
  return;
18402
18633
  }
18403
- const data = JSON.parse(readFileSync27(filePath, "utf-8"));
18634
+ const data = JSON.parse(readFileSync26(filePath, "utf-8"));
18404
18635
  res.json({ session: data });
18405
18636
  } catch (err) {
18406
18637
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
@@ -18413,10 +18644,10 @@ async function startWebServer(options = {}) {
18413
18644
  return;
18414
18645
  }
18415
18646
  const cwd = process.cwd();
18416
- const fullPath = resolve14(join29(cwd, filePath));
18647
+ const fullPath = resolve13(join28(cwd, filePath));
18417
18648
  try {
18418
18649
  const canonicalFull = realpathSync(fullPath);
18419
- const canonicalCwd = realpathSync(resolve14(cwd));
18650
+ const canonicalCwd = realpathSync(resolve13(cwd));
18420
18651
  if (!canonicalFull.startsWith(canonicalCwd + sep3) && canonicalFull !== canonicalCwd) {
18421
18652
  res.json({ error: "Access denied" });
18422
18653
  return;
@@ -18426,12 +18657,12 @@ async function startWebServer(options = {}) {
18426
18657
  return;
18427
18658
  }
18428
18659
  try {
18429
- const stat = statSync16(fullPath);
18660
+ const stat = statSync15(fullPath);
18430
18661
  if (stat.size > 512 * 1024) {
18431
18662
  res.json({ error: `File too large (${(stat.size / 1024).toFixed(0)} KB, max 512 KB)` });
18432
18663
  return;
18433
18664
  }
18434
- const content = readFileSync27(fullPath, "utf-8");
18665
+ const content = readFileSync26(fullPath, "utf-8");
18435
18666
  res.json({ content, size: stat.size });
18436
18667
  } catch {
18437
18668
  res.json({ error: "Cannot read file" });
@@ -18635,7 +18866,7 @@ async function startWebServer(options = {}) {
18635
18866
  });
18636
18867
  const MAX_PORT_ATTEMPTS = 10;
18637
18868
  let actualPort = port;
18638
- const result = await new Promise((resolve15, reject) => {
18869
+ const result = await new Promise((resolve14, reject) => {
18639
18870
  const tryListen = (attempt) => {
18640
18871
  server.once("error", (err) => {
18641
18872
  if (err.code === "EADDRINUSE" && attempt < MAX_PORT_ATTEMPTS) {
@@ -18666,7 +18897,7 @@ async function startWebServer(options = {}) {
18666
18897
  }
18667
18898
  console.log(` Press Ctrl+C to stop
18668
18899
  `);
18669
- resolve15({ port: actualPort, host, url });
18900
+ resolve14({ port: actualPort, host, url });
18670
18901
  });
18671
18902
  };
18672
18903
  tryListen(1);
@@ -18686,17 +18917,17 @@ function resolveProjectMcpPath() {
18686
18917
  const cwd = process.cwd();
18687
18918
  const gitRoot = getGitRoot(cwd);
18688
18919
  const projectRoot = gitRoot ?? cwd;
18689
- const configPath = join29(projectRoot, MCP_PROJECT_CONFIG_NAME);
18690
- return existsSync34(configPath) ? configPath : null;
18920
+ const configPath = join28(projectRoot, MCP_PROJECT_CONFIG_NAME);
18921
+ return existsSync33(configPath) ? configPath : null;
18691
18922
  }
18692
18923
  function loadProjectMcpConfig() {
18693
18924
  const cwd = process.cwd();
18694
18925
  const gitRoot = getGitRoot(cwd);
18695
18926
  const projectRoot = gitRoot ?? cwd;
18696
- const configPath = join29(projectRoot, MCP_PROJECT_CONFIG_NAME);
18697
- if (!existsSync34(configPath)) return null;
18927
+ const configPath = join28(projectRoot, MCP_PROJECT_CONFIG_NAME);
18928
+ if (!existsSync33(configPath)) return null;
18698
18929
  try {
18699
- const raw = JSON.parse(readFileSync27(configPath, "utf-8"));
18930
+ const raw = JSON.parse(readFileSync26(configPath, "utf-8"));
18700
18931
  return raw.mcpServers ?? raw;
18701
18932
  } catch {
18702
18933
  return null;
@@ -18733,13 +18964,13 @@ function parseCookie(cookie) {
18733
18964
  }
18734
18965
  async function openBrowser(url) {
18735
18966
  try {
18736
- const { spawn: spawn5 } = await import("child_process");
18967
+ const { spawn: spawn6 } = await import("child_process");
18737
18968
  if (process.platform === "win32") {
18738
- spawn5("cmd", ["/c", "start", '""', url], { detached: true, stdio: "ignore" }).unref();
18969
+ spawn6("cmd", ["/c", "start", '""', url], { detached: true, stdio: "ignore" }).unref();
18739
18970
  } else if (process.platform === "darwin") {
18740
- spawn5("open", [url], { detached: true, stdio: "ignore" }).unref();
18971
+ spawn6("open", [url], { detached: true, stdio: "ignore" }).unref();
18741
18972
  } else {
18742
- spawn5("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
18973
+ spawn6("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
18743
18974
  }
18744
18975
  } catch {
18745
18976
  }