jinzd-ai-cli 0.4.222 → 0.4.223

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 (32) hide show
  1. package/dist/{batch-QOOO7HRW.js → batch-PJIRZJCT.js} +2 -2
  2. package/dist/{chunk-OJL3PY36.js → chunk-2X2DOHA6.js} +3 -2
  3. package/dist/{chunk-TNW22OUY.js → chunk-35AVZVXZ.js} +3 -3
  4. package/dist/{chunk-NZ4X6GUC.js → chunk-3ARPAVNV.js} +2 -2
  5. package/dist/{chunk-5EE4GRGG.js → chunk-5ZPO47YE.js} +3 -3
  6. package/dist/{chunk-AV5OPK7Q.js → chunk-GH37CLOE.js} +1 -1
  7. package/dist/{chunk-NNWWMGYK.js → chunk-GXAGBILB.js} +1 -1
  8. package/dist/{chunk-VWTUYDZG.js → chunk-OABKF7MR.js} +1 -1
  9. package/dist/{chunk-QT5FA2ZY.js → chunk-OJXYL2W2.js} +4 -4
  10. package/dist/{chunk-FBK7NSIK.js → chunk-OPAOQZDC.js} +2 -2
  11. package/dist/{chunk-VBRCWH55.js → chunk-PNFOWBKL.js} +3 -3
  12. package/dist/{chunk-SISFAT6W.js → chunk-RGL2KKCO.js} +1 -1
  13. package/dist/{chunk-Q3ZUDA6S.js → chunk-VT6NRDD2.js} +1 -1
  14. package/dist/{chunk-UWUTLIOH.js → chunk-WDUKU5HH.js} +1 -1
  15. package/dist/{chunk-S6L5R6SS.js → chunk-ZILWADNL.js} +1 -1
  16. package/dist/{ci-IB7NWHZN.js → ci-ESZOGOGZ.js} +4 -4
  17. package/dist/{ci-format-YLGZN3D4.js → ci-format-TIDA4V7T.js} +2 -2
  18. package/dist/{constants-BOMLZEXC.js → constants-VFBSBHFO.js} +1 -1
  19. package/dist/{doctor-cli-2ND4LONW.js → doctor-cli-W3I33E4Q.js} +4 -4
  20. package/dist/electron-server.js +1736 -1735
  21. package/dist/{hub-6WADFJNC.js → hub-7BLT2JUU.js} +6 -6
  22. package/dist/{hub-server-LJ2JSKZ2.js → hub-server-XX36DX7O.js} +2 -2
  23. package/dist/index.js +18 -18
  24. package/dist/{persist-GTBEKVFL.js → persist-GJ2JGZFR.js} +1 -1
  25. package/dist/{pr-F7WXUH27.js → pr-7YET7ODT.js} +4 -4
  26. package/dist/{run-tests-NIY24YDP.js → run-tests-4PNOUHC3.js} +1 -1
  27. package/dist/{run-tests-5NEMM6EF.js → run-tests-HGBTHWNT.js} +2 -2
  28. package/dist/{server-QGZYDDZW.js → server-AN7HZVNG.js} +1265 -1264
  29. package/dist/{server-ACFX2J66.js → server-APQHU4TR.js} +4 -4
  30. package/dist/{task-orchestrator-4GOBFXPN.js → task-orchestrator-L7LBMQUQ.js} +15 -7
  31. package/dist/{usage-JXT2YVA6.js → usage-BTP5SSGC.js} +2 -2
  32. 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-GH37CLOE.js";
42
42
  import {
43
43
  hasSemanticIndex,
44
44
  semanticSearch
@@ -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
@@ -7257,7 +7257,7 @@ var ToolExecutor = class {
7257
7257
  rl.resume();
7258
7258
  process.stdout.write(prompt);
7259
7259
  this.confirming = true;
7260
- return new Promise((resolve15) => {
7260
+ return new Promise((resolve14) => {
7261
7261
  let completed = false;
7262
7262
  const cleanup = (result) => {
7263
7263
  if (completed) return;
@@ -7267,7 +7267,7 @@ var ToolExecutor = class {
7267
7267
  rl.pause();
7268
7268
  rlAny.output = savedOutput;
7269
7269
  this.confirming = false;
7270
- resolve15(result);
7270
+ resolve14(result);
7271
7271
  };
7272
7272
  const onLine = (line) => {
7273
7273
  const trimmed = line.trim();
@@ -7441,7 +7441,7 @@ var ToolExecutor = class {
7441
7441
  rl.resume();
7442
7442
  process.stdout.write(color("Proceed? [y/N] (type y + Enter to confirm) "));
7443
7443
  this.confirming = true;
7444
- return new Promise((resolve15) => {
7444
+ return new Promise((resolve14) => {
7445
7445
  let completed = false;
7446
7446
  const cleanup = (answer) => {
7447
7447
  if (completed) return;
@@ -7451,7 +7451,7 @@ var ToolExecutor = class {
7451
7451
  rl.pause();
7452
7452
  rlAny.output = savedOutput;
7453
7453
  this.confirming = false;
7454
- resolve15(answer === "y");
7454
+ resolve14(answer === "y");
7455
7455
  };
7456
7456
  const onLine = (line) => {
7457
7457
  const trimmed = line.trim();
@@ -8700,7 +8700,7 @@ var runInteractiveTool = {
8700
8700
  PYTHONDONTWRITEBYTECODE: "1"
8701
8701
  };
8702
8702
  const prefixWarnings = [argsTypeWarning, stdinTypeWarning].filter(Boolean).join("");
8703
- return new Promise((resolve15) => {
8703
+ return new Promise((resolve14) => {
8704
8704
  const child = spawn2(executable, cmdArgs.map(String), {
8705
8705
  cwd: process.cwd(),
8706
8706
  env,
@@ -8733,22 +8733,22 @@ var runInteractiveTool = {
8733
8733
  setTimeout(writeNextLine, 400);
8734
8734
  const timer = setTimeout(() => {
8735
8735
  child.kill();
8736
- resolve15(`${prefixWarnings}[Timeout after ${timeout}ms]
8736
+ resolve14(`${prefixWarnings}[Timeout after ${timeout}ms]
8737
8737
  ${buildOutput(stdout, stderr)}`);
8738
8738
  }, timeout);
8739
8739
  child.on("close", (code) => {
8740
8740
  clearTimeout(timer);
8741
8741
  const output = buildOutput(stdout, stderr);
8742
8742
  if (code !== 0 && code !== null) {
8743
- resolve15(`${prefixWarnings}Exit code ${code}:
8743
+ resolve14(`${prefixWarnings}Exit code ${code}:
8744
8744
  ${output}`);
8745
8745
  } else {
8746
- resolve15(`${prefixWarnings}${output || "(no output)"}`);
8746
+ resolve14(`${prefixWarnings}${output || "(no output)"}`);
8747
8747
  }
8748
8748
  });
8749
8749
  child.on("error", (err) => {
8750
8750
  clearTimeout(timer);
8751
- resolve15(
8751
+ resolve14(
8752
8752
  `${prefixWarnings}Failed to start process "${executable}": ${err.message}
8753
8753
  Hint: On Windows, use the full path to the executable, e.g.:
8754
8754
  C:\\Users\\Jinzd\\anaconda3\\envs\\python312\\python.exe`
@@ -9798,7 +9798,7 @@ function promptUser(rl, question) {
9798
9798
  console.log();
9799
9799
  console.log(chalk4.cyan("\u2753 ") + chalk4.bold(question));
9800
9800
  process.stdout.write(chalk4.cyan("> "));
9801
- return new Promise((resolve15) => {
9801
+ return new Promise((resolve14) => {
9802
9802
  let completed = false;
9803
9803
  const cleanup = (answer) => {
9804
9804
  if (completed) return;
@@ -9808,7 +9808,7 @@ function promptUser(rl, question) {
9808
9808
  rl.pause();
9809
9809
  rlAny.output = savedOutput;
9810
9810
  askUserContext.prompting = false;
9811
- resolve15(answer);
9811
+ resolve14(answer);
9812
9812
  };
9813
9813
  const onLine = (line) => {
9814
9814
  cleanup(line);
@@ -12253,7 +12253,7 @@ var McpClient = class {
12253
12253
  // 内部方法:JSON-RPC 通信
12254
12254
  // ══════════════════════════════════════════════════════════════════
12255
12255
  sendRequest(method, params) {
12256
- return new Promise((resolve15, reject) => {
12256
+ return new Promise((resolve14, reject) => {
12257
12257
  if (!this.process?.stdin?.writable) {
12258
12258
  return reject(new Error(`MCP server [${this.serverId}] stdin not writable`));
12259
12259
  }
@@ -12277,7 +12277,7 @@ var McpClient = class {
12277
12277
  this.pendingRequests.set(id, {
12278
12278
  resolve: (result) => {
12279
12279
  cleanup();
12280
- resolve15(result);
12280
+ resolve14(result);
12281
12281
  },
12282
12282
  reject: (error) => {
12283
12283
  cleanup();
@@ -12354,13 +12354,13 @@ var McpClient = class {
12354
12354
  }
12355
12355
  /** Promise 超时包装 */
12356
12356
  withTimeout(promise, ms, label) {
12357
- return new Promise((resolve15, reject) => {
12357
+ return new Promise((resolve14, reject) => {
12358
12358
  const timer = setTimeout(() => {
12359
12359
  reject(new Error(`MCP [${this.serverId}] ${label} timed out after ${ms}ms`));
12360
12360
  }, ms);
12361
12361
  promise.then((val) => {
12362
12362
  clearTimeout(timer);
12363
- resolve15(val);
12363
+ resolve14(val);
12364
12364
  }).catch((err) => {
12365
12365
  clearTimeout(timer);
12366
12366
  reject(err);
@@ -12864,33 +12864,33 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
12864
12864
  }
12865
12865
  /** Resolve a pending confirm from client response */
12866
12866
  resolveConfirm(requestId, approved) {
12867
- const resolve15 = this.pendingConfirms.get(requestId);
12868
- if (resolve15) {
12867
+ const resolve14 = this.pendingConfirms.get(requestId);
12868
+ if (resolve14) {
12869
12869
  this.clearPendingTimer(requestId);
12870
12870
  this.pendingConfirms.delete(requestId);
12871
12871
  this.confirming = false;
12872
- resolve15(approved);
12872
+ resolve14(approved);
12873
12873
  }
12874
12874
  }
12875
12875
  /** Resolve a pending batch confirm from client response */
12876
12876
  resolveBatchConfirm(requestId, decision) {
12877
- const resolve15 = this.pendingBatchConfirms.get(requestId);
12878
- if (resolve15) {
12877
+ const resolve14 = this.pendingBatchConfirms.get(requestId);
12878
+ if (resolve14) {
12879
12879
  this.clearPendingTimer(requestId);
12880
12880
  this.pendingBatchConfirms.delete(requestId);
12881
12881
  this.confirming = false;
12882
12882
  if (decision === "all" || decision === "none") {
12883
- resolve15(decision);
12883
+ resolve14(decision);
12884
12884
  } else {
12885
- resolve15(new Set(decision));
12885
+ resolve14(new Set(decision));
12886
12886
  }
12887
12887
  }
12888
12888
  }
12889
12889
  /** Cancel all pending confirms (e.g., on disconnect) */
12890
12890
  cancelAll() {
12891
- for (const resolve15 of this.pendingConfirms.values()) resolve15(false);
12891
+ for (const resolve14 of this.pendingConfirms.values()) resolve14(false);
12892
12892
  this.pendingConfirms.clear();
12893
- for (const resolve15 of this.pendingBatchConfirms.values()) resolve15("none");
12893
+ for (const resolve14 of this.pendingBatchConfirms.values()) resolve14("none");
12894
12894
  this.pendingBatchConfirms.clear();
12895
12895
  this.confirming = false;
12896
12896
  }
@@ -12966,8 +12966,8 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
12966
12966
  diff: this.getDiffPreview(call)
12967
12967
  };
12968
12968
  this.send(msg);
12969
- return new Promise((resolve15) => {
12970
- this.pendingConfirms.set(requestId, resolve15);
12969
+ return new Promise((resolve14) => {
12970
+ this.pendingConfirms.set(requestId, resolve14);
12971
12971
  this.pendingTimers.set(requestId, setTimeout(() => {
12972
12972
  if (this.pendingConfirms.has(requestId)) {
12973
12973
  this.resolveConfirm(requestId, false);
@@ -12991,8 +12991,8 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
12991
12991
  files
12992
12992
  };
12993
12993
  this.send(msg);
12994
- return new Promise((resolve15) => {
12995
- this.pendingBatchConfirms.set(requestId, resolve15);
12994
+ return new Promise((resolve14) => {
12995
+ this.pendingBatchConfirms.set(requestId, resolve14);
12996
12996
  this.pendingTimers.set(requestId, setTimeout(() => {
12997
12997
  if (this.pendingBatchConfirms.has(requestId)) {
12998
12998
  this.resolveBatchConfirm(requestId, "none");
@@ -13661,8 +13661,8 @@ function loadContextFiles(options) {
13661
13661
  }
13662
13662
 
13663
13663
  // 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";
13664
+ import { mkdirSync as mkdirSync16, createWriteStream as createWriteStream3 } from "fs";
13665
+ import { join as join26, dirname as dirname10 } from "path";
13666
13666
 
13667
13667
  // src/repl/commands/project-init.ts
13668
13668
  import { existsSync as existsSync26, readFileSync as readFileSync20, readdirSync as readdirSync11, statSync as statSync9 } from "fs";
@@ -14026,7 +14026,7 @@ ${topic}${contextSection}`;
14026
14026
  messages.push({
14027
14027
  role: "assistant",
14028
14028
  content: msg.content,
14029
- timestamp: msg.timestamp
14029
+ timestamp: new Date(msg.timestamp)
14030
14030
  });
14031
14031
  } else {
14032
14032
  const prefix = `[${msg.speakerName} (${msg.speaker})]:`;
@@ -14034,7 +14034,7 @@ ${topic}${contextSection}`;
14034
14034
  role: "user",
14035
14035
  content: `${prefix}
14036
14036
  ${msg.content}`,
14037
- timestamp: msg.timestamp
14037
+ timestamp: new Date(msg.timestamp)
14038
14038
  });
14039
14039
  }
14040
14040
  }
@@ -14429,14 +14429,14 @@ import { join as join21 } from "path";
14429
14429
  function discussionToMessages(state2) {
14430
14430
  const out = [];
14431
14431
  const t0 = state2.messages[0]?.timestamp ?? /* @__PURE__ */ new Date();
14432
- out.push({ role: "user", content: `\u{1F3DB} Topic: ${state2.topic}`, timestamp: t0 });
14432
+ out.push({ role: "user", content: `\u{1F3DB} Topic: ${state2.topic}`, timestamp: new Date(t0) });
14433
14433
  for (const m of state2.messages) {
14434
14434
  if (m.speaker === "system") {
14435
- out.push({ role: "system", content: m.content, timestamp: m.timestamp });
14435
+ out.push({ role: "system", content: m.content, timestamp: new Date(m.timestamp) });
14436
14436
  continue;
14437
14437
  }
14438
14438
  if (m.speaker === "human") {
14439
- out.push({ role: "user", content: `\u{1F9ED} ${m.speakerName}: ${m.content}`, timestamp: m.timestamp });
14439
+ out.push({ role: "user", content: `\u{1F9ED} ${m.speakerName}: ${m.content}`, timestamp: new Date(m.timestamp) });
14440
14440
  continue;
14441
14441
  }
14442
14442
  if (m.passed || !m.content.trim()) continue;
@@ -14446,7 +14446,7 @@ function discussionToMessages(state2) {
14446
14446
  content: `**${m.speakerName}** (${m.speaker})${tag}
14447
14447
 
14448
14448
  ${m.content}`,
14449
- timestamp: m.timestamp
14449
+ timestamp: new Date(m.timestamp)
14450
14450
  });
14451
14451
  }
14452
14452
  if (state2.summary && state2.summary.trim()) {
@@ -14468,1708 +14468,1726 @@ async function persistDiscussion(state2, config, defaultProvider, defaultModel)
14468
14468
 
14469
14469
  // src/web/commands/provider-commands.ts
14470
14470
  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();
14471
+ const id = args[0];
14472
+ if (!id) {
14473
+ ctx.send({ type: "error", message: "Usage: /provider <id>" });
14474
+ return;
14475
+ }
14476
+ const p = ctx.providers.get(id);
14477
+ if (!p) {
14478
+ ctx.send({ type: "error", message: `Provider "${id}" not available` });
14479
+ return;
14487
14480
  }
14481
+ ctx.currentProvider = id;
14482
+ ctx.currentModel = p.info.defaultModel;
14483
+ ctx.updateContextWindow();
14484
+ ctx.send({ type: "info", message: `Switched to provider: ${id} (${ctx.currentModel})` });
14485
+ ctx.sendStatus();
14488
14486
  }
14489
14487
  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;
14488
+ const sub = args[0];
14489
+ if (!sub) {
14490
+ ctx.send({ type: "error", message: "Usage: /model <id>|refresh [provider]|cache [clear [provider]]" });
14491
+ return;
14492
+ }
14493
+ if (sub === "refresh") {
14494
+ const providerId = args[1] ?? ctx.currentProvider;
14495
+ try {
14496
+ const models2 = await ctx.providers.refreshModels(providerId);
14497
+ ctx.send({ type: "info", message: `Refreshed ${models2.length} model(s) for ${providerId}.` });
14498
+ ctx.sendStatus();
14499
+ } catch (err) {
14500
+ ctx.send({ type: "error", message: `Model refresh failed: ${err instanceof Error ? err.message : String(err)}` });
14520
14501
  }
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` });
14502
+ return;
14503
+ }
14504
+ if (sub === "cache") {
14505
+ if ((args[1] ?? "").toLowerCase() === "clear") {
14506
+ const providerId = args[2];
14507
+ ctx.providers.clearModelCache(providerId);
14508
+ ctx.send({ type: "info", message: providerId ? `Cleared model cache for ${providerId}.` : "Cleared model cache." });
14526
14509
  return;
14527
14510
  }
14528
- ctx.currentModel = modelId;
14529
- ctx.updateContextWindow();
14530
- ctx.send({ type: "info", message: `Switched to model: ${modelId}` });
14531
- ctx.sendStatus();
14511
+ const rows = ctx.providers.getModelCacheStatus();
14512
+ ctx.send({
14513
+ type: "info",
14514
+ 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")
14515
+ });
14516
+ return;
14532
14517
  }
14518
+ const modelId = sub;
14519
+ const models = await ctx.providers.listModels(ctx.currentProvider);
14520
+ const found = models.find((m) => m.id === modelId);
14521
+ if (!found) {
14522
+ ctx.send({ type: "error", message: `Model "${modelId}" not found` });
14523
+ return;
14524
+ }
14525
+ ctx.currentModel = modelId;
14526
+ ctx.updateContextWindow();
14527
+ ctx.send({ type: "info", message: `Switched to model: ${modelId}` });
14528
+ ctx.sendStatus();
14533
14529
  }
14534
14530
  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
- }
14531
+ const sub = args[0];
14532
+ if (sub === "on") ctx.runtimeThinking = true;
14533
+ else if (sub === "off") ctx.runtimeThinking = false;
14534
+ else ctx.runtimeThinking = !ctx.runtimeThinking;
14535
+ ctx.send({ type: "info", message: `Thinking mode: ${ctx.runtimeThinking ? "ON" : "OFF"}` });
14536
+ ctx.sendStatus();
14543
14537
  }
14544
14538
  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();
14539
+ const sub = args[0];
14540
+ if (sub === "exit" || sub === "cancel") {
14541
+ ctx.planMode = false;
14542
+ ctx.send({ type: "info", message: "Plan mode OFF." });
14543
+ } else if (sub === "execute") {
14544
+ ctx.planMode = false;
14545
+ ctx.send({ type: "info", message: "Plan mode OFF. Executing with all tools enabled." });
14546
+ } else {
14547
+ ctx.planMode = !ctx.planMode;
14548
+ ctx.send({ type: "info", message: `Plan mode: ${ctx.planMode ? "ON (read-only tools only)" : "OFF"}` });
14558
14549
  }
14550
+ ctx.sendStatus();
14559
14551
  }
14560
14552
 
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();
14553
+ // src/web/commands/_shared.ts
14554
+ import { existsSync as existsSync29, readFileSync as readFileSync22, writeFileSync as writeFileSync3, mkdirSync as mkdirSync14, readdirSync as readdirSync13, statSync as statSync12, createWriteStream } from "fs";
14555
+ import { join as join24, resolve as resolve10, dirname as dirname8 } from "path";
14556
+
14557
+ // src/cli/review-prompts.ts
14558
+ function buildReviewPrompt(diff, gitContextStr, detailed) {
14559
+ 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.";
14560
+ return `# Code Review Request
14561
+
14562
+ ${level}
14563
+
14564
+ ## Git Status
14565
+ ${gitContextStr}
14566
+
14567
+ ## Code Changes (diff)
14568
+ \`\`\`diff
14569
+ ${diff}
14570
+ \`\`\`
14571
+
14572
+ ## Output Format
14573
+ Please structure your review as follows:
14574
+ 1. **Overall Assessment**: One-sentence summary of the change quality
14575
+ 2. **Issues** (if any): Each issue with [Severity] file:line \u2014 description + suggested fix
14576
+ 3. **Improvement Suggestions** (if any): Non-critical but recommended optimizations
14577
+ 4. **Highlights** (if any): Good practices worth acknowledging
14578
+
14579
+ Severity levels: \u{1F534} Critical / \u{1F7E1} Warning / \u{1F535} Info`;
14569
14580
  }
14570
- async function handleCompact(args, ctx) {
14571
- await ctx.compactSession(args.join(" ") || void 0);
14581
+ function buildSecurityReviewPrompt(diff, gitContextStr) {
14582
+ return `# Security Vulnerability Review
14583
+
14584
+ Analyze the following code changes **exclusively for security vulnerabilities**.
14585
+
14586
+ ## Categories to check:
14587
+ 1. **Injection** \u2014 SQL, command, path traversal, XSS, template injection
14588
+ 2. **Authentication & Authorization** \u2014 hardcoded credentials, missing auth checks, privilege escalation
14589
+ 3. **Secrets & Sensitive Data** \u2014 API keys, tokens, passwords in code, logging sensitive data
14590
+ 4. **Input Validation** \u2014 missing validation, unsafe deserialization, buffer issues
14591
+ 5. **Cryptography** \u2014 weak algorithms, improper random, hardcoded IVs/salts
14592
+ 6. **Dependencies** \u2014 known vulnerable packages, unsafe dynamic imports
14593
+ 7. **File System** \u2014 path traversal, unsafe file permissions, symlink attacks
14594
+ 8. **Network** \u2014 SSRF, insecure protocols, missing TLS validation
14595
+
14596
+ ## Git Status
14597
+ ${gitContextStr}
14598
+
14599
+ ## Code Changes (diff)
14600
+ \`\`\`diff
14601
+ ${diff}
14602
+ \`\`\`
14603
+
14604
+ ## Output Format
14605
+ For each finding:
14606
+ - **Severity**: \u{1F534} CRITICAL / \u{1F7E0} HIGH / \u{1F7E1} MEDIUM / \u{1F535} LOW / \u2139\uFE0F INFO
14607
+ - **Category**: (from list above)
14608
+ - **File & location**: file:line
14609
+ - **Description**: what the vulnerability is and how it could be exploited
14610
+ - **Recommended fix**: specific code change to resolve
14611
+
14612
+ If no security issues found, state "\u2705 No security vulnerabilities detected" with a brief explanation of what was checked.`;
14572
14613
  }
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
- }
14614
+
14615
+ // src/core/git-diff.ts
14616
+ import { execFileSync as execFileSync3 } from "child_process";
14617
+ function readGitDiff(options = {}) {
14618
+ const args = ["diff"];
14619
+ if (options.staged) args.push("--staged");
14620
+ return execFileSync3("git", args, {
14621
+ encoding: "utf-8",
14622
+ timeout: options.timeoutMs ?? 1e4
14623
+ }).trim();
14591
14624
  }
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>" });
14720
- }
14721
- }
14625
+
14626
+ // src/diagnostics/doctor-report.ts
14627
+ import { existsSync as existsSync28, statSync as statSync11 } from "fs";
14628
+ import { join as join23 } from "path";
14629
+ import { arch as arch2, platform as platform6, release as release2 } from "os";
14630
+
14631
+ // src/diagnostics/crash-log.ts
14632
+ import {
14633
+ existsSync as existsSync27,
14634
+ mkdirSync as mkdirSync13,
14635
+ readdirSync as readdirSync12,
14636
+ readFileSync as readFileSync21,
14637
+ statSync as statSync10,
14638
+ unlinkSync as unlinkSync6,
14639
+ writeFileSync as writeFileSync2
14640
+ } from "fs";
14641
+ import { join as join22 } from "path";
14642
+ import { homedir as homedir8, platform as platform5, release, arch } from "os";
14643
+ var LOGS_DIR_NAME = "logs";
14644
+ var CRASH_LOG_PREFIX = "crash-";
14645
+ function getLogsDir(configDir) {
14646
+ const base = configDir ?? join22(homedir8(), CONFIG_DIR_NAME);
14647
+ return join22(base, LOGS_DIR_NAME);
14722
14648
  }
14723
- async function handleExport(args, ctx) {
14724
- {
14725
- const format = args[0] === "json" ? "json" : "md";
14726
- ctx.exportConversation(format);
14727
- }
14649
+ function truncate(s, n) {
14650
+ return s.length <= n ? s : s.slice(0, n) + "\u2026";
14728
14651
  }
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
14652
+ function listRecentCrashes(limit = 10, configDir) {
14653
+ const dir = getLogsDir(configDir);
14654
+ if (!existsSync27(dir)) return [];
14655
+ const files = [];
14656
+ let names;
14657
+ try {
14658
+ names = readdirSync12(dir);
14659
+ } catch {
14660
+ return [];
14661
+ }
14662
+ for (const name of names) {
14663
+ if (!name.startsWith(CRASH_LOG_PREFIX) || !name.endsWith(".log")) continue;
14664
+ const full = join22(dir, name);
14665
+ try {
14666
+ const st = statSync10(full);
14667
+ const head = readFileSync21(full, "utf-8").split("\n").slice(0, 20);
14668
+ const tsLine = head.find((l) => l.startsWith("timestamp:"));
14669
+ const kindLine = head.find((l) => l.startsWith("kind:"));
14670
+ const msgIdx = head.findIndex((l) => l.trim() === "## message");
14671
+ const message = msgIdx >= 0 && head[msgIdx + 1] ? head[msgIdx + 1] : "";
14672
+ files.push({
14673
+ file: full,
14674
+ timestamp: tsLine ? tsLine.replace("timestamp:", "").trim() : st.mtime.toISOString(),
14675
+ kind: kindLine ? kindLine.replace("kind:", "").trim() : "unknown",
14676
+ message: truncate(message, 200),
14677
+ sizeBytes: st.size
14741
14678
  });
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)`);
14679
+ } catch {
14764
14680
  }
14765
- ctx.send({ type: "info", message: lines.join("\n") });
14766
14681
  }
14682
+ files.sort((a, b) => a.timestamp < b.timestamp ? 1 : -1);
14683
+ return files.slice(0, limit);
14767
14684
  }
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.` });
14819
- }
14685
+ function getConfigDirUsage(configDir) {
14686
+ const base = configDir ?? join22(homedir8(), CONFIG_DIR_NAME);
14687
+ if (!existsSync27(base)) return { totalBytes: 0, entries: [] };
14688
+ const entries = [];
14689
+ let total = 0;
14690
+ let names;
14691
+ try {
14692
+ names = readdirSync12(base);
14693
+ } catch {
14694
+ return { totalBytes: 0, entries: [] };
14695
+ }
14696
+ for (const name of names) {
14697
+ const full = join22(base, name);
14698
+ const bytes = dirSize(full);
14699
+ entries.push({ name, bytes });
14700
+ total += bytes;
14820
14701
  }
14702
+ entries.sort((a, b) => b.bytes - a.bytes);
14703
+ return { totalBytes: total, entries };
14821
14704
  }
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
- }
14926
- }
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") });
14965
- }
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();
14705
+ function dirSize(path3) {
14706
+ try {
14707
+ const st = statSync10(path3);
14708
+ if (st.isFile()) return st.size;
14709
+ if (!st.isDirectory()) return 0;
14710
+ let sum = 0;
14711
+ for (const name of readdirSync12(path3)) {
14712
+ sum += dirSize(join22(path3, name));
14988
14713
  }
14989
- ctx.send({ type: "error", message: `Unknown subcommand: ${sub}. Use list/new/switch/delete/rename/diff/cherry-pick.` });
14714
+ return sum;
14715
+ } catch {
14716
+ return 0;
14990
14717
  }
14991
14718
  }
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
- }
14719
+
14720
+ // src/diagnostics/doctor-report.ts
14721
+ function checkFile(label, path3) {
14722
+ const exists = existsSync28(path3);
14723
+ let sizeBytes = null;
14724
+ if (exists) {
15010
14725
  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}` });
14726
+ sizeBytes = statSync11(path3).size;
14727
+ } catch {
15023
14728
  }
15024
14729
  }
14730
+ return { label, path: path3, exists, sizeBytes };
15025
14731
  }
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.");
14732
+ function buildDoctorReport(options) {
14733
+ const config = options.config;
14734
+ const cwd = options.cwd ?? process.cwd();
14735
+ const configDir = config.getConfigDir();
14736
+ const projectRoot = getGitRoot(cwd) ?? cwd;
14737
+ const contextCfg = config.get("context");
14738
+ const contextSetting = config.get("contextFile");
14739
+ const context = loadContextFiles({
14740
+ cwd,
14741
+ configDir,
14742
+ projectRoot,
14743
+ setting: contextSetting,
14744
+ maxBytes: contextCfg.projectDocMaxBytes,
14745
+ fallbackFilenames: contextCfg.projectDocFallbackFilenames
14746
+ });
14747
+ const hooksConfig = config.get("hooks");
14748
+ const hooks = listHooks(hooksConfig, configDir);
14749
+ const pendingHooks = getPendingHookTrust(hooksConfig, configDir);
14750
+ const plugins = listInstalledPlugins(configDir);
14751
+ const activeAssets = getActivePluginAssets(configDir);
14752
+ const network = config.get("networkPolicy");
14753
+ const mcpServers = config.get("mcpServers");
14754
+ const allStats = getStatsSnapshot();
14755
+ return {
14756
+ version: VERSION,
14757
+ npmCheck: options.npmCheck,
14758
+ node: process.version,
14759
+ platform: `${platform6()} ${release2()} (${arch2()})`,
14760
+ cwd,
14761
+ projectRoot,
14762
+ configDir,
14763
+ providers: options.providers.listAll().map((p) => ({ id: p.id, displayName: p.displayName, configured: p.configured })),
14764
+ files: [
14765
+ checkFile("config.json", join23(configDir, "config.json")),
14766
+ checkFile("memory.md", join23(configDir, MEMORY_FILE_NAME)),
14767
+ checkFile("memory.jsonl", join23(configDir, "memory.jsonl")),
14768
+ checkFile("dev-state.md", join23(configDir, DEV_STATE_FILE_NAME)),
14769
+ checkFile("hooks-trust.json", join23(configDir, "hooks-trust.json")),
14770
+ checkFile("plugin-state.json", join23(configDir, "plugins", "plugin-state.json"))
14771
+ ],
14772
+ context: {
14773
+ enabled: contextSetting !== false,
14774
+ setting: contextSetting,
14775
+ candidates: context.candidates,
14776
+ maxBytes: context.maxBytes,
14777
+ layers: context.layers.map((l) => ({ level: l.level, fileName: l.fileName, displayPath: l.displayPath, bytes: l.byteCount, chars: l.charCount, truncated: l.truncated })),
14778
+ skipped: context.skipped.map((s) => ({ level: s.level, fileName: s.fileName, displayPath: s.displayPath, reason: s.reason, message: s.message }))
14779
+ },
14780
+ permissions: {
14781
+ defaultProfile: config.get("defaultPermissionProfile"),
14782
+ allowedProfiles: config.get("allowedPermissionProfiles"),
14783
+ customProfileCount: Object.keys(config.get("permissionProfiles")).length,
14784
+ legacyDefaultAction: config.get("defaultPermission"),
14785
+ ruleCount: config.get("permissionRules").length
14786
+ },
14787
+ networkPolicy: {
14788
+ enabled: network.enabled,
14789
+ defaultAction: network.defaultAction,
14790
+ allowDomains: network.allowDomains.length,
14791
+ denyDomains: network.denyDomains.length,
14792
+ allowPorts: network.allowPorts.length,
14793
+ denyPorts: network.denyPorts.length,
14794
+ allowPrivateNetwork: network.allowPrivateNetwork,
14795
+ toolOverrides: Object.fromEntries(Object.entries(network.tools).filter(([, v]) => v !== void 0))
14796
+ },
14797
+ hooks: {
14798
+ enabled: hooksConfig?.enabled !== false,
14799
+ total: hooks.length,
14800
+ project: hooks.filter((h) => h.source === "project").length,
14801
+ pendingTrust: pendingHooks.length,
14802
+ disabled: hooks.filter((h) => h.disabled).length
14803
+ },
14804
+ plugins: {
14805
+ root: join23(configDir, "plugins"),
14806
+ installed: plugins.length,
14807
+ active: activeAssets.plugins.length,
14808
+ invalid: plugins.filter((p) => !p.valid).length,
14809
+ untrusted: plugins.filter((p) => p.valid && !p.trusted).length,
14810
+ disabled: plugins.filter((p) => p.valid && !p.enabled).length,
14811
+ mcpServers: Object.keys(activeAssets.mcpServers).length,
14812
+ skillDirs: activeAssets.skillDirs.length,
14813
+ commandDirs: activeAssets.commandDirs.length,
14814
+ agentDirs: activeAssets.agentDirs.length,
14815
+ entries: plugins.map((p) => ({ name: p.name, version: p.manifest.version, enabled: p.enabled, trusted: p.trusted, valid: p.valid, error: p.error }))
14816
+ },
14817
+ mcp: {
14818
+ enabled: config.get("mcpEnabled"),
14819
+ configured: Object.keys(mcpServers).length,
14820
+ projectConfigExists: existsSync28(join23(projectRoot, MCP_PROJECT_CONFIG_NAME)),
14821
+ statuses: options.mcpStatuses ?? []
14822
+ },
14823
+ recentCrashes: listRecentCrashes(5, configDir),
14824
+ diskUsage: getConfigDirUsage(configDir),
14825
+ toolStats: {
14826
+ totalCalls: allStats.reduce((a, b) => a + b.calls, 0),
14827
+ totalFailures: allStats.reduce((a, b) => a + b.failures, 0),
14828
+ topUsed: getTopUsedTools(5),
14829
+ topFailing: getTopFailingTools(5)
15063
14830
  }
15064
- ctx.send({ type: "info", message: rewindLines.join("\n") });
15065
- ctx.sendSessionMessages();
15066
- ctx.sendStatus();
15067
- }
14831
+ };
15068
14832
  }
15069
14833
 
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();
14834
+ // src/diagnostics/doctor-cli.ts
14835
+ function formatBytes(n) {
14836
+ if (n < 1024) return `${n} B`;
14837
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
14838
+ if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;
14839
+ return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
15075
14840
  }
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
- }
14841
+ function formatDoctorReport(report, ansi = true) {
14842
+ const wrap = (code, s) => ansi ? `${code}${s}\x1B[0m` : s;
14843
+ const B = (s) => wrap("\x1B[1m", s);
14844
+ const D = (s) => wrap("\x1B[2m", s);
14845
+ const G = (s) => wrap("\x1B[32m", s);
14846
+ const Y = (s) => wrap("\x1B[33m", s);
14847
+ const R = (s) => wrap("\x1B[31m", s);
14848
+ const out = [];
14849
+ out.push("");
14850
+ out.push(B("\u{1FA7A} AI-CLI Health Check"));
14851
+ out.push("");
14852
+ const nc = report.npmCheck;
14853
+ let versionLine = ` version: ${report.version}`;
14854
+ if (nc?.status === "up-to-date") versionLine += ` ${D("(latest on npm)")}`;
14855
+ else if (nc?.status === "outdated" && nc.latest) versionLine += ` ${Y(`\u2192 ${nc.latest} available`)} ${D("(npm i -g jinzd-ai-cli@latest)")}`;
14856
+ else if (nc?.status === "skipped") versionLine += ` ${D(`(npm check skipped: ${nc.reason ?? "unknown"})`)}`;
14857
+ out.push(versionLine);
14858
+ out.push(` node: ${report.node}`);
14859
+ out.push(` platform: ${report.platform}`);
14860
+ out.push(` cwd: ${report.cwd}`);
14861
+ out.push(` project: ${report.projectRoot}`);
14862
+ out.push(` config: ${report.configDir}`);
14863
+ out.push("");
14864
+ out.push(B("API Keys:"));
14865
+ for (const p of report.providers) {
14866
+ out.push(` ${p.configured ? G("\u2713") : D("\u25CB")} ${p.id.padEnd(14)} ${p.configured ? G("configured") : D("not configured")}`);
14867
+ }
14868
+ out.push("");
14869
+ out.push(B("Context Files:"));
14870
+ out.push(` status: ${report.context.enabled ? G("enabled") : D("disabled")} \xB7 max ${formatBytes(report.context.maxBytes)}`);
14871
+ if (report.context.layers.length === 0) out.push(` ${D("(no context files loaded)")}`);
14872
+ for (const l of report.context.layers) {
14873
+ out.push(` ${G("\u2713")} ${l.level.padEnd(7)} ${l.displayPath} ${D(`(${formatBytes(l.bytes)}${l.truncated ? ", truncated" : ""})`)}`);
14874
+ }
14875
+ if (report.context.skipped.length > 0) {
14876
+ out.push(` ${D("skipped:")}`);
14877
+ for (const s of report.context.skipped.slice(0, 5)) {
14878
+ out.push(` ${Y("!")} ${s.level}:${s.fileName} ${s.reason}${s.message ? ` \xB7 ${s.message}` : ""}`);
15096
14879
  }
15097
- ctx.send({ type: "info", message: lines.join("\n") });
15098
14880
  }
14881
+ out.push("");
14882
+ out.push(B("Permissions / Network:"));
14883
+ out.push(` profile: ${report.permissions.defaultProfile} \xB7 allowed ${report.permissions.allowedProfiles.join(", ")} \xB7 rules ${report.permissions.ruleCount} \xB7 custom profiles ${report.permissions.customProfileCount}`);
14884
+ const np = report.networkPolicy;
14885
+ 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"}`);
14886
+ const overrides = Object.entries(np.toolOverrides).map(([k, v]) => `${k}=${v}`).join(", ");
14887
+ if (overrides) out.push(` tool overrides: ${overrides}`);
14888
+ out.push("");
14889
+ out.push(B("Hooks / Plugins:"));
14890
+ 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}`);
14891
+ 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}`);
14892
+ 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)`);
14893
+ if (report.plugins.entries.length > 0) {
14894
+ for (const p of report.plugins.entries.slice(0, 5)) {
14895
+ const state2 = p.valid ? `${p.enabled ? "enabled" : "disabled"} \xB7 ${p.trusted ? "trusted" : "untrusted"}` : `invalid \xB7 ${p.error ?? ""}`;
14896
+ out.push(` ${p.valid ? G("\u2713") : R("\u2717")} ${p.name}@${p.version} ${D(state2)}`);
14897
+ }
14898
+ }
14899
+ out.push("");
14900
+ out.push(B("MCP Servers:"));
14901
+ out.push(` global configured: ${report.mcp.configured} \xB7 project .mcp.json: ${report.mcp.projectConfigExists ? "yes" : "no"} \xB7 mcpEnabled: ${report.mcp.enabled ? "true" : "false"}`);
14902
+ if (report.mcp.statuses.length === 0) out.push(` ${D("(no connected manager status in this mode)")}`);
14903
+ for (const s of report.mcp.statuses) {
14904
+ const state2 = s.connected ? `${G("connected")} \xB7 ${s.serverName} \xB7 ${s.toolCount} tools` : `${R("disconnected")}${s.error ? D(` \xB7 ${s.error}`) : ""}`;
14905
+ out.push(` ${s.connected ? G("\u2713") : R("\u2717")} ${s.serverId.padEnd(16)} ${state2}`);
14906
+ }
14907
+ out.push("");
14908
+ out.push(B("Config Files:"));
14909
+ for (const f of report.files) {
14910
+ const extra = f.exists && f.sizeBytes !== null ? ` ${D(`(${formatBytes(f.sizeBytes)})`)}` : "";
14911
+ out.push(` ${f.exists ? G("\u2713") : D("\u2013")} ${f.label.padEnd(16)} ${f.exists ? f.path : D("(not found)")}${extra}`);
14912
+ }
14913
+ out.push("");
14914
+ out.push(B("Recent Crashes (last 5):"));
14915
+ if (report.recentCrashes.length === 0) out.push(` ${D("(none - clean!)")}`);
14916
+ for (const c of report.recentCrashes) {
14917
+ out.push(` ${R("\u2717")} ${c.timestamp} ${Y(c.kind)}`);
14918
+ out.push(` ${D(c.message)}`);
14919
+ out.push(` ${D(c.file)}`);
14920
+ }
14921
+ out.push("");
14922
+ const ts = report.toolStats;
14923
+ out.push(B("Tool Usage:"));
14924
+ out.push(` total: ${ts.totalCalls} calls \xB7 ${ts.totalFailures} failures (${ts.totalCalls ? (ts.totalFailures * 100 / ts.totalCalls).toFixed(1) : "0.0"}%)`);
14925
+ if (ts.topUsed.length > 0) {
14926
+ out.push(` ${D("top used:")}`);
14927
+ 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`);
14928
+ }
14929
+ if (ts.topFailing.length > 0) {
14930
+ out.push(` ${D("top failing:")}`);
14931
+ 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));
14932
+ }
14933
+ out.push("");
14934
+ const dirAnnotations = {
14935
+ models: "semantic search model -> /index semantic-clear",
14936
+ index: "symbol index -> /index clear",
14937
+ history: "conversation history",
14938
+ logs: "crash logs",
14939
+ "memory-index": "chat memory index -> /memory index-clear",
14940
+ plugins: "installed package plugins"
14941
+ };
14942
+ out.push(B("Disk Usage (~/.aicli):"));
14943
+ out.push(` total: ${formatBytes(report.diskUsage.totalBytes)}`);
14944
+ for (const e of report.diskUsage.entries.slice(0, 10)) {
14945
+ const note = dirAnnotations[e.name];
14946
+ out.push(` ${e.name.padEnd(18)} ${formatBytes(e.bytes)}${note ? ` ${D(`\xB7 ${note}`)}` : ""}`);
14947
+ }
14948
+ out.push("");
14949
+ out.push(G("\u2713 Health check complete"));
14950
+ out.push("");
14951
+ return out.join("\n");
15099
14952
  }
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) });
14953
+
14954
+ // src/web/commands/_shared.ts
14955
+ function lastAssistantText(messages) {
14956
+ for (let i = messages.length - 1; i >= 0; i--) {
14957
+ const m = messages[i];
14958
+ if (!m || m.role !== "assistant") continue;
14959
+ const c = m.content;
14960
+ if (typeof c === "string") return c;
14961
+ if (Array.isArray(c)) {
14962
+ const text = c.filter((p) => p && p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
14963
+ if (text) return text;
15138
14964
  }
14965
+ return void 0;
15139
14966
  }
14967
+ return void 0;
15140
14968
  }
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." });
14969
+
14970
+ // src/web/commands/session-commands.ts
14971
+ async function handleClear(_args, ctx) {
14972
+ ctx.saveIfNeeded();
14973
+ ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
14974
+ ctx.resetWebSessionUsage();
14975
+ ctx.send({ type: "info", message: "Conversation cleared." });
14976
+ ctx.sendStatus();
14977
+ ctx.sendSessionList();
14978
+ }
14979
+ async function handleCompact(args, ctx) {
14980
+ await ctx.compactSession(args.join(" ") || void 0);
14981
+ }
14982
+ async function handleStatus(_args, ctx) {
14983
+ const session = ctx.sessions.current;
14984
+ const cacheRead = ctx.sessionTokenUsage.cacheReadTokens;
14985
+ const cacheCreate = ctx.sessionTokenUsage.cacheCreationTokens;
14986
+ const cost = computeCost(ctx.currentProvider, ctx.currentModel, ctx.sessionTokenUsage);
14987
+ const cacheLine = cacheRead > 0 || cacheCreate > 0 ? `
14988
+ Cache: write=${cacheCreate} read=${cacheRead}` : "";
14989
+ const costLine = cost != null ? `
14990
+ Cost: ${formatCost(cost)}` : "";
14991
+ ctx.send({
14992
+ type: "info",
14993
+ message: `Provider: ${ctx.currentProvider}
14994
+ Model: ${ctx.currentModel}
14995
+ Session: ${session?.id ?? "none"} (${session?.messages.length ?? 0} messages)
14996
+ Tokens: in=${ctx.sessionTokenUsage.inputTokens} out=${ctx.sessionTokenUsage.outputTokens}${cacheLine}${costLine}`
14997
+ });
14998
+ }
14999
+ async function handleSession(args, ctx) {
15000
+ const sub = args[0];
15001
+ if (sub === "new") {
15002
+ ctx.saveIfNeeded();
15003
+ const created = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
15004
+ ctx.unsavedSessions.set(created.id, created);
15005
+ ctx.resetWebSessionUsage();
15006
+ ctx.send({ type: "info", message: "New session created." });
15007
+ ctx.sendStatus();
15008
+ ctx.sendSessionList();
15009
+ return;
15010
+ }
15011
+ if (sub === "load" && args[1]) {
15012
+ const targetId = args[1];
15013
+ ctx.saveIfNeeded();
15014
+ const cachedExact = ctx.unsavedSessions.get(targetId);
15015
+ const cached = cachedExact ?? [...ctx.unsavedSessions.values()].find((s) => s.id.startsWith(targetId));
15016
+ if (cached) {
15017
+ ctx.sessions.setCurrent(cached);
15018
+ ctx.resetWebSessionUsage();
15019
+ ctx.send({
15020
+ type: "info",
15021
+ message: `Loaded session: ${cached.id.slice(0, 8)} "${cached.title ?? ""}" (${cached.messages.length} messages)`
15022
+ });
15023
+ ctx.sendSessionMessages();
15024
+ ctx.sendStatus();
15025
+ ctx.sendSessionList();
15026
+ return;
15145
15027
  }
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}` });
15028
+ const list = ctx.sessions.listSessions();
15029
+ const found = list.find((s) => s.id.startsWith(targetId));
15030
+ if (found) {
15031
+ ctx.sessions.loadSession(found.id);
15032
+ ctx.resetWebSessionUsage();
15033
+ ctx.send({ type: "info", message: `Loaded session: ${found.id.slice(0, 8)} "${found.title ?? ""}" (${found.messageCount} messages)` });
15034
+ const loadedSession = ctx.sessions.current;
15035
+ if (loadedSession && loadedSession.messages.length > 0) {
15036
+ const lastMsg = loadedSession.messages[loadedSession.messages.length - 1];
15037
+ const isIncomplete = lastMsg.role === "tool" || lastMsg.role === "assistant" && lastMsg.toolCalls && lastMsg.toolCalls.length > 0;
15038
+ if (isIncomplete) {
15039
+ ctx.send({
15040
+ type: "info",
15041
+ 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.'
15042
+ });
15043
+ }
15154
15044
  }
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}`);
15045
+ ctx.sendSessionMessages();
15046
+ ctx.sendStatus();
15047
+ ctx.sendSessionList();
15048
+ return;
15165
15049
  }
15166
- lines.push("");
15167
- lines.push("Use /mcp reconnect to reconnect.");
15168
- ctx.send({ type: "info", message: lines.join("\n") });
15050
+ const recreated = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
15051
+ ctx.unsavedSessions.set(recreated.id, recreated);
15052
+ ctx.resetWebSessionUsage();
15053
+ ctx.send({
15054
+ type: "info",
15055
+ message: `Previous session (${targetId.slice(0, 8)}) is no longer available \u2014 started a new one.`
15056
+ });
15057
+ ctx.sendStatus();
15058
+ ctx.sendSessionList();
15059
+ return;
15169
15060
  }
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")}` });
15061
+ if (sub === "list") {
15062
+ ctx.sendSessionList();
15063
+ return;
15064
+ }
15065
+ if (sub === "delete" && args[1]) {
15066
+ const targetId = args[1];
15067
+ const cachedKey = ctx.unsavedSessions.has(targetId) ? targetId : [...ctx.unsavedSessions.keys()].find((k) => k.startsWith(targetId));
15068
+ if (cachedKey) {
15069
+ ctx.unsavedSessions.delete(cachedKey);
15070
+ if (ctx.sessions.current?.id === cachedKey) {
15071
+ ctx.sessions.clearCurrent();
15190
15072
  }
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();
15073
+ ctx.send({ type: "info", message: `Deleted session: ${cachedKey.slice(0, 8)}` });
15074
+ ctx.sendSessionList();
15075
+ return;
15076
+ }
15077
+ const list = ctx.sessions.listSessions();
15078
+ const found = list.find((s) => s.id.startsWith(targetId));
15079
+ if (found) {
15080
+ ctx.sessions.deleteSession(found.id);
15081
+ ctx.send({ type: "info", message: `Deleted session: ${found.id.slice(0, 8)}` });
15082
+ ctx.sendSessionList();
15200
15083
  } 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
- }
15084
+ ctx.send({ type: "error", message: `Session not found: ${targetId}` });
15211
15085
  }
15086
+ return;
15212
15087
  }
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.` });
15088
+ if (sub === "batch-delete") {
15089
+ const ids = args.slice(1).filter(Boolean);
15090
+ if (ids.length === 0) {
15091
+ ctx.send({ type: "error", message: "No session IDs provided." });
15092
+ return;
15221
15093
  }
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") });
15094
+ const list = ctx.sessions.listSessions();
15095
+ let deleted = 0;
15096
+ for (const targetId of ids) {
15097
+ const cachedKey = ctx.unsavedSessions.has(targetId) ? targetId : [...ctx.unsavedSessions.keys()].find((k) => k.startsWith(targetId));
15098
+ if (cachedKey) {
15099
+ ctx.unsavedSessions.delete(cachedKey);
15100
+ deleted++;
15101
+ continue;
15102
+ }
15103
+ const found = list.find((s) => s.id.startsWith(targetId));
15104
+ if (found) {
15105
+ ctx.sessions.deleteSession(found.id);
15106
+ deleted++;
15234
15107
  }
15235
- } catch {
15236
- ctx.send({ type: "info", message: `Cannot read: ${commandsDir}` });
15237
15108
  }
15109
+ ctx.send({ type: "info", message: `Deleted ${deleted} session(s).` });
15110
+ ctx.sendSessionList();
15111
+ return;
15238
15112
  }
15113
+ if (sub === "rename" && args[1] && args.length >= 3) {
15114
+ const targetId = args[1];
15115
+ const newTitle = args.slice(2).join(" ").trim();
15116
+ if (!newTitle) {
15117
+ ctx.send({ type: "error", message: "Title cannot be empty." });
15118
+ return;
15119
+ }
15120
+ const cachedKey = ctx.unsavedSessions.has(targetId) ? targetId : [...ctx.unsavedSessions.keys()].find((k) => k.startsWith(targetId));
15121
+ if (cachedKey) {
15122
+ const session = ctx.unsavedSessions.get(cachedKey);
15123
+ session.title = newTitle;
15124
+ ctx.send({ type: "info", message: `Renamed session: "${newTitle}"` });
15125
+ ctx.sendSessionList();
15126
+ return;
15127
+ }
15128
+ const list = ctx.sessions.listSessions();
15129
+ const found = list.find((s) => s.id.startsWith(targetId));
15130
+ if (found) {
15131
+ const session = ctx.sessions.loadSession(found.id);
15132
+ session.title = newTitle;
15133
+ ctx.sessions.save();
15134
+ ctx.send({ type: "info", message: `Renamed session: "${newTitle}"` });
15135
+ ctx.sendSessionList();
15136
+ } else {
15137
+ ctx.send({ type: "error", message: `Session not found: ${targetId}` });
15138
+ }
15139
+ return;
15140
+ }
15141
+ ctx.send({ type: "info", message: "Usage: /session new | list | load <id> | delete <id> | rename <id> <title>" });
15239
15142
  }
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();
15143
+ async function handleExport(args, ctx) {
15144
+ const format = args[0] === "json" ? "json" : "md";
15145
+ ctx.exportConversation(format);
15146
+ }
15147
+ async function handleCost(_args, ctx) {
15148
+ const u = ctx.sessionTokenUsage;
15149
+ const total = u.inputTokens + u.outputTokens + u.cacheCreationTokens + u.cacheReadTokens;
15150
+ const cost = computeCost(ctx.currentProvider, ctx.currentModel, u);
15151
+ let savings = null;
15152
+ if (cost != null && u.cacheReadTokens > 0) {
15153
+ const withoutCache = computeCost(ctx.currentProvider, ctx.currentModel, {
15154
+ inputTokens: u.inputTokens + u.cacheReadTokens,
15155
+ outputTokens: u.outputTokens,
15156
+ cacheCreationTokens: u.cacheCreationTokens,
15157
+ cacheReadTokens: 0
15158
+ });
15159
+ if (withoutCache != null) savings = withoutCache - cost;
15160
+ }
15161
+ const lines = [
15162
+ "\u{1F4B0} Session Cost & Token Usage",
15163
+ ` Provider : ${ctx.currentProvider}`,
15164
+ ` Model : ${ctx.currentModel}`,
15165
+ ` Input (uncached): ${u.inputTokens.toLocaleString()}`,
15166
+ ` Output : ${u.outputTokens.toLocaleString()}`
15167
+ ];
15168
+ if (u.cacheCreationTokens > 0) lines.push(` Cache write : ${u.cacheCreationTokens.toLocaleString()}`);
15169
+ if (u.cacheReadTokens > 0) {
15170
+ const pct = Math.round(u.cacheReadTokens / (u.cacheReadTokens + u.inputTokens) * 100);
15171
+ lines.push(` Cache read : ${u.cacheReadTokens.toLocaleString()} (${pct}% hit rate)`);
15172
+ }
15173
+ lines.push(` Total tokens : ${total.toLocaleString()}`);
15174
+ if (cost != null) {
15175
+ lines.push(` Cost : ${formatCost(cost)}`);
15176
+ if (savings != null && savings > 0) {
15177
+ lines.push(` Cache savings : -${formatCost(savings)} (vs no cache)`);
15178
+ }
15179
+ } else {
15180
+ lines.push(` Cost : \u2014 (pricing unknown for this model)`);
15181
+ }
15182
+ ctx.send({ type: "info", message: lines.join("\n") });
15312
15183
  }
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." });
15184
+ async function handleCheckpoint(args, ctx) {
15185
+ const session = ctx.sessions.current;
15186
+ if (!session) {
15187
+ ctx.send({ type: "error", message: "No active session." });
15188
+ return;
15189
+ }
15190
+ const sub = args[0]?.toLowerCase();
15191
+ if (!sub || sub === "list") {
15192
+ const cps = session.checkpoints;
15193
+ if (cps.length === 0) {
15194
+ ctx.send({ type: "info", message: "No checkpoints. Use /checkpoint save <name> to create one." });
15195
+ } else {
15196
+ const lines = [`\u{1F4CC} Checkpoints (${cps.length}):`, ""];
15197
+ for (const cp of cps) {
15198
+ const ts = cp.timestamp.toLocaleString();
15199
+ lines.push(` ${cp.name.padEnd(20)} msg #${cp.messageIndex} ${ts}`);
15200
+ }
15201
+ ctx.send({ type: "info", message: lines.join("\n") });
15320
15202
  }
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." });
15203
+ return;
15204
+ }
15205
+ const cpName = args.slice(1).join(" ").trim();
15206
+ if (sub === "save") {
15207
+ if (!cpName) {
15208
+ ctx.send({ type: "error", message: "Usage: /checkpoint save <name>" });
15209
+ return;
15328
15210
  }
15329
- if (!diff) {
15330
- ctx.send({ type: "info", message: "No changes to review." + (staged ? "" : " Try --staged for staged changes.") });
15211
+ session.createCheckpoint(cpName);
15212
+ ctx.send({ type: "info", message: `\u2713 Checkpoint "${cpName}" saved at message #${session.messages.length}` });
15213
+ } else if (sub === "restore") {
15214
+ if (!cpName) {
15215
+ ctx.send({ type: "error", message: "Usage: /checkpoint restore <name>" });
15216
+ return;
15217
+ }
15218
+ const ok = session.restoreCheckpoint(cpName);
15219
+ if (ok) {
15220
+ ctx.send({ type: "info", message: `\u2713 Restored to checkpoint "${cpName}" (${session.messages.length} messages)` });
15221
+ ctx.sendSessionMessages();
15222
+ ctx.sendStatus();
15223
+ } else {
15224
+ ctx.send({ type: "error", message: `Checkpoint "${cpName}" not found.` });
15331
15225
  }
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;
15226
+ } else if (sub === "delete") {
15227
+ if (!cpName) {
15228
+ ctx.send({ type: "error", message: "Usage: /checkpoint delete <name>" });
15229
+ return;
15339
15230
  }
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}` });
15231
+ const ok = session.deleteCheckpoint(cpName);
15232
+ if (ok) {
15233
+ ctx.send({ type: "info", message: `\u2713 Deleted checkpoint "${cpName}"` });
15234
+ } else {
15235
+ ctx.send({ type: "error", message: `Checkpoint "${cpName}" not found.` });
15348
15236
  }
15237
+ } else {
15238
+ ctx.send({ type: "error", message: `Unknown subcommand: ${sub}. Use save/restore/list/delete.` });
15349
15239
  }
15350
15240
  }
15351
- async function handleSecurityReview(args, ctx) {
15352
- {
15353
- const gitCtx = getGitContext();
15354
- if (!gitCtx) {
15355
- ctx.send({ type: "error", message: "Not a git repository." });
15241
+ async function handleBranch(args, ctx) {
15242
+ const session = ctx.sessions.current;
15243
+ if (!session) {
15244
+ ctx.send({ type: "error", message: "No active session." });
15245
+ return;
15246
+ }
15247
+ const sub = args[0]?.toLowerCase();
15248
+ const resolve14 = (ref) => {
15249
+ const r = session.resolveBranchRef(ref);
15250
+ if (r.ok) return r.id;
15251
+ if (r.reason === "ambiguous") {
15252
+ const list = r.matches.map((m) => `${m.id} (${m.title})`).join(", ");
15253
+ ctx.send({ type: "error", message: `Ambiguous branch reference "${ref}". Matches: ${list}. Use the 6-char id.` });
15254
+ } else {
15255
+ ctx.send({ type: "error", message: `Branch "${ref}" not found. Use /branch list to see all branches.` });
15356
15256
  }
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." });
15257
+ return null;
15258
+ };
15259
+ if (!sub || sub === "list") {
15260
+ const branches = session.listBranches();
15261
+ const lines = [`\u{1F33F} Branches (${branches.length}):`, ""];
15262
+ for (const b of branches) {
15263
+ const marker = b.id === session.activeBranchId ? "\u25CF " : " ";
15264
+ const count = b.id === session.activeBranchId ? session.messages.length : session.getBranchMessages(b.id)?.length ?? 0;
15265
+ const parent = b.parentBranchId ? ` \u2190 ${b.parentBranchId}@${b.parentMessageIndex}` : "";
15266
+ lines.push(` ${marker}${b.id.padEnd(10)} ${b.title.padEnd(20)} (${count} msgs)${parent}`);
15363
15267
  }
15364
- if (!secDiff) {
15365
- ctx.send({ type: "info", message: "No changes to review." + (secStaged ? "" : " Try --staged for staged changes.") });
15268
+ lines.push("");
15269
+ 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>");
15270
+ ctx.send({ type: "info", message: lines.join("\n") });
15271
+ return;
15272
+ }
15273
+ if (sub === "new") {
15274
+ const idxArg = args[1];
15275
+ if (!idxArg) {
15276
+ ctx.send({ type: "error", message: "Usage: /branch new <msgIndex> [title]" });
15277
+ return;
15366
15278
  }
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;
15279
+ const fromIndex = parseInt(idxArg, 10);
15280
+ if (isNaN(fromIndex) || fromIndex < 0 || fromIndex > session.messages.length) {
15281
+ ctx.send({ type: "error", message: `Invalid msgIndex: ${idxArg}. Range: 0-${session.messages.length}` });
15282
+ return;
15374
15283
  }
15375
- const secPrompt = buildSecurityReviewPrompt(secDiff, formatGitContextForPrompt(gitCtx));
15376
- ctx.send({ type: "info", message: "\u{1F512} Scanning for security vulnerabilities..." });
15284
+ const title = args.slice(2).join(" ").trim() || void 0;
15377
15285
  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 });
15286
+ const newId = session.createBranch(fromIndex, title);
15287
+ await ctx.sessions.save();
15288
+ ctx.send({ type: "info", message: `\u2713 Created branch "${newId}" from message #${fromIndex}. Now active (${session.messages.length} messages).` });
15289
+ ctx.sendSessionMessages();
15290
+ ctx.sendStatus();
15381
15291
  } catch (err) {
15382
- ctx.send({ type: "error", message: `Security review failed: ${err.message}` });
15292
+ ctx.send({ type: "error", message: err.message });
15383
15293
  }
15294
+ return;
15384
15295
  }
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}` });
15296
+ if (sub === "switch") {
15297
+ const ref = args[1];
15298
+ if (!ref) {
15299
+ ctx.send({ type: "error", message: "Usage: /branch switch <id|title>" });
15300
+ return;
15301
+ }
15302
+ const id = resolve14(ref);
15303
+ if (!id) return;
15304
+ const ok = session.switchBranch(id);
15305
+ if (ok) {
15306
+ await ctx.sessions.save();
15307
+ const b = session.getActiveBranch();
15308
+ ctx.send({ type: "info", message: `\u2713 Switched to branch "${b.id}" \u2014 ${b.title} (${session.messages.length} messages)` });
15309
+ ctx.sendSessionMessages();
15310
+ ctx.sendStatus();
15311
+ } else {
15312
+ ctx.send({ type: "error", message: `Cannot switch to "${id}" (already active).` });
15401
15313
  }
15314
+ return;
15402
15315
  }
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." });
15316
+ if (sub === "delete") {
15317
+ const ref = args[1];
15318
+ if (!ref) {
15319
+ ctx.send({ type: "error", message: "Usage: /branch delete <id|title>" });
15320
+ return;
15409
15321
  }
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
- }
15322
+ const id = resolve14(ref);
15323
+ if (!id) return;
15324
+ const ok = session.deleteBranch(id);
15325
+ if (ok) {
15326
+ await ctx.sessions.save();
15327
+ ctx.send({ type: "info", message: `\u2713 Deleted branch "${id}"` });
15328
+ ctx.sendStatus();
15329
+ } else {
15330
+ ctx.send({ type: "error", message: `Cannot delete "${id}" (active, or last remaining branch).` });
15415
15331
  }
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
- }
15332
+ return;
15333
+ }
15334
+ if (sub === "rename") {
15335
+ const ref = args[1];
15336
+ const title = args.slice(2).join(" ").trim();
15337
+ if (!ref || !title) {
15338
+ ctx.send({ type: "error", message: "Usage: /branch rename <id|title> <new title>" });
15339
+ return;
15340
+ }
15341
+ const id = resolve14(ref);
15342
+ if (!id) return;
15343
+ const ok = session.renameBranch(id, title);
15344
+ if (ok) {
15345
+ await ctx.sessions.save();
15346
+ ctx.send({ type: "info", message: `\u2713 Renamed branch "${id}" \u2192 "${title}"` });
15347
+ ctx.sendStatus();
15348
+ } else {
15349
+ ctx.send({ type: "error", message: `Branch "${id}" not found.` });
15350
+ }
15351
+ return;
15352
+ }
15353
+ if (sub === "diff") {
15354
+ const ref = args[1];
15355
+ if (!ref) {
15356
+ ctx.send({ type: "error", message: "Usage: /branch diff <id|title>" });
15357
+ return;
15358
+ }
15359
+ const id = resolve14(ref);
15360
+ if (!id) return;
15361
+ const d = session.diffBranches(id);
15362
+ if (!d) {
15363
+ ctx.send({ type: "error", message: `Branch "${id}" not found.` });
15364
+ return;
15365
+ }
15366
+ const preview = (m) => {
15367
+ const text = typeof m.content === "string" ? m.content : "[complex content]";
15368
+ return text.replace(/\s+/g, " ").trim().slice(0, 70);
15369
+ };
15370
+ const lines = [
15371
+ `\u{1F500} diff ${id} (source) \u2194 ${session.activeBranchId} (active)`,
15372
+ `Common prefix: ${d.commonPrefix} message(s)`
15373
+ ];
15374
+ if (d.sourceOnly.length === 0 && d.targetOnly.length === 0) {
15375
+ lines.push("Branches are identical.");
15376
+ } else {
15377
+ if (d.sourceOnly.length > 0) {
15378
+ lines.push("", `Only in ${id} (${d.sourceOnly.length}):`);
15379
+ d.sourceOnly.forEach(
15380
+ (m, i) => lines.push(` [${d.commonPrefix + i}] ${m.role}: ${preview(m)}`)
15381
+ );
15382
+ }
15383
+ if (d.targetOnly.length > 0) {
15384
+ lines.push("", `Only in ${session.activeBranchId} (active) (${d.targetOnly.length}):`);
15385
+ d.targetOnly.forEach(
15386
+ (m, i) => lines.push(` [${d.commonPrefix + i}] ${m.role}: ${preview(m)}`)
15387
+ );
15450
15388
  }
15389
+ lines.push("", `Use /branch cherry-pick ${id} <index> to copy a message into the active branch.`);
15451
15390
  }
15452
- diffLines2.push(`
15453
- **Summary:** ${fileMap.size} file(s) \u2014 ${newFiles} new, ${modifiedFiles} modified`);
15454
- ctx.send({ type: "info", message: diffLines2.join("\n") });
15391
+ ctx.send({ type: "info", message: lines.join("\n") });
15392
+ return;
15455
15393
  }
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.` });
15394
+ if (sub === "cherry-pick") {
15395
+ const ref = args[1];
15396
+ const idxStr = args[2];
15397
+ if (!ref || !idxStr) {
15398
+ ctx.send({ type: "error", message: "Usage: /branch cherry-pick <source-id|title> <msg-index>" });
15399
+ return;
15465
15400
  }
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}` });
15401
+ const id = resolve14(ref);
15402
+ if (!id) return;
15403
+ const idx = parseInt(idxStr, 10);
15404
+ if (Number.isNaN(idx)) {
15405
+ ctx.send({ type: "error", message: `Invalid message index: "${idxStr}"` });
15406
+ return;
15476
15407
  }
15408
+ const picked = session.cherryPickMessage(id, idx);
15409
+ if (!picked) {
15410
+ ctx.send({ type: "error", message: `Cherry-pick failed \u2014 index ${idx} out of range for branch "${id}".` });
15411
+ return;
15412
+ }
15413
+ await ctx.sessions.save();
15414
+ const pickPreview = typeof picked.content === "string" ? picked.content.replace(/\s+/g, " ").trim().slice(0, 60) : "[complex content]";
15415
+ ctx.send({ type: "info", message: `\u2713 Cherry-picked ${picked.role} message from "${id}"[${idx}] \u2192 active branch (${session.messages.length} msgs): ${pickPreview}` });
15416
+ ctx.sendSessionMessages();
15417
+ ctx.sendStatus();
15418
+ return;
15477
15419
  }
15420
+ ctx.send({ type: "error", message: `Unknown subcommand: ${sub}. Use list/new/switch/delete/rename/diff/cherry-pick.` });
15478
15421
  }
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>" });
15422
+ async function handleFork(args, ctx) {
15423
+ const session = ctx.sessions.current;
15424
+ if (!session) {
15425
+ ctx.send({ type: "info", message: "No active session to fork." });
15426
+ return;
15427
+ }
15428
+ const sub = args.join(" ").trim();
15429
+ let messageCount = session.messages.length;
15430
+ let fromLabel = "current position";
15431
+ if (sub) {
15432
+ const cp = session.checkpoints.find((c) => c.name === sub);
15433
+ if (!cp) {
15434
+ const available = session.checkpoints.map((c) => c.name);
15435
+ ctx.send({ type: "error", message: available.length > 0 ? `Checkpoint "${sub}" not found. Available: ${available.join(", ")}` : `Checkpoint "${sub}" not found. No checkpoints saved.` });
15436
+ return;
15484
15437
  }
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);
15438
+ messageCount = cp.messageIndex;
15439
+ fromLabel = `checkpoint "${cp.name}" (${cp.messageIndex} messages)`;
15440
+ }
15441
+ try {
15442
+ const originalId = session.id.slice(0, 8);
15443
+ const forked = await ctx.sessions.forkSession(messageCount);
15444
+ ctx.send({ type: "info", message: `\u{1F500} Session Forked
15445
+ Original: ${originalId}
15446
+ Forked: ${forked.id.slice(0, 8)} "${forked.title ?? "(untitled)"}"
15447
+ From: ${fromLabel}
15448
+ Messages: ${forked.messages.length} copied` });
15449
+ ctx.sendSessionMessages();
15450
+ ctx.sendStatus();
15451
+ ctx.sendSessionList();
15452
+ } catch (err) {
15453
+ ctx.send({ type: "error", message: `Fork failed: ${err.message}` });
15490
15454
  }
15491
15455
  }
15456
+ async function handleRewind(args, ctx) {
15457
+ const session = ctx.sessions.current;
15458
+ if (!session || session.messages.length === 0) {
15459
+ ctx.send({ type: "info", message: "No messages to rewind." });
15460
+ return;
15461
+ }
15462
+ const rewindSub = args[0];
15463
+ if (rewindSub === "list" || !rewindSub) {
15464
+ const lines = [`Conversation messages (${session.messages.length} total):
15465
+ `];
15466
+ const cpIndices = (await import("./file-checkpoint-CGH6OJVI.js")).fileCheckpoints.getMessageIndices();
15467
+ for (let i = 0; i < session.messages.length; i++) {
15468
+ const m = session.messages[i];
15469
+ const text = getContentText(m.content).replace(/\n/g, " ").slice(0, 60);
15470
+ const pin = cpIndices.includes(i) ? " \u{1F4CC}" : "";
15471
+ lines.push(` [${i + 1}] ${m.role.padEnd(10)} ${text}${pin}`);
15472
+ }
15473
+ lines.push("", "Usage: /rewind <n> \u2014 rewind to message N", "\u{1F4CC} = file checkpoint");
15474
+ ctx.send({ type: "info", message: lines.join("\n") });
15475
+ return;
15476
+ }
15477
+ const rewindN = parseInt(rewindSub, 10);
15478
+ if (isNaN(rewindN) || rewindN < 1 || rewindN > session.messages.length) {
15479
+ ctx.send({ type: "error", message: `Invalid message number: ${rewindSub}. Range: 1-${session.messages.length}` });
15480
+ return;
15481
+ }
15482
+ const { fileCheckpoints: fc } = await import("./file-checkpoint-CGH6OJVI.js");
15483
+ const rewindRemoved = session.messages.length - rewindN;
15484
+ const rewindResult = fc.restoreToMessageIndex(rewindN);
15485
+ session.messages = session.messages.slice(0, rewindN);
15486
+ session.checkpoints = session.checkpoints.filter((c) => c.messageIndex <= rewindN);
15487
+ session.updated = /* @__PURE__ */ new Date();
15488
+ ctx.sessions.save();
15489
+ const rewindLines = [`\u2713 Rewound to message ${rewindN}`, ` Messages removed: ${rewindRemoved}`];
15490
+ if (rewindResult.restored > 0 || rewindResult.deleted > 0) {
15491
+ rewindLines.push(` Files restored: ${rewindResult.restored}, deleted: ${rewindResult.deleted}`);
15492
+ for (const f of rewindResult.files) rewindLines.push(` ${f}`);
15493
+ } else {
15494
+ rewindLines.push(" No file changes to revert.");
15495
+ }
15496
+ ctx.send({ type: "info", message: rewindLines.join("\n") });
15497
+ ctx.sendSessionMessages();
15498
+ ctx.sendStatus();
15499
+ }
15492
15500
 
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
- }
15501
+ // src/web/commands/tooling-commands.ts
15502
+ async function handleTools(_args, ctx) {
15503
+ ctx.sendToolsList();
15504
+ }
15505
+ async function handlePlugins(_args, ctx) {
15506
+ const configDir = ctx.config.getConfigDir();
15507
+ const pluginsDir = join24(configDir, PLUGINS_DIR_NAME);
15508
+ const pluginTools = ctx.toolRegistry.listPluginTools();
15509
+ const pluginPackages = listInstalledPlugins(configDir);
15510
+ const lines = [`\u{1F50C} **Plugins:**`, `Dir: ${pluginsDir}`, ""];
15511
+ if (pluginTools.length === 0 && pluginPackages.length === 0) {
15512
+ lines.push("No plugins loaded.");
15513
+ lines.push("Use /plugin install <path>, /plugin trust <name>, then /plugin enable <name>.");
15514
+ } else {
15515
+ if (pluginPackages.length > 0) {
15516
+ lines.push(`Package plugins (${pluginPackages.length}):`);
15517
+ for (const p of pluginPackages) lines.push(` \u2022 ${p.name}@${p.manifest.version} \u2014 ${p.enabled ? "enabled" : "disabled"} / ${p.trusted ? "trusted" : "untrusted"}`);
15518
+ lines.push("");
15509
15519
  }
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).` });
15520
+ if (pluginTools.length > 0) {
15521
+ lines.push(`Legacy JS plugin tools (${pluginTools.length}):`);
15522
+ for (const t of pluginTools) lines.push(` \u2022 ${t.definition.name} \u2014 ${t.definition.description}`);
15516
15523
  }
15517
15524
  }
15525
+ ctx.send({ type: "info", message: lines.join("\n") });
15518
15526
  }
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");
15527
+ async function handlePlugin(args, ctx) {
15528
+ const configDir = ctx.config.getConfigDir();
15529
+ const sub = args[0] ?? "list";
15530
+ try {
15531
+ if (sub === "list") {
15532
+ const plugins = listInstalledPlugins(configDir);
15533
+ const lines = [`\u{1F50C} **Package Plugins**`, `Dir: ${pluginRoot(configDir)}`, ""];
15534
+ if (plugins.length === 0) lines.push("No package plugins installed.");
15535
+ for (const p of plugins) lines.push(` \u2022 ${p.name}@${p.manifest.version} \u2014 ${p.enabled ? "enabled" : "disabled"} / ${p.trusted ? "trusted" : "untrusted"}`);
15536
+ ctx.send({ type: "info", message: lines.join("\n") });
15537
+ return;
15537
15538
  }
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
- }
15539
+ if (sub === "install") {
15540
+ const input = args[1];
15541
+ if (!input) throw new Error("Usage: /plugin install <path>");
15542
+ const plugin = installPlugin(configDir, input);
15543
+ ctx.send({ type: "info", message: `Installed plugin ${plugin.name}@${plugin.manifest.version}. Run /plugin trust ${plugin.name}, then /plugin enable ${plugin.name}.` });
15544
+ return;
15544
15545
  }
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") });
15546
+ if (sub === "trust") {
15547
+ const name = args[1];
15548
+ if (!name) throw new Error("Usage: /plugin trust <name>");
15549
+ const state2 = trustPlugin(configDir, name);
15550
+ ctx.send({ type: "info", message: `Trusted plugin ${state2.name}.` });
15551
+ return;
15552
+ }
15553
+ if (sub === "enable" || sub === "disable") {
15554
+ const name = args[1];
15555
+ if (!name) throw new Error(`Usage: /plugin ${sub} <name>`);
15556
+ const state2 = setPluginEnabled(configDir, name, sub === "enable");
15557
+ ctx.send({ type: "info", message: `${sub === "enable" ? "Enabled" : "Disabled"} plugin ${state2.name}. Restart Web UI for changes to take effect.` });
15558
+ return;
15559
+ }
15560
+ if (sub === "inspect") {
15561
+ const name = args[1];
15562
+ if (!name) throw new Error("Usage: /plugin inspect <name>");
15563
+ ctx.send({ type: "info", message: describePlugin(getInstalledPlugin(configDir, name)).join("\n") });
15564
+ return;
15565
+ }
15566
+ throw new Error("Usage: /plugin [list|install|enable|disable|inspect|trust]");
15567
+ } catch (err) {
15568
+ ctx.send({ type: "error", message: err instanceof Error ? err.message : String(err) });
15549
15569
  }
15550
15570
  }
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)" });
15571
+ async function handleMcp(_args, ctx) {
15572
+ if (!ctx.mcpManager) {
15573
+ ctx.send({ type: "info", message: "MCP not configured. Add mcpServers to config.json." });
15574
+ return;
15575
+ }
15576
+ const sub = _args[0];
15577
+ if (sub === "reconnect") {
15578
+ ctx.send({ type: "info", message: "\u{1F504} Reconnecting MCP servers..." });
15579
+ try {
15580
+ await ctx.mcpManager.reconnectAll();
15581
+ ctx.send({ type: "info", message: "\u2713 MCP reconnect complete." });
15582
+ } catch (err) {
15583
+ ctx.send({ type: "error", message: `Reconnect failed: ${err.message}` });
15584
+ }
15585
+ ctx.sendToolsList();
15586
+ return;
15587
+ }
15588
+ const statuses = ctx.mcpManager.getStatus();
15589
+ if (statuses.length === 0) {
15590
+ ctx.send({ type: "info", message: "No MCP servers configured." });
15591
+ return;
15592
+ }
15593
+ const lines = [`\u{1F50C} **MCP Servers (${statuses.length}):**`, ""];
15594
+ for (const s of statuses) {
15595
+ const state2 = s.connected ? `\u2713 connected \xB7 ${s.serverName} \xB7 ${s.toolCount} tools` : `\u2717 disconnected${s.error ? ` \xB7 ${s.error}` : ""}`;
15596
+ lines.push(` ${s.serverId}: ${state2}`);
15597
+ }
15598
+ lines.push("");
15599
+ lines.push("Use /mcp reconnect to reconnect.");
15600
+ ctx.send({ type: "info", message: lines.join("\n") });
15601
+ }
15602
+ async function handleSkill(args, ctx) {
15603
+ const sub = args[0];
15604
+ if (!ctx.skillManager) {
15605
+ ctx.send({ type: "error", message: "Skill system not available." });
15606
+ return;
15607
+ }
15608
+ if (!sub || sub === "list") {
15609
+ const skills = ctx.skillManager.listSkills();
15610
+ const active = ctx.skillManager.getActive();
15611
+ if (skills.length === 0) {
15612
+ ctx.send({ type: "info", message: "No skills available. Add .md files to ~/.aicli/skills/" });
15618
15613
  } else {
15619
- ctx.send({
15620
- type: "error",
15621
- message: `Unknown subcommand: ${sub}. Use status/rebuild/clear/semantic-rebuild/semantic-clear.`
15614
+ const lines = skills.map((s) => {
15615
+ const isActive = active?.meta.name === s.meta.name;
15616
+ const marker = isActive ? " \u2705 (active)" : "";
15617
+ return ` ${s.meta.name}${marker} \u2014 ${s.meta.description || "(no description)"}`;
15622
15618
  });
15619
+ ctx.send({ type: "info", message: `\u{1F4DA} Skills:
15620
+ ${lines.join("\n")}` });
15623
15621
  }
15622
+ return;
15623
+ }
15624
+ if (sub === "off") {
15625
+ ctx.skillManager.deactivate();
15626
+ ctx.send({ type: "info", message: "\u{1F50C} Skill deactivated." });
15627
+ ctx.sendToolsList();
15628
+ ctx.sendStatus();
15629
+ return;
15630
+ }
15631
+ if (sub === "reload") {
15632
+ ctx.skillManager.loadSkills(getActivePluginAssets(ctx.config.getConfigDir()).skillDirs);
15633
+ ctx.send({ type: "info", message: `\u{1F504} Reloaded ${ctx.skillManager.listSkills().length} skill(s).` });
15634
+ ctx.sendToolsList();
15635
+ return;
15636
+ }
15637
+ const activated = ctx.skillManager.activate(sub);
15638
+ if (activated) {
15639
+ ctx.send({ type: "info", message: `\u2705 Skill activated: ${activated.meta.name}
15640
+ ${activated.meta.description || ""}` });
15641
+ ctx.sendToolsList();
15642
+ ctx.sendStatus();
15643
+ } else {
15644
+ const available = ctx.skillManager.listSkills().map((s) => s.meta.name).join(", ");
15645
+ ctx.send({ type: "error", message: `Skill "${sub}" not found. Available: ${available}` });
15624
15646
  }
15625
15647
  }
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();
15648
+ async function handleCommands(_args, ctx) {
15649
+ const configDir = ctx.config.getConfigDir();
15650
+ const commandsDir = join24(configDir, CUSTOM_COMMANDS_DIR_NAME);
15651
+ if (!existsSync29(commandsDir)) {
15652
+ ctx.send({ type: "info", message: `No custom commands directory.
15653
+ Create: ${commandsDir}/ with .md files.` });
15654
+ return;
15655
+ }
15656
+ try {
15657
+ const files = readdirSync13(commandsDir).filter((f) => f.endsWith(".md"));
15658
+ if (files.length === 0) {
15659
+ ctx.send({ type: "info", message: `No custom commands found in ${commandsDir}
15660
+ Add .md files to create commands.` });
15633
15661
  } else {
15634
- ctx.memoryShow();
15662
+ const lines = [`\u{1F4CB} **Custom Commands (${files.length}):**`, `Dir: ${commandsDir}`, ""];
15663
+ for (const f of files) {
15664
+ const name = f.replace(/\.md$/, "");
15665
+ lines.push(` /${name}`);
15666
+ }
15667
+ ctx.send({ type: "info", message: lines.join("\n") });
15635
15668
  }
15669
+ } catch {
15670
+ ctx.send({ type: "info", message: `Cannot read: ${commandsDir}` });
15636
15671
  }
15637
15672
  }
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
15673
 
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.` });
15674
+ // src/web/commands/review-commands.ts
15675
+ async function handleReview(args, ctx) {
15676
+ const gitCtx = getGitContext();
15677
+ if (!gitCtx) {
15678
+ ctx.send({ type: "error", message: "Not a git repository." });
15679
+ return;
15661
15680
  }
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;
15681
+ const staged = args.includes("--staged");
15682
+ const detailed = args.includes("--detailed");
15683
+ let diff;
15695
15684
  try {
15696
- names = readdirSync15(dir);
15685
+ diff = readGitDiff({ staged });
15697
15686
  } catch {
15698
- return [];
15687
+ ctx.send({ type: "error", message: "Failed to run git diff." });
15688
+ return;
15699
15689
  }
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
- }
15690
+ if (!diff) {
15691
+ ctx.send({ type: "info", message: "No changes to review." + (staged ? "" : " Try --staged for staged changes.") });
15692
+ return;
15693
+ }
15694
+ const MAX_DIFF = 8e3;
15695
+ let truncated = false;
15696
+ if (diff.length > MAX_DIFF) {
15697
+ const head = diff.slice(0, Math.floor(MAX_DIFF * 0.7));
15698
+ const tail = diff.slice(diff.length - Math.floor(MAX_DIFF * 0.2));
15699
+ diff = head + "\n\n... [diff truncated, " + diff.length + " chars total] ...\n\n" + tail;
15700
+ truncated = true;
15701
+ }
15702
+ const reviewPrompt = buildReviewPrompt(diff, formatGitContextForPrompt(gitCtx), detailed);
15703
+ ctx.send({ type: "info", message: "\u{1F50D} Analyzing changes..." });
15704
+ try {
15705
+ const review = await ctx.chatOnce(reviewPrompt, { temperature: 0.3, maxTokens: 8192 });
15706
+ const msg = truncated ? review + "\n\n\u26A0 Diff was truncated. Consider reviewing smaller changesets." : review;
15707
+ ctx.send({ type: "info", message: msg });
15708
+ } catch (err) {
15709
+ ctx.send({ type: "error", message: `Review failed: ${err.message}` });
15719
15710
  }
15720
- files.sort((a, b) => a.timestamp < b.timestamp ? 1 : -1);
15721
- return files.slice(0, limit);
15722
15711
  }
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;
15712
+ async function handleSecurityReview(args, ctx) {
15713
+ const gitCtx = getGitContext();
15714
+ if (!gitCtx) {
15715
+ ctx.send({ type: "error", message: "Not a git repository." });
15716
+ return;
15717
+ }
15718
+ const secStaged = args.includes("--staged");
15719
+ let secDiff;
15729
15720
  try {
15730
- names = readdirSync15(base);
15721
+ secDiff = readGitDiff({ staged: secStaged });
15731
15722
  } catch {
15732
- return { totalBytes: 0, entries: [] };
15723
+ ctx.send({ type: "error", message: "Failed to run git diff." });
15724
+ return;
15733
15725
  }
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;
15726
+ if (!secDiff) {
15727
+ ctx.send({ type: "info", message: "No changes to review." + (secStaged ? "" : " Try --staged for staged changes.") });
15728
+ return;
15729
+ }
15730
+ const SEC_MAX_DIFF = 8e3;
15731
+ let secTruncated = false;
15732
+ if (secDiff.length > SEC_MAX_DIFF) {
15733
+ const head = secDiff.slice(0, Math.floor(SEC_MAX_DIFF * 0.7));
15734
+ const tail = secDiff.slice(secDiff.length - Math.floor(SEC_MAX_DIFF * 0.2));
15735
+ secDiff = head + "\n\n... [diff truncated, " + secDiff.length + " chars total] ...\n\n" + tail;
15736
+ secTruncated = true;
15737
+ }
15738
+ const secPrompt = buildSecurityReviewPrompt(secDiff, formatGitContextForPrompt(gitCtx));
15739
+ ctx.send({ type: "info", message: "\u{1F512} Scanning for security vulnerabilities..." });
15740
+ try {
15741
+ const secReview = await ctx.chatOnce(secPrompt, { temperature: 0.2, maxTokens: 8192 });
15742
+ const secMsg = secTruncated ? secReview + "\n\n\u26A0 Diff was truncated. Consider reviewing smaller changesets." : secReview;
15743
+ ctx.send({ type: "info", message: secMsg });
15744
+ } catch (err) {
15745
+ ctx.send({ type: "error", message: `Security review failed: ${err.message}` });
15739
15746
  }
15740
- entries.sort((a, b) => b.bytes - a.bytes);
15741
- return { totalBytes: total, entries };
15742
15747
  }
15743
- function dirSize(path3) {
15748
+ async function handleTest(args, ctx) {
15749
+ ctx.send({ type: "info", message: "\u{1F9EA} Running tests..." });
15744
15750
  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;
15751
+ const { executeTests } = await import("./run-tests-4PNOUHC3.js");
15752
+ const argStr = args.join(" ").trim();
15753
+ let testArgs = {};
15754
+ if (argStr) {
15755
+ const isCommand = argStr.includes(" ") || /^(mvn|gradle|npm|pytest|cargo|go)\b/.test(argStr);
15756
+ testArgs = isCommand ? { command: argStr } : { filter: argStr };
15757
+ }
15758
+ const report = await executeTests(testArgs);
15759
+ ctx.send({ type: "info", message: report });
15760
+ } catch (err) {
15761
+ ctx.send({ type: "error", message: `Test failed: ${err.message}` });
15755
15762
  }
15756
15763
  }
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 {
15764
+ async function handleDiff(args, ctx) {
15765
+ const history = undoStack.getHistory();
15766
+ if (history.length === 0) {
15767
+ ctx.send({ type: "info", message: "No file modifications in this session." });
15768
+ return;
15769
+ }
15770
+ const fileMap = /* @__PURE__ */ new Map();
15771
+ for (const entry of history) {
15772
+ if (!fileMap.has(entry.filePath)) {
15773
+ fileMap.set(entry.filePath, { earliest: entry.previousContent });
15766
15774
  }
15767
15775
  }
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)
15776
+ const statsOnly = args[0]?.toLowerCase() === "--stats";
15777
+ let newFiles = 0;
15778
+ let modifiedFiles = 0;
15779
+ const diffLines2 = [];
15780
+ for (const [filePath, { earliest }] of fileMap) {
15781
+ const currentContent = existsSync29(filePath) ? (() => {
15782
+ try {
15783
+ return readFileSync22(filePath, "utf-8");
15784
+ } catch {
15785
+ return null;
15786
+ }
15787
+ })() : null;
15788
+ const isNew = earliest === null;
15789
+ if (isNew) newFiles++;
15790
+ else modifiedFiles++;
15791
+ if (statsOnly) continue;
15792
+ if (isNew) {
15793
+ diffLines2.push(`
15794
+ \u{1F4C4} [NEW] ${filePath}`);
15795
+ if (currentContent !== null) {
15796
+ diffLines2.push(` ${currentContent.split("\n").length} lines`);
15797
+ } else {
15798
+ diffLines2.push(" (file was deleted after creation)");
15799
+ }
15800
+ } else {
15801
+ diffLines2.push(`
15802
+ \u{1F4DD} [MODIFIED] ${filePath}`);
15803
+ const oldText = earliest ?? "";
15804
+ const newText = currentContent ?? "";
15805
+ if (oldText === newText) {
15806
+ diffLines2.push(" (no net change)");
15807
+ } else {
15808
+ diffLines2.push(renderDiff(oldText, newText, { filePath, maxLines: 80 }));
15809
+ }
15868
15810
  }
15869
- };
15811
+ }
15812
+ diffLines2.push(`
15813
+ **Summary:** ${fileMap.size} file(s) \u2014 ${newFiles} new, ${modifiedFiles} modified`);
15814
+ ctx.send({ type: "info", message: diffLines2.join("\n") });
15815
+ }
15816
+ async function handleInit(args, ctx) {
15817
+ const cwd = process.cwd();
15818
+ const targetPath = join24(cwd, "AICLI.md");
15819
+ const force = args.includes("--force");
15820
+ if (existsSync29(targetPath) && !force) {
15821
+ ctx.send({ type: "info", message: `AICLI.md already exists at ${targetPath}
15822
+ Use /init --force to overwrite.` });
15823
+ return;
15824
+ }
15825
+ ctx.send({ type: "info", message: "\u{1F4DD} Scanning project structure..." });
15826
+ try {
15827
+ const projectInfo = scanProject(cwd);
15828
+ const prompt = buildInitPrompt(projectInfo, cwd);
15829
+ const content = await ctx.chatOnce(prompt, { temperature: 0.3, maxTokens: 4096 });
15830
+ writeFileSync3(targetPath, content, "utf-8");
15831
+ ctx.send({ type: "info", message: `\u2713 Generated: ${targetPath} (${content.length} chars)
15832
+ Use /context reload to load it.` });
15833
+ } catch (err) {
15834
+ ctx.send({ type: "error", message: `Failed to generate AICLI.md: ${err.message}` });
15835
+ }
15870
15836
  }
15837
+ async function handleScaffold(args, ctx) {
15838
+ const description = args.join(" ").trim();
15839
+ if (!description) {
15840
+ ctx.send({ type: "error", message: "Usage: /scaffold <project description>" });
15841
+ return;
15842
+ }
15843
+ ctx.send({ type: "info", message: "\u{1F3D7}\uFE0F Generating project scaffold..." });
15844
+ const scaffoldPrompt = `Please scaffold a project based on this description: ${description}
15871
15845
 
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`;
15846
+ Create the necessary files and directory structure. Use write_file and bash tools to set up the project.`;
15847
+ await ctx.handleChat(scaffoldPrompt);
15878
15848
  }
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")}`);
15849
+
15850
+ // src/web/commands/memory-commands.ts
15851
+ async function handleSystem(args, ctx) {
15852
+ const text = args.join(" ").trim();
15853
+ if (!text) {
15854
+ const current = ctx.activeSystemPrompt;
15855
+ if (current) {
15856
+ const preview = current.length > 500 ? current.slice(0, 500) + "..." : current;
15857
+ ctx.send({ type: "info", message: `\u{1F4CB} Current system prompt (${current.length} chars):
15858
+
15859
+ ${preview}` });
15860
+ } else {
15861
+ ctx.send({ type: "info", message: "No custom system prompt set. Using default.\nUsage: /system <prompt>" });
15862
+ }
15863
+ return;
15905
15864
  }
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" : ""})`)}`);
15865
+ if (text === "clear" || text === "reset") {
15866
+ ctx.activeSystemPrompt = ctx.loadContextFiles();
15867
+ ctx.send({ type: "info", message: "\u2713 System prompt reset to default (context files)." });
15868
+ } else {
15869
+ ctx.activeSystemPrompt = text;
15870
+ ctx.send({ type: "info", message: `\u2713 System prompt set (${text.length} chars).` });
15912
15871
  }
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}` : ""}`);
15872
+ }
15873
+ async function handleContext(_args, ctx) {
15874
+ const sub = _args[0];
15875
+ if (sub === "reload") {
15876
+ ctx.activeSystemPrompt = ctx.loadContextFiles();
15877
+ ctx.send({ type: "info", message: ctx.activeSystemPrompt ? `\u2713 Context reloaded (${ctx.activeSystemPrompt.length} chars).` : "\u2713 No context files found." });
15878
+ return;
15879
+ }
15880
+ const result = ctx.contextLoadResult;
15881
+ const layers = ["\u{1F4DA} **Context Layers:**", ""];
15882
+ if (result && result.layers.length > 0) {
15883
+ const labels = { global: "Global ", project: "Project", plugin: "Plugin ", local: "Local " };
15884
+ for (const layer of result.layers) {
15885
+ layers.push(` \u2713 ${labels[layer.level] ?? layer.level}: ${layer.displayPath} (${layer.charCount} chars${layer.truncated ? ", truncated" : ""})`);
15917
15886
  }
15887
+ layers.push(` Total: ${result.totalChars} chars (${result.layers.length} layer${result.layers.length > 1 ? "s" : ""})`);
15888
+ } else {
15889
+ layers.push(" No context files loaded.");
15890
+ layers.push(" Search order per layer: AICLI.override.md \u2192 AGENTS.override.md \u2192 AICLI.md \u2192 CLAUDE.md \u2192 AGENTS.md");
15918
15891
  }
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)}`);
15892
+ if (result && result.skipped.length > 0) {
15893
+ layers.push("");
15894
+ layers.push("Skipped:");
15895
+ for (const skipped of result.skipped.slice(0, 5)) {
15896
+ layers.push(` - ${skipped.displayPath}: ${skipped.reason}${skipped.message ? ` (${skipped.message})` : ""}`);
15935
15897
  }
15936
15898
  }
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}`);
15899
+ layers.push("");
15900
+ layers.push(`Total prompt length: ${ctx.activeSystemPrompt?.length ?? 0} chars`);
15901
+ layers.push("Use /context reload to refresh.");
15902
+ ctx.send({ type: "info", message: layers.join("\n") });
15903
+ }
15904
+ async function handleIndex(args, ctx) {
15905
+ const sub = (args[0] ?? "status").toLowerCase();
15906
+ const root = process.cwd();
15907
+ const { loadIndex: loadIndex2, clearIndex } = await import("./store-VO37H6LS.js");
15908
+ const { indexProject: indexProject2 } = await import("./indexer-4WWS3VIL.js");
15909
+ const { loadVectorStore, clearVectorStore } = await import("./vector-store-JBAE6PS4.js");
15910
+ if (sub === "status") {
15911
+ const idx = loadIndex2(root);
15912
+ const vec = loadVectorStore(root);
15913
+ if (!idx) {
15914
+ ctx.send({ type: "info", message: `No symbol index for ${root}. Run /index rebuild.` });
15915
+ return;
15916
+ }
15917
+ const semantic = vec ? `${vec.count} vectors \xD7 ${vec.dim}-dim` : "not built \u2014 run /index semantic-rebuild to enable search_code";
15918
+ ctx.send({
15919
+ type: "info",
15920
+ message: [
15921
+ "\u{1F50E} Symbol index:",
15922
+ ` Root: ${idx.root}`,
15923
+ ` Generated: ${idx.generated}`,
15924
+ ` Files: ${idx.fileCount}`,
15925
+ ` Symbols: ${idx.symbolCount}`,
15926
+ ` Semantic: ${semantic}`
15927
+ ].join("\n")
15928
+ });
15929
+ } else if (sub === "rebuild") {
15930
+ ctx.send({ type: "info", message: `Indexing ${root}\u2026` });
15931
+ const { stats } = await indexProject2(root, { force: true });
15932
+ ctx.send({
15933
+ type: "info",
15934
+ message: `\u2713 Indexed ${stats.filesParsed} files (${stats.symbols} symbols) in ${stats.durationMs}ms`
15935
+ });
15936
+ } else if (sub === "clear") {
15937
+ clearIndex(root);
15938
+ clearVectorStore(root);
15939
+ ctx.send({ type: "info", message: `\u2713 Cleared symbol + semantic index for ${root}` });
15940
+ } else if (sub === "semantic-rebuild") {
15941
+ const idx = loadIndex2(root);
15942
+ if (!idx) {
15943
+ ctx.send({ type: "error", message: "No symbol index yet. Run /index rebuild first." });
15944
+ return;
15945
+ }
15946
+ if (idx.symbolCount === 0) {
15947
+ ctx.send({
15948
+ type: "error",
15949
+ 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."
15950
+ });
15951
+ return;
15952
+ }
15953
+ ctx.send({
15954
+ type: "info",
15955
+ message: `Building semantic index for ${idx.symbolCount} symbols\u2026 (first run downloads ~117 MB model)`
15956
+ });
15957
+ try {
15958
+ const { rebuildSemanticIndex } = await import("./semantic-FF6DDJI6.js");
15959
+ const stats = await rebuildSemanticIndex(root);
15960
+ const first = stats.modelFirstLoadMs ? ` (model load+first batch ${stats.modelFirstLoadMs}ms)` : "";
15961
+ ctx.send({
15962
+ type: "info",
15963
+ message: `\u2713 Embedded ${stats.symbolsEmbedded} symbols in ${stats.durationMs}ms${first}`
15964
+ });
15965
+ } catch (err) {
15966
+ const msg = err instanceof Error ? err.message : String(err);
15967
+ ctx.send({ type: "error", message: `Semantic rebuild failed: ${msg}` });
15968
+ }
15969
+ } else if (sub === "semantic-clear") {
15970
+ clearVectorStore(root);
15971
+ ctx.send({ type: "info", message: "\u2713 Cleared semantic index (symbol index preserved)" });
15972
+ } else {
15973
+ ctx.send({
15974
+ type: "error",
15975
+ message: `Unknown subcommand: ${sub}. Use status/rebuild/clear/semantic-rebuild/semantic-clear.`
15976
+ });
15944
15977
  }
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}`);
15978
+ }
15979
+ async function handleMemory(args, ctx) {
15980
+ const sub = args[0];
15981
+ if (sub === "add" && args.length > 1) {
15982
+ ctx.memoryAdd(args.slice(1).join(" "));
15983
+ } else if (sub === "clear") {
15984
+ ctx.memoryClear();
15985
+ } else {
15986
+ ctx.memoryShow();
15950
15987
  }
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)}`);
15988
+ }
15989
+ async function handleAddDir(args, ctx) {
15990
+ const sub = args[0]?.trim();
15991
+ if (!sub) {
15992
+ ctx.send({ type: "info", message: ctx.addedDirs.size > 0 ? `\u{1F4C1} Added directories:
15993
+ ${[...ctx.addedDirs].map((d) => ` \u2022 ${d}`).join("\n")}
15994
+
15995
+ Use /add-dir remove to clear.` : "No directories added.\nUsage: /add-dir <path> | /add-dir remove" });
15996
+ return;
15958
15997
  }
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`);
15998
+ if (sub === "remove" || sub === "clear") {
15999
+ ctx.addedDirs.clear();
16000
+ ctx.send({ type: "info", message: "\u2713 All added directories removed from context." });
16001
+ return;
15966
16002
  }
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));
16003
+ const dirPath = resolve10(sub);
16004
+ if (!existsSync29(dirPath)) {
16005
+ ctx.send({ type: "error", message: `Directory not found: ${dirPath}` });
16006
+ return;
15970
16007
  }
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}`)}` : ""}`);
16008
+ if (!statSync12(dirPath).isDirectory()) {
16009
+ ctx.send({ type: "error", message: `Not a directory: ${dirPath}` });
16010
+ return;
15985
16011
  }
15986
- out.push("");
15987
- out.push(G("\u2713 Health check complete"));
15988
- out.push("");
15989
- return out.join("\n");
16012
+ ctx.addedDirs.add(dirPath);
16013
+ ctx.send({ type: "info", message: `\u2713 Added directory: ${dirPath}
16014
+ It will be included in AI context for subsequent messages.` });
15990
16015
  }
15991
16016
 
15992
16017
  // src/web/commands/security-commands.ts
15993
16018
  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:
16019
+ const sub = args[0];
16020
+ if (sub === "show" || !sub) {
16021
+ ctx.send({ type: "info", message: `\u2699\uFE0F Configuration:
15998
16022
  \`\`\`json
15999
16023
  ${ctx.config.toFormattedJSON()}
16000
16024
  \`\`\`` });
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>" });
16025
+ } else if (sub === "get" && args[1]) {
16026
+ const val = ctx.config.getByPath(args[1]);
16027
+ const display = typeof val === "object" ? JSON.stringify(val, null, 2) : String(val ?? "undefined");
16028
+ ctx.send({ type: "info", message: `${args[1]} = ${display}` });
16029
+ } else if (sub === "set" && args[1] && args[2] !== void 0) {
16030
+ try {
16031
+ ctx.config.setByPath(args[1], args.slice(2).join(" "));
16032
+ ctx.send({ type: "info", message: `\u2713 ${args[1]} = ${args.slice(2).join(" ")}` });
16033
+ } catch (err) {
16034
+ ctx.send({ type: "error", message: `Config error: ${err.message}` });
16014
16035
  }
16036
+ } else {
16037
+ ctx.send({ type: "info", message: "Usage: /config [show] | /config get <key> | /config set <key> <value>" });
16015
16038
  }
16016
16039
  }
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
- }
16040
+ async function handleDoctor(_args, ctx) {
16041
+ const report = buildDoctorReport({
16042
+ config: ctx.config,
16043
+ providers: ctx.providers,
16044
+ cwd: process.cwd(),
16045
+ mcpStatuses: ctx.mcpManager?.getStatus() ?? []
16046
+ });
16047
+ ctx.send({ type: "info", message: formatDoctorReport(report, false) });
16048
+ }
16049
+ async function handleBug(_args, ctx) {
16050
+ const session = ctx.sessions.current;
16051
+ const lines = [
16052
+ "\u{1F41B} **Bug Report Template**",
16053
+ "",
16054
+ "**Environment:**",
16055
+ ` ai-cli version: ${VERSION}`,
16056
+ ` Node.js: ${process.version}`,
16057
+ ` OS: ${process.platform} ${process.arch}`,
16058
+ ` Provider: ${ctx.currentProvider}`,
16059
+ ` Model: ${ctx.currentModel}`,
16060
+ ` Session: ${session?.id.slice(0, 8) ?? "none"} (${session?.messages.length ?? 0} messages)`,
16061
+ "",
16062
+ "**Description:**",
16063
+ "(Describe the bug)",
16064
+ "",
16065
+ "**Steps to reproduce:**",
16066
+ "1. ...",
16067
+ "",
16068
+ "**Expected behavior:**",
16069
+ "...",
16070
+ "",
16071
+ "**Actual behavior:**",
16072
+ "...",
16073
+ "",
16074
+ "Report at: https://github.com/jinzhengdong/ai-cli/issues"
16075
+ ];
16076
+ ctx.send({ type: "info", message: lines.join("\n") });
16058
16077
  }
16059
16078
 
16060
16079
  // src/web/commands/agent-commands.ts
16061
16080
  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"}
16081
+ const sub = args[0]?.toLowerCase() ?? "status";
16082
+ if (sub === "on") {
16083
+ ctx.toolExecutor.sessionAutoMode = true;
16084
+ 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." });
16085
+ } else if (sub === "off") {
16086
+ ctx.toolExecutor.sessionAutoMode = false;
16087
+ ctx.send({ type: "info", message: "Auto Mode disabled." });
16088
+ } else if (sub === "status") {
16089
+ ctx.send({ type: "info", message: `Auto Mode: ${ctx.toolExecutor.sessionAutoMode ? "on" : "off"}
16072
16090
  Scope: current session only
16073
16091
  Denied: /permissions recently-denied` });
16074
- } else {
16075
- ctx.send({ type: "error", message: "Usage: /auto [on|off|status]" });
16076
- }
16077
- ctx.sendStatus();
16092
+ } else {
16093
+ ctx.send({ type: "error", message: "Usage: /auto [on|off|status]" });
16078
16094
  }
16095
+ ctx.sendStatus();
16079
16096
  }
16080
16097
  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." });
16098
+ const sub = args[0]?.toLowerCase() ?? "recently-denied";
16099
+ if (sub === "recently-denied") {
16100
+ const rows = getRecentlyDeniedAutoActions();
16101
+ if (rows.length === 0) {
16102
+ ctx.send({ type: "info", message: "No recently denied Auto Mode actions." });
16099
16103
  } else {
16100
- ctx.send({ type: "error", message: "Usage: /permissions recently-denied|clear-denied" });
16104
+ const lines = [`Recently denied Auto Mode actions (${rows.length})`, ""];
16105
+ for (const r of rows.slice(0, 20)) {
16106
+ lines.push(`- ${r.timestamp} ${r.tool} ${r.ruleId}`);
16107
+ lines.push(` ${r.reason}`);
16108
+ lines.push(` ${r.argsPreview}`);
16109
+ }
16110
+ ctx.send({ type: "info", message: lines.join("\n") });
16101
16111
  }
16112
+ } else if (sub === "clear-denied") {
16113
+ clearRecentlyDeniedAutoActions();
16114
+ ctx.send({ type: "info", message: "Cleared recently denied Auto Mode actions." });
16115
+ } else {
16116
+ ctx.send({ type: "error", message: "Usage: /permissions recently-denied|clear-denied" });
16102
16117
  }
16103
16118
  }
16104
16119
  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
- }
16120
+ const sub = args[0]?.toLowerCase();
16121
+ if (sub === "off") {
16122
+ ctx.toolExecutor.sessionAutoApprove = false;
16123
+ ctx.send({ type: "info", message: "\u{1F512} Auto-approve disabled \u2014 confirmations restored for this session." });
16124
+ } else {
16125
+ ctx.toolExecutor.sessionAutoApprove = true;
16126
+ 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
16127
  }
16115
16128
  }
16116
16129
  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") });
16130
+ const sub = args[0]?.toLowerCase() ?? "summary";
16131
+ if (sub === "list") {
16132
+ const agents = listAgentConfigs(ctx.config.getConfigDir());
16133
+ const lines = [`Agents (${agents.length})`, ""];
16134
+ for (const agent of agents) {
16135
+ const active = agent.name === getPreferredAgentName() ? " *" : " ";
16136
+ const model = [agent.provider, agent.model].filter(Boolean).join("/") || "inherit";
16137
+ lines.push(`${active} ${agent.name} [${agent.source ?? "builtin"}] \u2014 ${agent.description ?? ""}`);
16138
+ lines.push(` model: ${model} \xB7 permission: ${agent.permissionProfile ?? "workspace-write"} \xB7 maxRounds: ${agent.maxToolRounds ?? "inherit"}`);
16139
+ }
16140
+ lines.push("", "Config dirs: ~/.aicli/agents/ and .aicli/agents/");
16141
+ ctx.send({ type: "info", message: lines.join("\n") });
16142
+ return;
16143
+ }
16144
+ if (sub === "switch") {
16145
+ const name = args[1];
16146
+ if (!name) {
16147
+ ctx.send({ type: "error", message: "Usage: /agent switch <name>" });
16148
+ return;
16130
16149
  }
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}
16150
+ const agent = resolveAgentConfig(name, ctx.config.getConfigDir());
16151
+ if (!agent) {
16152
+ const available = listAgentConfigs(ctx.config.getConfigDir()).map((a) => a.name).join(", ");
16153
+ ctx.send({ type: "error", message: `Unknown agent: ${name}
16140
16154
  Available: ${available}` });
16141
- }
16142
- setPreferredAgentName(agent.name);
16143
- ctx.send({ type: "info", message: `Preferred spawn_agent role: ${agent.name}` });
16144
- ctx.sendStatus();
16155
+ return;
16145
16156
  }
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();
16157
+ setPreferredAgentName(agent.name);
16158
+ ctx.send({ type: "info", message: `Preferred spawn_agent role: ${agent.name}` });
16159
+ ctx.sendStatus();
16160
+ return;
16161
+ }
16162
+ if (sub === "stop") {
16163
+ const target = args[1] ?? "all";
16164
+ const count = requestAgentStop(target);
16165
+ ctx.send({ type: "info", message: count > 0 ? `Stop requested for ${count} running agent(s).` : "No matching running agents." });
16166
+ ctx.sendStatus();
16167
+ return;
16168
+ }
16169
+ if (sub === "summary") {
16170
+ const runs2 = listAgentRuns();
16171
+ if (runs2.length === 0) {
16172
+ ctx.send({ type: "info", message: "No sub-agent runs yet." });
16173
+ return;
16151
16174
  }
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") });
16175
+ const lines = [`Recent agent runs (${runs2.length})`, ""];
16176
+ for (const run of runs2.slice(0, 20)) {
16177
+ lines.push(`${run.id} ${run.status} ${run.agentName} ${run.startedAt}`);
16178
+ lines.push(` task: ${run.task.slice(0, 120)}${run.task.length > 120 ? "..." : ""}`);
16179
+ if (run.toolNames.length > 0) lines.push(` tools: ${run.toolNames.join(", ")}`);
16180
+ if (run.summary) lines.push(` summary: ${run.summary.slice(0, 180)}${run.summary.length > 180 ? "..." : ""}`);
16181
+ if (run.error) lines.push(` error: ${run.error}`);
16166
16182
  }
16167
- ctx.send({ type: "error", message: "Usage: /agent list|switch <name>|stop <id|all>|summary" });
16183
+ ctx.send({ type: "info", message: lines.join("\n") });
16184
+ return;
16168
16185
  }
16186
+ ctx.send({ type: "error", message: "Usage: /agent list|switch <name>|stop <id|all>|summary" });
16169
16187
  }
16170
16188
 
16171
16189
  // src/web/commands/misc-commands.ts
16172
- async function handleHelp(args, ctx) {
16190
+ async function handleHelp(_args, ctx) {
16173
16191
  ctx.send({
16174
16192
  type: "info",
16175
16193
  message: [
@@ -16227,90 +16245,87 @@ async function handleHelp(args, ctx) {
16227
16245
  ].join("\n")
16228
16246
  });
16229
16247
  }
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
- }
16248
+ async function handleAbout(_args, ctx) {
16249
+ const providerList = ctx.providers.listAll();
16250
+ const toolCount = ctx.toolRegistry.getDefinitions().length;
16251
+ const mcpTools = ctx.mcpManager?.getTotalToolCount() ?? 0;
16252
+ const lines = [
16253
+ `\u{1F916} **ai-cli** v${VERSION}`,
16254
+ "",
16255
+ `${DESCRIPTION}`,
16256
+ `Author: ${AUTHOR} <${AUTHOR_EMAIL}>`,
16257
+ "",
16258
+ `**Providers:** ${providerList.length} (${providerList.map((p) => p.id).join(", ")})`,
16259
+ `**Tools:** ${toolCount + mcpTools} (${toolCount} built-in${mcpTools > 0 ? ` + ${mcpTools} MCP` : ""})`,
16260
+ `**REPL Commands:** 37`,
16261
+ "",
16262
+ `GitHub: https://github.com/jinzhengdong/ai-cli`
16263
+ ];
16264
+ ctx.send({ type: "info", message: lines.join("\n") });
16249
16265
  }
16250
16266
  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("");
16267
+ const query = args.join(" ").trim();
16268
+ if (!query) {
16269
+ ctx.send({ type: "error", message: "Usage: /search <keyword>" });
16270
+ return;
16271
+ }
16272
+ const results = ctx.sessions.searchMessages(query);
16273
+ if (results.length === 0) {
16274
+ ctx.send({ type: "info", message: `No sessions found containing "${query}".` });
16275
+ return;
16276
+ }
16277
+ const lines = [`\u{1F50D} Found ${results.length} session(s) containing "${query}"`, ""];
16278
+ for (const r of results) {
16279
+ const { sessionMeta, matches } = r;
16280
+ const dateStr = sessionMeta.updated instanceof Date ? sessionMeta.updated.toLocaleDateString() : String(sessionMeta.updated);
16281
+ lines.push(`**${sessionMeta.id.slice(0, 8)}** [${dateStr}] ${sessionMeta.provider} / ${sessionMeta.model}`);
16282
+ if (sessionMeta.title) lines.push(` ${sessionMeta.title}`);
16283
+ for (const m of matches) {
16284
+ const icon = m.role === "user" ? "\u{1F464}" : "\u{1F916}";
16285
+ lines.push(` ${icon} ${m.snippet}`);
16271
16286
  }
16272
- lines.push("Use /session load <id> to resume a session.");
16273
- ctx.send({ type: "info", message: lines.join("\n") });
16287
+ lines.push("");
16274
16288
  }
16289
+ lines.push("Use /session load <id> to resume a session.");
16290
+ ctx.send({ type: "info", message: lines.join("\n") });
16275
16291
  }
16276
16292
  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);
16293
+ const sub = args.join(" ").trim();
16294
+ if (sub === "list") {
16295
+ const history = undoStack.getHistory();
16296
+ if (history.length === 0) {
16297
+ ctx.send({ type: "info", message: "Undo stack is empty." });
16298
+ return;
16307
16299
  }
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:
16300
+ const lines = [`\u{1F4CB} Undo Stack (${history.length} entries, newest last):`, ""];
16301
+ history.forEach((entry, i) => {
16302
+ const timeStr = entry.timestamp.toLocaleTimeString();
16303
+ const typeTag = entry.isDirectory ? "[dir]" : entry.previousContent === null ? "[new]" : "[mod]";
16304
+ lines.push(` ${String(i + 1).padStart(3)} ${typeTag} ${entry.description} ${timeStr}`);
16305
+ });
16306
+ ctx.send({ type: "info", message: lines.join("\n") });
16307
+ return;
16308
+ }
16309
+ const n = sub ? parseInt(sub, 10) : 1;
16310
+ if (isNaN(n) || n < 1) {
16311
+ ctx.send({ type: "info", message: "Usage: /undo [list | <n>]" });
16312
+ return;
16313
+ }
16314
+ if (undoStack.depth === 0) {
16315
+ ctx.send({ type: "info", message: "Nothing to undo." });
16316
+ return;
16317
+ }
16318
+ const count = Math.min(n, undoStack.depth);
16319
+ const undoResults = [];
16320
+ for (let i = 0; i < count; i++) {
16321
+ const undoResult = undoStack.undo();
16322
+ if (undoResult) undoResults.push(undoResult.result);
16323
+ }
16324
+ if (undoResults.length === 0) {
16325
+ ctx.send({ type: "info", message: "Nothing to undo." });
16326
+ } else {
16327
+ ctx.send({ type: "info", message: `\u2713 ${undoResults.length} operation(s) undone:
16312
16328
  ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
16313
- }
16314
16329
  }
16315
16330
  }
16316
16331
 
@@ -16359,20 +16374,6 @@ var webCommandHandlers = {
16359
16374
  search: handleSearch,
16360
16375
  undo: handleUndo
16361
16376
  };
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
16377
  var SessionHandler = class {
16377
16378
  ws;
16378
16379
  config;
@@ -16551,10 +16552,10 @@ var SessionHandler = class {
16551
16552
  return;
16552
16553
  }
16553
16554
  case "ask_user_response": {
16554
- const resolve15 = this.pendingAskUser.get(msg.requestId);
16555
- if (resolve15) {
16555
+ const resolve14 = this.pendingAskUser.get(msg.requestId);
16556
+ if (resolve14) {
16556
16557
  this.pendingAskUser.delete(msg.requestId);
16557
- resolve15(msg.answer);
16558
+ resolve14(msg.answer);
16558
16559
  }
16559
16560
  return;
16560
16561
  }
@@ -16567,10 +16568,10 @@ var SessionHandler = class {
16567
16568
  case "memory_manage":
16568
16569
  return this.handleMemoryManage(msg.action, msg.id, msg.expiresAt);
16569
16570
  case "auto_pause_response": {
16570
- const resolve15 = this.pendingAutoPause.get(msg.requestId);
16571
- if (resolve15) {
16571
+ const resolve14 = this.pendingAutoPause.get(msg.requestId);
16572
+ if (resolve14) {
16572
16573
  this.pendingAutoPause.delete(msg.requestId);
16573
- resolve15({ action: msg.action, message: msg.message });
16574
+ resolve14({ action: msg.action, message: msg.message });
16574
16575
  }
16575
16576
  return;
16576
16577
  }
@@ -16585,10 +16586,10 @@ var SessionHandler = class {
16585
16586
  this.hubOrchestrator?.abort();
16586
16587
  return;
16587
16588
  case "hub_steer": {
16588
- const resolve15 = this.pendingHubReview.get(msg.requestId);
16589
- if (resolve15) {
16589
+ const resolve14 = this.pendingHubReview.get(msg.requestId);
16590
+ if (resolve14) {
16590
16591
  this.pendingHubReview.delete(msg.requestId);
16591
- resolve15({ action: msg.action, message: msg.message });
16592
+ resolve14({ action: msg.action, message: msg.message });
16592
16593
  }
16593
16594
  return;
16594
16595
  }
@@ -16601,9 +16602,9 @@ var SessionHandler = class {
16601
16602
  runLifecycleHooks(mergePluginHooks(this.config.get("hooks") ?? void 0, this.config.getConfigDir()), "Stop", { sessionId: this.sessions.current?.id }, { configDir: this.config.getConfigDir() });
16602
16603
  this.toolExecutor.cancelAll();
16603
16604
  if (this.abortController) this.abortController.abort();
16604
- for (const resolve15 of this.pendingAskUser.values()) resolve15(null);
16605
+ for (const resolve14 of this.pendingAskUser.values()) resolve14(null);
16605
16606
  this.pendingAskUser.clear();
16606
- for (const resolve15 of this.pendingAutoPause.values()) resolve15({ action: "stop" });
16607
+ for (const resolve14 of this.pendingAutoPause.values()) resolve14({ action: "stop" });
16607
16608
  this.pendingAutoPause.clear();
16608
16609
  this.saveIfNeeded();
16609
16610
  }
@@ -16737,9 +16738,9 @@ var SessionHandler = class {
16737
16738
  this.hubOrchestrator = orchestrator;
16738
16739
  orchestrator.onEvent = (event) => this.send({ type: "hub_event", event });
16739
16740
  if (config.humanSteer) {
16740
- orchestrator.onRoundReview = ({ round, maxRounds }) => new Promise((resolve15) => {
16741
+ orchestrator.onRoundReview = ({ round, maxRounds }) => new Promise((resolve14) => {
16741
16742
  const requestId = `hubrev_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
16742
- this.pendingHubReview.set(requestId, resolve15);
16743
+ this.pendingHubReview.set(requestId, resolve14);
16743
16744
  this.send({ type: "hub_review", requestId, round, maxRounds });
16744
16745
  });
16745
16746
  }
@@ -17072,7 +17073,7 @@ Try: /compact to reduce context, /clear to reset, or switch to a larger-context
17072
17073
  this.send({ type: "response_done", content, usage });
17073
17074
  if (pendingTeeSave && isCleanDocumentBody(content)) {
17074
17075
  try {
17075
- mkdirSync17(dirname11(pendingTeeSave), { recursive: true });
17076
+ mkdirSync16(dirname10(pendingTeeSave), { recursive: true });
17076
17077
  const bodyToSave = stripOuterCodeFence(content);
17077
17078
  atomicWriteFileSync(pendingTeeSave, bodyToSave);
17078
17079
  undoStack.push(pendingTeeSave, `save_last_response (deferred): ${pendingTeeSave}`);
@@ -17097,8 +17098,8 @@ Try: /compact to reduce context, /clear to reset, or switch to a larger-context
17097
17098
  onMcpToolUsed: (name) => this.usedMcpToolNames.add(name),
17098
17099
  requestAutoPause: async ({ effectiveRound, maxToolRounds: totalRounds, toolSummary }) => {
17099
17100
  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);
17101
+ const pauseResp = await new Promise((resolve14) => {
17102
+ this.pendingAutoPause.set(requestId, resolve14);
17102
17103
  this.send({
17103
17104
  type: "auto_pause_request",
17104
17105
  requestId,
@@ -17248,8 +17249,8 @@ ${summaryContent}`,
17248
17249
  let isError = false;
17249
17250
  let summary;
17250
17251
  try {
17251
- mkdirSync17(dirname11(saveToFile), { recursive: true });
17252
- fileStream = createWriteStream4(saveToFile);
17252
+ mkdirSync16(dirname10(saveToFile), { recursive: true });
17253
+ fileStream = createWriteStream3(saveToFile);
17253
17254
  const teeSystemPrompt = stripToolCallReminder(systemPrompt ?? "") + CONTENT_ONLY_STREAM_REMINDER;
17254
17255
  const teeExtraMessages = extraMessages.length > 0 ? [...extraMessages, { role: "user", content: TEE_FINAL_USER_NUDGE }] : [{ role: "user", content: TEE_FINAL_USER_NUDGE }];
17255
17256
  const chatRequest = {
@@ -17277,8 +17278,8 @@ ${summaryContent}`,
17277
17278
  }
17278
17279
  if (chunk.done) break;
17279
17280
  }
17280
- await new Promise((resolve15, reject) => {
17281
- fileStream.end((err) => err ? reject(err) : resolve15());
17281
+ await new Promise((resolve14, reject) => {
17282
+ fileStream.end((err) => err ? reject(err) : resolve14());
17282
17283
  });
17283
17284
  const verdict = evaluateTeeContent(fullContent, saveToFile, priorContent);
17284
17285
  if (verdict.kind === "reject") {
@@ -17304,7 +17305,7 @@ ${summaryContent}`,
17304
17305
  } catch (err) {
17305
17306
  if (fileStream) {
17306
17307
  try {
17307
- await new Promise((resolve15) => fileStream.end(() => resolve15()));
17308
+ await new Promise((resolve14) => fileStream.end(() => resolve14()));
17308
17309
  } catch {
17309
17310
  }
17310
17311
  }
@@ -17541,7 +17542,7 @@ ${entry.content}`;
17541
17542
  const configDir = this.config.getConfigDir();
17542
17543
  try {
17543
17544
  atomicWriteFileSync(memoryStorePath(configDir), "");
17544
- atomicWriteFileSync(join27(configDir, MEMORY_FILE_NAME), "");
17545
+ atomicWriteFileSync(join26(configDir, MEMORY_FILE_NAME), "");
17545
17546
  this.send({ type: "info", message: "\u{1F5D1}\uFE0F Persistent memory cleared." });
17546
17547
  this.sendMemoryEntries();
17547
17548
  } catch (err) {
@@ -17823,8 +17824,8 @@ async function setupProxy(configProxy) {
17823
17824
  }
17824
17825
 
17825
17826
  // 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";
17827
+ import { existsSync as existsSync32, readFileSync as readFileSync25, writeFileSync as writeFileSync6, mkdirSync as mkdirSync17, readdirSync as readdirSync16, copyFileSync } from "fs";
17828
+ import { join as join27 } from "path";
17828
17829
  import { createHmac, randomBytes, timingSafeEqual, pbkdf2Sync } from "crypto";
17829
17830
  var USERS_FILE = "users.json";
17830
17831
  var TOKEN_EXPIRY_HOURS = 24;
@@ -17839,7 +17840,7 @@ var AuthManager = class {
17839
17840
  db;
17840
17841
  constructor(baseDir) {
17841
17842
  this.baseDir = baseDir;
17842
- this.usersFile = join28(baseDir, USERS_FILE);
17843
+ this.usersFile = join27(baseDir, USERS_FILE);
17843
17844
  this.db = this.loadOrCreate();
17844
17845
  }
17845
17846
  // ── Public API ─────────────────────────────────────────────────
@@ -17868,9 +17869,9 @@ var AuthManager = class {
17868
17869
  }
17869
17870
  const salt = randomBytes(16).toString("hex");
17870
17871
  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 });
17872
+ const dataDir = join27(USERS_DIR, username);
17873
+ const fullDataDir = join27(this.baseDir, dataDir);
17874
+ mkdirSync17(join27(fullDataDir, "history"), { recursive: true });
17874
17875
  const user = {
17875
17876
  username,
17876
17877
  passwordHash,
@@ -17985,7 +17986,7 @@ var AuthManager = class {
17985
17986
  getUserDataDir(username) {
17986
17987
  const user = this.db.users.find((u) => u.username === username);
17987
17988
  if (!user) throw new Error(`User not found: ${username}`);
17988
- return join28(this.baseDir, user.dataDir);
17989
+ return join27(this.baseDir, user.dataDir);
17989
17990
  }
17990
17991
  /** List all usernames */
17991
17992
  listUsers() {
@@ -18021,30 +18022,30 @@ var AuthManager = class {
18021
18022
  const err = this.register(username, password);
18022
18023
  if (err) return err;
18023
18024
  const userDir = this.getUserDataDir(username);
18024
- const globalConfig = join28(this.baseDir, "config.json");
18025
- if (existsSync33(globalConfig)) {
18025
+ const globalConfig = join27(this.baseDir, "config.json");
18026
+ if (existsSync32(globalConfig)) {
18026
18027
  try {
18027
- const content = readFileSync26(globalConfig, "utf-8");
18028
- writeFileSync7(join28(userDir, "config.json"), content, "utf-8");
18028
+ const content = readFileSync25(globalConfig, "utf-8");
18029
+ writeFileSync6(join27(userDir, "config.json"), content, "utf-8");
18029
18030
  } catch {
18030
18031
  }
18031
18032
  }
18032
- const globalMemory = join28(this.baseDir, "memory.md");
18033
- if (existsSync33(globalMemory)) {
18033
+ const globalMemory = join27(this.baseDir, "memory.md");
18034
+ if (existsSync32(globalMemory)) {
18034
18035
  try {
18035
- const content = readFileSync26(globalMemory, "utf-8");
18036
- writeFileSync7(join28(userDir, "memory.md"), content, "utf-8");
18036
+ const content = readFileSync25(globalMemory, "utf-8");
18037
+ writeFileSync6(join27(userDir, "memory.md"), content, "utf-8");
18037
18038
  } catch {
18038
18039
  }
18039
18040
  }
18040
- const globalHistory = join28(this.baseDir, "history");
18041
- if (existsSync33(globalHistory)) {
18041
+ const globalHistory = join27(this.baseDir, "history");
18042
+ if (existsSync32(globalHistory)) {
18042
18043
  try {
18043
- const files = readdirSync17(globalHistory).filter((f) => f.endsWith(".json"));
18044
- const userHistory = join28(userDir, "history");
18044
+ const files = readdirSync16(globalHistory).filter((f) => f.endsWith(".json"));
18045
+ const userHistory = join27(userDir, "history");
18045
18046
  for (const f of files) {
18046
18047
  try {
18047
- copyFileSync(join28(globalHistory, f), join28(userHistory, f));
18048
+ copyFileSync(join27(globalHistory, f), join27(userHistory, f));
18048
18049
  } catch {
18049
18050
  }
18050
18051
  }
@@ -18055,9 +18056,9 @@ var AuthManager = class {
18055
18056
  }
18056
18057
  // ── Private methods ────────────────────────────────────────────
18057
18058
  loadOrCreate() {
18058
- if (existsSync33(this.usersFile)) {
18059
+ if (existsSync32(this.usersFile)) {
18059
18060
  try {
18060
- return JSON.parse(readFileSync26(this.usersFile, "utf-8"));
18061
+ return JSON.parse(readFileSync25(this.usersFile, "utf-8"));
18061
18062
  } catch {
18062
18063
  }
18063
18064
  }
@@ -18073,7 +18074,7 @@ var AuthManager = class {
18073
18074
  this.saveDB(this.db);
18074
18075
  }
18075
18076
  saveDB(db) {
18076
- mkdirSync18(this.baseDir, { recursive: true });
18077
+ mkdirSync17(this.baseDir, { recursive: true });
18077
18078
  atomicWriteFileSync(this.usersFile, JSON.stringify(db, null, 2));
18078
18079
  }
18079
18080
  /** Legacy hash — kept only for migrating old users (v0.2.x) */
@@ -18105,11 +18106,11 @@ function getModuleDir() {
18105
18106
  if (typeof import.meta?.url === "string") {
18106
18107
  const url = new URL(import.meta.url);
18107
18108
  const filePath = url.pathname.replace(/^\/([A-Z]:)/i, "$1");
18108
- return dirname12(filePath);
18109
+ return dirname11(filePath);
18109
18110
  }
18110
18111
  } catch {
18111
18112
  }
18112
- return resolve14(".");
18113
+ return resolve13(".");
18113
18114
  }
18114
18115
  async function startWebServer(options = {}) {
18115
18116
  const port = options.port ?? 3e3;
@@ -18177,8 +18178,8 @@ async function startWebServer(options = {}) {
18177
18178
  }
18178
18179
  }
18179
18180
  let skillManager = null;
18180
- const skillsDir = join29(config.getConfigDir(), SKILLS_DIR_NAME);
18181
- if (existsSync34(skillsDir)) {
18181
+ const skillsDir = join28(config.getConfigDir(), SKILLS_DIR_NAME);
18182
+ if (existsSync33(skillsDir)) {
18182
18183
  skillManager = new SkillManager(skillsDir, config.get("ui").skillSizeWarn);
18183
18184
  skillManager.loadSkills(pluginAssets.skillDirs);
18184
18185
  const count = skillManager.listSkills().length;
@@ -18249,18 +18250,18 @@ async function startWebServer(options = {}) {
18249
18250
  next();
18250
18251
  };
18251
18252
  const moduleDir = getModuleDir();
18252
- let clientDir = join29(moduleDir, "web", "client");
18253
- if (!existsSync34(clientDir)) {
18254
- clientDir = join29(moduleDir, "client");
18253
+ let clientDir = join28(moduleDir, "web", "client");
18254
+ if (!existsSync33(clientDir)) {
18255
+ clientDir = join28(moduleDir, "client");
18255
18256
  }
18256
- if (!existsSync34(clientDir)) {
18257
- clientDir = join29(moduleDir, "..", "..", "src", "web", "client");
18257
+ if (!existsSync33(clientDir)) {
18258
+ clientDir = join28(moduleDir, "..", "..", "src", "web", "client");
18258
18259
  }
18259
- if (!existsSync34(clientDir)) {
18260
- clientDir = join29(process.cwd(), "src", "web", "client");
18260
+ if (!existsSync33(clientDir)) {
18261
+ clientDir = join28(process.cwd(), "src", "web", "client");
18261
18262
  }
18262
18263
  console.log(` Static files: ${clientDir}`);
18263
- app.use("/vendor", express.static(join29(clientDir, "vendor"), { maxAge: "1y", immutable: true }));
18264
+ app.use("/vendor", express.static(join28(clientDir, "vendor"), { maxAge: "1y", immutable: true }));
18264
18265
  app.use(express.static(clientDir));
18265
18266
  app.get("/api/status", (_req, res) => {
18266
18267
  res.json({
@@ -18327,10 +18328,10 @@ async function startWebServer(options = {}) {
18327
18328
  app.get("/api/files", requireAuth, (req, res) => {
18328
18329
  const cwd = process.cwd();
18329
18330
  const prefix = req.query.prefix || "";
18330
- const targetDir = join29(cwd, prefix);
18331
+ const targetDir = join28(cwd, prefix);
18331
18332
  try {
18332
- const canonicalTarget = realpathSync(resolve14(targetDir));
18333
- const canonicalCwd = realpathSync(resolve14(cwd));
18333
+ const canonicalTarget = realpathSync(resolve13(targetDir));
18334
+ const canonicalCwd = realpathSync(resolve13(cwd));
18334
18335
  if (!canonicalTarget.startsWith(canonicalCwd + sep3) && canonicalTarget !== canonicalCwd) {
18335
18336
  res.json({ files: [] });
18336
18337
  return;
@@ -18341,10 +18342,10 @@ async function startWebServer(options = {}) {
18341
18342
  }
18342
18343
  try {
18343
18344
  const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "dist-cjs", "release", "__pycache__", ".next", ".nuxt", "coverage", ".cache"]);
18344
- const entries = readdirSync18(targetDir, { withFileTypes: true });
18345
+ const entries = readdirSync17(targetDir, { withFileTypes: true });
18345
18346
  const files = entries.filter((e) => !SKIP.has(e.name) && !e.name.startsWith(".")).slice(0, 50).map((e) => ({
18346
18347
  name: e.name,
18347
- path: relative4(cwd, join29(targetDir, e.name)).replace(/\\/g, "/"),
18348
+ path: relative4(cwd, join28(targetDir, e.name)).replace(/\\/g, "/"),
18348
18349
  isDir: e.isDirectory()
18349
18350
  }));
18350
18351
  res.json({ files });
@@ -18384,8 +18385,8 @@ async function startWebServer(options = {}) {
18384
18385
  try {
18385
18386
  const authUser = req._authUser;
18386
18387
  const histDir = authUser ? getUserShared(authUser).config.getHistoryDir() : config.getHistoryDir();
18387
- const filePath = join29(histDir, `${id}.json`);
18388
- if (!existsSync34(filePath)) {
18388
+ const filePath = join28(histDir, `${id}.json`);
18389
+ if (!existsSync33(filePath)) {
18389
18390
  res.status(404).json({ error: "Session not found" });
18390
18391
  return;
18391
18392
  }
@@ -18400,7 +18401,7 @@ async function startWebServer(options = {}) {
18400
18401
  res.status(404).json({ error: "Session not found" });
18401
18402
  return;
18402
18403
  }
18403
- const data = JSON.parse(readFileSync27(filePath, "utf-8"));
18404
+ const data = JSON.parse(readFileSync26(filePath, "utf-8"));
18404
18405
  res.json({ session: data });
18405
18406
  } catch (err) {
18406
18407
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
@@ -18413,10 +18414,10 @@ async function startWebServer(options = {}) {
18413
18414
  return;
18414
18415
  }
18415
18416
  const cwd = process.cwd();
18416
- const fullPath = resolve14(join29(cwd, filePath));
18417
+ const fullPath = resolve13(join28(cwd, filePath));
18417
18418
  try {
18418
18419
  const canonicalFull = realpathSync(fullPath);
18419
- const canonicalCwd = realpathSync(resolve14(cwd));
18420
+ const canonicalCwd = realpathSync(resolve13(cwd));
18420
18421
  if (!canonicalFull.startsWith(canonicalCwd + sep3) && canonicalFull !== canonicalCwd) {
18421
18422
  res.json({ error: "Access denied" });
18422
18423
  return;
@@ -18426,12 +18427,12 @@ async function startWebServer(options = {}) {
18426
18427
  return;
18427
18428
  }
18428
18429
  try {
18429
- const stat = statSync16(fullPath);
18430
+ const stat = statSync15(fullPath);
18430
18431
  if (stat.size > 512 * 1024) {
18431
18432
  res.json({ error: `File too large (${(stat.size / 1024).toFixed(0)} KB, max 512 KB)` });
18432
18433
  return;
18433
18434
  }
18434
- const content = readFileSync27(fullPath, "utf-8");
18435
+ const content = readFileSync26(fullPath, "utf-8");
18435
18436
  res.json({ content, size: stat.size });
18436
18437
  } catch {
18437
18438
  res.json({ error: "Cannot read file" });
@@ -18635,7 +18636,7 @@ async function startWebServer(options = {}) {
18635
18636
  });
18636
18637
  const MAX_PORT_ATTEMPTS = 10;
18637
18638
  let actualPort = port;
18638
- const result = await new Promise((resolve15, reject) => {
18639
+ const result = await new Promise((resolve14, reject) => {
18639
18640
  const tryListen = (attempt) => {
18640
18641
  server.once("error", (err) => {
18641
18642
  if (err.code === "EADDRINUSE" && attempt < MAX_PORT_ATTEMPTS) {
@@ -18666,7 +18667,7 @@ async function startWebServer(options = {}) {
18666
18667
  }
18667
18668
  console.log(` Press Ctrl+C to stop
18668
18669
  `);
18669
- resolve15({ port: actualPort, host, url });
18670
+ resolve14({ port: actualPort, host, url });
18670
18671
  });
18671
18672
  };
18672
18673
  tryListen(1);
@@ -18686,17 +18687,17 @@ function resolveProjectMcpPath() {
18686
18687
  const cwd = process.cwd();
18687
18688
  const gitRoot = getGitRoot(cwd);
18688
18689
  const projectRoot = gitRoot ?? cwd;
18689
- const configPath = join29(projectRoot, MCP_PROJECT_CONFIG_NAME);
18690
- return existsSync34(configPath) ? configPath : null;
18690
+ const configPath = join28(projectRoot, MCP_PROJECT_CONFIG_NAME);
18691
+ return existsSync33(configPath) ? configPath : null;
18691
18692
  }
18692
18693
  function loadProjectMcpConfig() {
18693
18694
  const cwd = process.cwd();
18694
18695
  const gitRoot = getGitRoot(cwd);
18695
18696
  const projectRoot = gitRoot ?? cwd;
18696
- const configPath = join29(projectRoot, MCP_PROJECT_CONFIG_NAME);
18697
- if (!existsSync34(configPath)) return null;
18697
+ const configPath = join28(projectRoot, MCP_PROJECT_CONFIG_NAME);
18698
+ if (!existsSync33(configPath)) return null;
18698
18699
  try {
18699
- const raw = JSON.parse(readFileSync27(configPath, "utf-8"));
18700
+ const raw = JSON.parse(readFileSync26(configPath, "utf-8"));
18700
18701
  return raw.mcpServers ?? raw;
18701
18702
  } catch {
18702
18703
  return null;