jinzd-ai-cli 0.4.307 → 0.4.308

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 (95) hide show
  1. package/README.md +1 -0
  2. package/THIRD_PARTY_NOTICES.md +3 -0
  3. package/dist/{agent-client-6GX6QQDU.js → agent-client-TIEAYZXV.js} +4 -1
  4. package/dist/{auth-PE3Z5OHS.js → auth-QTQNZ6IX.js} +2 -2
  5. package/dist/{batch-HMADXNOD.js → batch-S7XLEEOH.js} +3 -3
  6. package/dist/{chat-index-2D5GK2DB.js → chat-index-7CLR4CBG.js} +4 -3
  7. package/dist/{chat-index-DGUOUUQW.js → chat-index-RLVLQRRC.js} +3 -2
  8. package/dist/{chunk-SJFZPS65.js → chunk-2DYQDLLF.js} +1 -1
  9. package/dist/{chunk-VAV2IJ3R.js → chunk-2MNUMLN4.js} +1 -1
  10. package/dist/chunk-2XMUCC42.js +67 -0
  11. package/dist/{chunk-5GQL4TSZ.js → chunk-3537CQ5M.js} +2 -2
  12. package/dist/{chunk-OAXLMF3U.js → chunk-3YVNDYOY.js} +1 -1
  13. package/dist/{chunk-RYQBBEMM.js → chunk-4PR5AVJ5.js} +115 -19
  14. package/dist/{chunk-GHWZVPUV.js → chunk-6JHACXB3.js} +7 -5
  15. package/dist/{chunk-CIM2PV2F.js → chunk-7OHXFTZO.js} +2 -2
  16. package/dist/{chunk-IUVDZVZ4.js → chunk-AXTLYOA7.js} +24 -17
  17. package/dist/{chunk-XJOK5353.js → chunk-B3MPYI4E.js} +2 -2
  18. package/dist/{chunk-3CKJOTVM.js → chunk-BGZN4K7Q.js} +4 -4
  19. package/dist/chunk-BMLQPSHP.js +69 -0
  20. package/dist/{chunk-NRSYBKWA.js → chunk-C7CKDFJS.js} +1 -1
  21. package/dist/{chunk-NQJSPMY2.js → chunk-E2PNXNKI.js} +4 -4
  22. package/dist/chunk-FVXNEZEW.js +161 -0
  23. package/dist/{chunk-YKJO3SI4.js → chunk-G6DVGQ4I.js} +1 -1
  24. package/dist/{chunk-YFDQIUQ3.js → chunk-HC33C4U3.js} +18 -18
  25. package/dist/chunk-JJFJWD35.js +162 -0
  26. package/dist/{chunk-KRCYR46A.js → chunk-JJNUASMJ.js} +264 -55
  27. package/dist/{chunk-NYNSYLFY.js → chunk-JL44HGZC.js} +1 -1
  28. package/dist/{chunk-NUJTNH76.js → chunk-LBWKMTT3.js} +1 -1
  29. package/dist/{chunk-AHL6KOGM.js → chunk-LR3TGZWQ.js} +2 -2
  30. package/dist/{chunk-WQ3OMEC4.js → chunk-M6HLYG33.js} +1 -1
  31. package/dist/{chunk-I4GQRMFV.js → chunk-M6YV5KSS.js} +6 -4
  32. package/dist/{chunk-3TBXHLSC.js → chunk-NLK45GUS.js} +1 -1
  33. package/dist/{chunk-KBDKKHXB.js → chunk-O6CGOVD4.js} +2 -2
  34. package/dist/chunk-QMFMST2T.js +39 -0
  35. package/dist/{chunk-MI4SFZGX.js → chunk-RMEJR3AW.js} +5 -2
  36. package/dist/{chunk-UFW4S3RU.js → chunk-RVMCY6DP.js} +1 -1
  37. package/dist/{chunk-LF67HWWR.js → chunk-RWRZVQCB.js} +1 -1
  38. package/dist/chunk-SXIJLSVF.js +41 -0
  39. package/dist/chunk-TDQW2YHK.js +65 -0
  40. package/dist/{chunk-L2RJPVHV.js → chunk-TPTFV3Y6.js} +266 -512
  41. package/dist/{chunk-45UVJXFQ.js → chunk-W2UOVGRC.js} +1 -1
  42. package/dist/{chunk-VXEF2SWG.js → chunk-WT252ZDV.js} +2 -2
  43. package/dist/{chunk-4W6VDVQU.js → chunk-WVEIUSXE.js} +246 -56
  44. package/dist/{chunk-AQF6IXSR.js → chunk-YBZM7PHI.js} +4 -4
  45. package/dist/{ci-QD2Q7NWY.js → ci-5HONSUAS.js} +7 -7
  46. package/dist/{ci-format-42XIZO5P.js → ci-format-URUH3ZZ5.js} +2 -2
  47. package/dist/{constants-NMASHFDL.js → constants-BLDBI7IG.js} +1 -1
  48. package/dist/{constants-5CMEFZQB.js → constants-FG27DQMS.js} +1 -1
  49. package/dist/doctor-cli-QQLMNJN7.js +18 -0
  50. package/dist/electron-server.js +886 -828
  51. package/dist/file-checkpoint-64A2AXHE.js +7 -0
  52. package/dist/{file-checkpoint-NKBHGC7L.js → file-checkpoint-VVPUJNCI.js} +2 -1
  53. package/dist/{hub-ZLF6GMQD.js → hub-GGFW3DCC.js} +3 -3
  54. package/dist/{hub-server-CBKCP5VD.js → hub-server-JLC7CNP7.js} +55 -17
  55. package/dist/index.js +42 -40
  56. package/dist/{indexer-P3FPRTPJ.js → indexer-GKIWSSRP.js} +3 -3
  57. package/dist/indexer-VRXDZ74A.js +10 -0
  58. package/dist/{persist-S77WRVLD.js → persist-ZZXRWFBX.js} +4 -4
  59. package/dist/{persistent-memory-GJE7HKVK.js → persistent-memory-UNLCXUWH.js} +3 -3
  60. package/dist/{persistent-memory-MBSBTBGY.js → persistent-memory-YYWIN6BG.js} +3 -3
  61. package/dist/{pr-SXRW24JG.js → pr-YCFGPFI7.js} +7 -7
  62. package/dist/{project-trust-MUG325AW.js → project-trust-6CJB5ZMX.js} +1 -1
  63. package/dist/{project-trust-NKYHL3VZ.js → project-trust-KGG2BWKC.js} +1 -1
  64. package/dist/run-tests-GHY4BYDE.js +13 -0
  65. package/dist/run-tests-KRNMFXAN.js +12 -0
  66. package/dist/{semantic-7GM5M4MU.js → semantic-N227CLSC.js} +4 -4
  67. package/dist/{semantic-QOBAXEQ4.js → semantic-TRQAZSRY.js} +4 -4
  68. package/dist/{server-MO3EU2XD.js → server-73UBHSFV.js} +16 -14
  69. package/dist/{server-GGEO7SA2.js → server-QRAWSG4Z.js} +231 -103
  70. package/dist/{store-SIKHCUYJ.js → store-2N6PPPBP.js} +2 -2
  71. package/dist/{store-CMSSWZF7.js → store-76VVFDXM.js} +2 -2
  72. package/dist/{task-orchestrator-JGEEQ2OE.js → task-orchestrator-GI3RGLZI.js} +15 -14
  73. package/dist/{usage-B7RIAPPT.js → usage-GWTIGZF5.js} +4 -4
  74. package/dist/{vector-store-JAXWDZYW.js → vector-store-T2DVUMY4.js} +2 -2
  75. package/dist/{vector-store-PWIOQFTP.js → vector-store-V6UGEYWK.js} +2 -2
  76. package/dist/web/client/app.js +7 -0
  77. package/dist/web/client/app.js.br +0 -0
  78. package/dist/web/client/app.js.gz +0 -0
  79. package/dist/web/client/util.js +14 -23
  80. package/dist/web/client/util.js.br +0 -0
  81. package/dist/web/client/util.js.gz +0 -0
  82. package/dist/web/client/vendor/DOMPurify-LICENSE.txt +202 -0
  83. package/dist/web/client/vendor/purify.es.mjs +2722 -0
  84. package/dist/web/client/vendor/purify.es.mjs.br +0 -0
  85. package/dist/web/client/vendor/purify.es.mjs.gz +0 -0
  86. package/package.json +4 -2
  87. package/dist/chunk-4BKXL7SM.js +0 -98
  88. package/dist/chunk-6BUTA5VW.js +0 -38
  89. package/dist/chunk-IW3Q7AE5.js +0 -40
  90. package/dist/chunk-SKET65WZ.js +0 -96
  91. package/dist/doctor-cli-IOVSHBDI.js +0 -18
  92. package/dist/file-checkpoint-CGH6OJVI.js +0 -6
  93. package/dist/indexer-FPKY7LCT.js +0 -10
  94. package/dist/run-tests-EQCAMFQE.js +0 -10
  95. package/dist/run-tests-V3N7YV3P.js +0 -9
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  hasSemanticIndex,
3
3
  semanticSearch
4
- } from "./chunk-KBDKKHXB.js";
5
- import "./chunk-OAXLMF3U.js";
4
+ } from "./chunk-O6CGOVD4.js";
5
+ import "./chunk-3YVNDYOY.js";
6
6
  import {
7
7
  buildChatIndex,
8
8
  clearChatIndex,
@@ -15,17 +15,22 @@ import {
15
15
  searchCandidates,
16
16
  searchChatMemory,
17
17
  updateSessionInIndex
18
- } from "./chunk-I4GQRMFV.js";
18
+ } from "./chunk-M6YV5KSS.js";
19
19
  import "./chunk-JV5N65KN.js";
20
20
  import {
21
- fileCheckpoints
22
- } from "./chunk-SKET65WZ.js";
21
+ MAX_SNAPSHOT_TOTAL_BYTES,
22
+ assertReadablePath,
23
+ canonicalPath,
24
+ fileCheckpoints,
25
+ isCredentialVault,
26
+ readSnapshot
27
+ } from "./chunk-FVXNEZEW.js";
23
28
  import {
24
29
  indexProject
25
- } from "./chunk-UFW4S3RU.js";
30
+ } from "./chunk-RVMCY6DP.js";
26
31
  import {
27
32
  loadIndex
28
- } from "./chunk-45UVJXFQ.js";
33
+ } from "./chunk-W2UOVGRC.js";
29
34
  import {
30
35
  addMemoryEntry,
31
36
  backupMemoryStore,
@@ -50,23 +55,39 @@ import {
50
55
  touchMemoryReferences,
51
56
  updateMemoryApproval,
52
57
  updateMemoryEntry
53
- } from "./chunk-CIM2PV2F.js";
58
+ } from "./chunk-7OHXFTZO.js";
54
59
  import {
55
60
  DEFAULT_PATTERNS,
56
61
  redactJson,
57
62
  scanString
58
63
  } from "./chunk-FSC6KEWU.js";
59
64
  import {
65
+ atomicWriteFile,
60
66
  atomicWriteFileSync
61
- } from "./chunk-6BUTA5VW.js";
67
+ } from "./chunk-2XMUCC42.js";
62
68
  import {
63
69
  formatGitContextForPrompt,
64
70
  getGitContext,
65
71
  getGitRoot
66
72
  } from "./chunk-CGLYJRFA.js";
67
73
  import {
68
- runTestsTool
69
- } from "./chunk-KRCYR46A.js";
74
+ IS_WINDOWS,
75
+ WIN_UTF8_PREAMBLE,
76
+ currentAbortSignal,
77
+ decodeWindowsBuffer,
78
+ killProcessTree,
79
+ looksLikeGbkMojibake,
80
+ recoverGbkMojibake,
81
+ runTestsTool,
82
+ shellInvocation
83
+ } from "./chunk-WVEIUSXE.js";
84
+ import {
85
+ getCurrentSessionKey,
86
+ getExecutionConfigDir,
87
+ runWithExecutionContext,
88
+ runWithSessionKey,
89
+ scopedState
90
+ } from "./chunk-QMFMST2T.js";
70
91
  import {
71
92
  AGENTIC_BEHAVIOR_GUIDELINE,
72
93
  APP_NAME,
@@ -101,7 +122,65 @@ import {
101
122
  SUBAGENT_MAX_ROUNDS_LIMIT,
102
123
  VERSION,
103
124
  buildUserIdentityPrompt
104
- } from "./chunk-SJFZPS65.js";
125
+ } from "./chunk-2DYQDLLF.js";
126
+
127
+ // src/web/access.ts
128
+ import { randomBytes, timingSafeEqual } from "crypto";
129
+ import { networkInterfaces } from "os";
130
+ function equalToken(a, b) {
131
+ if (typeof a !== "string" || a.length > 4096) return false;
132
+ const left = Buffer.from(a), right = Buffer.from(b);
133
+ return left.length === right.length && timingSafeEqual(left, right);
134
+ }
135
+ var WebAccess = class {
136
+ constructor(host, externalOrigin) {
137
+ this.externalOrigin = externalOrigin;
138
+ if (!["0.0.0.0", "::"].includes(host)) this.hosts.add(host.includes(":") ? `[${host}]` : host.toLowerCase());
139
+ else for (const interfaces of Object.values(networkInterfaces())) for (const entry of interfaces ?? []) {
140
+ this.hosts.add(entry.address.includes(":") ? `[${entry.address}]` : entry.address);
141
+ }
142
+ if (externalOrigin && new URL(externalOrigin).origin !== externalOrigin) throw new Error("External origin must be an exact http(s) origin");
143
+ if (externalOrigin && !/^https?:\/\//.test(externalOrigin)) throw new Error("External origin must use HTTP(S)");
144
+ }
145
+ externalOrigin;
146
+ localToken = randomBytes(32).toString("base64url");
147
+ hosts = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
148
+ accepts(req) {
149
+ const host = req.headers.host;
150
+ if (!host) return false;
151
+ try {
152
+ const url = new URL(`http://${host}`);
153
+ const internal = this.hosts.has(url.hostname.toLowerCase()) && Number(url.port || 80) === req.socket.localPort;
154
+ const external = this.externalOrigin && new URL(this.externalOrigin).host === host;
155
+ if (!internal && !external) return false;
156
+ const origin = req.headers.origin;
157
+ return !origin || origin === (external ? this.externalOrigin : `http://${host}`);
158
+ } catch {
159
+ return false;
160
+ }
161
+ }
162
+ };
163
+ var AuthRateLimiter = class {
164
+ ips = /* @__PURE__ */ new Map();
165
+ start = 0;
166
+ count = 0;
167
+ take(ip) {
168
+ const now = Date.now();
169
+ if (now - this.start >= 6e4) {
170
+ this.start = now;
171
+ this.count = 0;
172
+ }
173
+ if (++this.count > 120) return false;
174
+ for (const [key, value] of this.ips) if (now - value.start >= 6e4) this.ips.delete(key);
175
+ let bucket = this.ips.get(ip);
176
+ if (!bucket) {
177
+ if (this.ips.size >= 2048) return false;
178
+ bucket = { start: now, count: 0 };
179
+ this.ips.set(ip, bucket);
180
+ }
181
+ return ++bucket.count <= 30;
182
+ }
183
+ };
105
184
 
106
185
  // src/web/server.ts
107
186
  import express from "express";
@@ -125,9 +204,9 @@ function imageValidationError(images) {
125
204
  }
126
205
 
127
206
  // src/web/server.ts
128
- import { join as join28, dirname as dirname11, resolve as resolve12, relative as relative6, sep as sep5, extname as extname3, resolve as resolvePath } from "path";
129
- import { existsSync as existsSync32, readFileSync as readFileSync25, readdirSync as readdirSync15, statSync as statSync16, realpathSync as realpathSync3, createReadStream } from "fs";
130
- import { networkInterfaces } from "os";
207
+ import { join as join25, dirname as dirname11, resolve as resolve12, relative as relative6, sep as sep5, extname as extname3, resolve as resolvePath } from "path";
208
+ import { existsSync as existsSync31, readFileSync as readFileSync22, readdirSync as readdirSync15, statSync as statSync16, realpathSync as realpathSync3, createReadStream } from "fs";
209
+ import { networkInterfaces as networkInterfaces2 } from "os";
131
210
 
132
211
  // src/config/config-manager.ts
133
212
  import { readFileSync, existsSync, mkdirSync } from "fs";
@@ -650,8 +729,8 @@ ${err}`
650
729
  return changed;
651
730
  }
652
731
  save() {
653
- mkdirSync(this.configDir, { recursive: true });
654
- atomicWriteFileSync(this.configPath, JSON.stringify(this.config, null, 2));
732
+ mkdirSync(this.configDir, { recursive: true, mode: 448 });
733
+ atomicWriteFileSync(this.configPath, JSON.stringify(this.config, null, 2), { mode: 384 });
655
734
  }
656
735
  getApiKey(providerId) {
657
736
  const envKey = EnvLoader.getApiKey(providerId);
@@ -1293,6 +1372,7 @@ function getDangerLevel(toolName, args) {
1293
1372
  }
1294
1373
  if (toolName === "bash") {
1295
1374
  const cmd = String(args["command"] ?? "");
1375
+ if (/\bgit\s+(?:clean\b|reset\s+--hard\b|push\b[^\n]*(?:--force\b|\s-f\b))/i.test(cmd)) return "destructive";
1296
1376
  if (/\brm\s+[^\n]*(?:-\w*[rRfF]\w*|--recursive|--force)\b/.test(cmd)) return "destructive";
1297
1377
  if (/\brm\s+\S/.test(cmd)) return "destructive";
1298
1378
  if (/\brmdir\b|\bformat\b|\bmkfs\b|\bdd\s+if=|\bshred\b|\bfdisk\b|\bparted\b/.test(cmd)) return "destructive";
@@ -1308,8 +1388,10 @@ function getDangerLevel(toolName, args) {
1308
1388
  if (/\bSet-Content\b|\bOut-File\b|\bAdd-Content\b|\bCopy-Item\b|\bMove-Item\b|\bSet-ItemProperty\b|\bNew-ItemProperty\b/i.test(cmd)) return "write";
1309
1389
  if (/\b(python3?|node|deno|bun|perl|ruby|php|bash|sh|zsh|pwsh|powershell)(\.exe)?\b[^\n]*?\s-(?:Command|EncodedCommand|c|e|p|r)\b/i.test(cmd)) return "write";
1310
1390
  if (/\bcmd(\.exe)?\s+\/[ck]\b/i.test(cmd)) return "write";
1311
- return "safe";
1391
+ if (!/[;&|`$<>\n\r(){}]/.test(cmd) && (/^(?:pwd|ls|cat|head|tail|wc|Get-ChildItem|Get-Location|Get-Content)(?:\s+[\w ./\\:*?=,@'"-]+)?\s*$/i.test(cmd.trim()) || /^echo(?:\s+[\w .,'"-]+)?\s*$/.test(cmd.trim()) || /^(?:node|python3?)\s+(?:-v|--version)\s*$/.test(cmd.trim()) || /^git\s+(?:status|log)(?:\s+(?:--short|--porcelain|--oneline|-\d+))*\s*$/.test(cmd.trim()))) return "safe";
1392
+ return "write";
1312
1393
  }
1394
+ if (toolName === "run_tests") return "write";
1313
1395
  if (toolName === "write_file") return "write";
1314
1396
  if (toolName === "edit_file") return "write";
1315
1397
  if (toolName === "save_last_response") return "write";
@@ -1324,7 +1406,7 @@ function getDangerLevel(toolName, args) {
1324
1406
  if (toolName === "git_commit") return "write";
1325
1407
  if (toolName === "git_status" || toolName === "git_diff" || toolName === "git_log") return "safe";
1326
1408
  if (toolName === "notebook_edit") return "write";
1327
- if (toolName === "read_file" || toolName === "list_dir" || toolName === "grep_files" || toolName === "glob_files" || toolName === "web_fetch" || toolName === "save_memory" || toolName === "ask_user" || toolName === "write_todos" || toolName === "google_search" || toolName === "spawn_agent" || toolName === "run_tests") return "safe";
1409
+ if (toolName === "read_file" || toolName === "list_dir" || toolName === "grep_files" || toolName === "glob_files" || toolName === "web_fetch" || toolName === "save_memory" || toolName === "ask_user" || toolName === "write_todos" || toolName === "google_search" || toolName === "spawn_agent") return "safe";
1328
1410
  return "write";
1329
1411
  }
1330
1412
  function schemaToJsonSchema(schema) {
@@ -4678,7 +4760,7 @@ function clearModelCache(configDir, providerId) {
4678
4760
  delete cache.providers[providerId];
4679
4761
  saveModelCache(configDir, cache);
4680
4762
  }
4681
- function normalizeLiveModels(models, fetchedAt, source = "live") {
4763
+ function normalizeLiveModels(models, fetchedAt, source2 = "live") {
4682
4764
  const seen = /* @__PURE__ */ new Set();
4683
4765
  const normalized = [];
4684
4766
  for (const model of models) {
@@ -4690,7 +4772,7 @@ function normalizeLiveModels(models, fetchedAt, source = "live") {
4690
4772
  contextWindow: model.contextWindow || LIVE_DEFAULT_CONTEXT,
4691
4773
  supportsStreaming: model.supportsStreaming ?? true,
4692
4774
  ...model.supportsThinking !== void 0 ? { supportsThinking: model.supportsThinking } : {},
4693
- source,
4775
+ source: source2,
4694
4776
  available: true,
4695
4777
  metadataUnknown: model.metadataUnknown ?? !model.contextWindow,
4696
4778
  ...fetchedAt ? { fetchedAt } : {}
@@ -5463,26 +5545,33 @@ var SessionManager = class {
5463
5545
  }
5464
5546
  /** P1-PERF-03: `force` 跳过脏检查(用于进程退出、fork 等必须落盘场景) */
5465
5547
  async save(force = false) {
5466
- if (!this._current) return;
5467
- if (this._current.messages.length === 0) return;
5468
- if (!force && !this._current.dirty) return;
5548
+ const session = this._current;
5549
+ if (!session) return;
5550
+ if (session.messages.length === 0) return;
5551
+ if (!force && !session.dirty) return;
5469
5552
  mkdirSync3(this.historyDir, { recursive: true });
5470
- const filePath = join4(this.historyDir, `${this._current.id}.json`);
5471
- const raw = this._current.toJSON();
5553
+ const filePath = join4(this.historyDir, `${session.id}.json`);
5554
+ const raw = session.toJSON();
5472
5555
  const opts = this.redactOptionsForSave();
5473
5556
  const { value: payload, hits } = redactJson(raw, opts);
5474
5557
  this.lastRedactionHits = hits.length;
5475
- atomicWriteFileSync(filePath, JSON.stringify(payload, null, 2));
5476
- this._current.dirty = false;
5558
+ const serialized = JSON.stringify(payload, null, 2);
5559
+ session.dirty = false;
5560
+ try {
5561
+ await atomicWriteFile(filePath, serialized, { mode: 384 });
5562
+ } catch (error) {
5563
+ session.dirty = true;
5564
+ throw error;
5565
+ }
5477
5566
  try {
5478
5567
  updateSessionInIndex(this.historyDir, {
5479
- id: this._current.id,
5480
- provider: this._current.provider,
5481
- model: this._current.model,
5482
- title: this._current.title,
5483
- messageCount: this._current.messages.length,
5484
- created: this._current.created,
5485
- updated: this._current.updated
5568
+ id: session.id,
5569
+ provider: session.provider,
5570
+ model: session.model,
5571
+ title: session.title,
5572
+ messageCount: session.messages.length,
5573
+ created: session.created,
5574
+ updated: session.updated
5486
5575
  }, filePath);
5487
5576
  } catch {
5488
5577
  }
@@ -5633,16 +5722,22 @@ var SessionManager = class {
5633
5722
  };
5634
5723
 
5635
5724
  // src/tools/builtin/bash.ts
5636
- import { spawn as spawn3 } from "child_process";
5725
+ import { spawn as spawn2 } from "child_process";
5637
5726
  import { existsSync as existsSync6, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
5638
- import { platform as platform3 } from "os";
5727
+ import { platform as platform2 } from "os";
5639
5728
  import { resolve } from "path";
5640
5729
 
5641
5730
  // src/tools/undo-stack.ts
5642
- import { readFileSync as readFileSync5, writeFileSync, unlinkSync as unlinkSync3, rmdirSync, existsSync as existsSync5 } from "fs";
5731
+ import { writeFileSync, unlinkSync as unlinkSync3, rmdirSync, existsSync as existsSync5 } from "fs";
5643
5732
  var MAX_UNDO_DEPTH = 20;
5644
5733
  var UndoStack = class {
5645
- stack = [];
5734
+ stateKey = /* @__PURE__ */ Symbol("stack");
5735
+ get stack() {
5736
+ return scopedState(this.stateKey, () => ({ items: [] })).items;
5737
+ }
5738
+ set stack(items) {
5739
+ scopedState(this.stateKey, () => ({ items: [] })).items = items;
5740
+ }
5646
5741
  /**
5647
5742
  * 在执行文件写入操作之前调用,保存当前状态。
5648
5743
  * @param filePath 将要被写入的文件路径
@@ -5651,11 +5746,7 @@ var UndoStack = class {
5651
5746
  push(filePath, description) {
5652
5747
  let previousContent = null;
5653
5748
  if (existsSync5(filePath)) {
5654
- try {
5655
- previousContent = readFileSync5(filePath, "utf-8");
5656
- } catch {
5657
- return;
5658
- }
5749
+ previousContent = readSnapshot(filePath);
5659
5750
  }
5660
5751
  this.pushEntry({
5661
5752
  filePath,
@@ -5692,7 +5783,7 @@ var UndoStack = class {
5692
5783
  /** 内部统一入栈方法,含溢出裁剪 */
5693
5784
  pushEntry(entry) {
5694
5785
  this.stack.push(entry);
5695
- if (this.stack.length > MAX_UNDO_DEPTH) {
5786
+ while (this.stack.length > MAX_UNDO_DEPTH || this.stack.reduce((n, e) => n + Buffer.byteLength(e.previousContent ?? ""), 0) > MAX_SNAPSHOT_TOTAL_BYTES) {
5696
5787
  this.stack.shift();
5697
5788
  }
5698
5789
  }
@@ -5753,186 +5844,17 @@ var UndoStack = class {
5753
5844
  };
5754
5845
  var undoStack = new UndoStack();
5755
5846
 
5756
- // src/core/interrupt.ts
5757
- var controller = new AbortController();
5758
- function currentAbortSignal() {
5759
- return controller.signal;
5760
- }
5761
-
5762
- // src/tools/session-context.ts
5763
- import { AsyncLocalStorage } from "async_hooks";
5764
- var als = new AsyncLocalStorage();
5765
- var DEFAULT_SESSION_KEY = "__default__";
5766
- function getCurrentSessionKey() {
5767
- const ctx = als.getStore();
5768
- if (!ctx || !ctx.sessionKey) return DEFAULT_SESSION_KEY;
5769
- return ctx.sessionKey;
5770
- }
5771
- function runWithSessionKey(sessionKey, fn) {
5772
- const key = sessionKey && sessionKey.length > 0 ? sessionKey : DEFAULT_SESSION_KEY;
5773
- return als.run({ sessionKey: key }, fn);
5774
- }
5775
-
5776
- // src/tools/win-shell.ts
5777
- import { platform } from "os";
5778
- import { spawn } from "child_process";
5779
- var IS_WINDOWS = platform() === "win32";
5780
- var WIN_UTF8_PREAMBLE = "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; $OutputEncoding = [System.Text.Encoding]::UTF8; ";
5781
- function shellInvocation(command) {
5782
- if (IS_WINDOWS) {
5783
- return {
5784
- shell: "powershell.exe",
5785
- args: ["-NoProfile", "-NonInteractive", "-Command", WIN_UTF8_PREAMBLE + command]
5786
- };
5787
- }
5788
- return { shell: process.env["SHELL"] ?? "/bin/bash", args: ["-c", command] };
5789
- }
5790
- function decodeWindowsBuffer(buf) {
5791
- const utf8 = buf.toString("utf-8");
5792
- if (!utf8.includes("\uFFFD")) return utf8;
5793
- try {
5794
- return new TextDecoder("gbk").decode(buf);
5795
- } catch {
5796
- return utf8;
5797
- }
5798
- }
5799
- var GBK_MOJIBAKE_FINGERPRINTS = [
5800
- "\uFFFD",
5801
- "\u9215",
5802
- "\u951B",
5803
- "\u9983",
5804
- "\u9241",
5805
- "\u6D93",
5806
- "\u935B",
5807
- "\u9428",
5808
- "\u7EFE",
5809
- "\u95AB",
5810
- "\u9593",
5811
- "\u95BF",
5812
- "\u68E3",
5813
- "\u95B4",
5814
- "\u5A11",
5815
- "\u95B8",
5816
- "\u95BB",
5817
- "\u7F01",
5818
- "\u95C1"
5819
- ];
5820
- var gbkReverse = null;
5821
- function gbkReverseMap() {
5822
- if (gbkReverse) return gbkReverse;
5823
- const map = /* @__PURE__ */ new Map();
5824
- try {
5825
- const dec = new TextDecoder("gbk");
5826
- for (let hi = 129; hi <= 254; hi++) {
5827
- for (let lo = 64; lo <= 254; lo++) {
5828
- if (lo === 127) continue;
5829
- const ch = dec.decode(new Uint8Array([hi, lo]));
5830
- if (ch.length === 1 && ch !== "\uFFFD" && !map.has(ch)) map.set(ch, [hi, lo]);
5831
- }
5832
- }
5833
- } catch {
5834
- }
5835
- gbkReverse = map;
5836
- return map;
5837
- }
5838
- function decodeRunAsChinese(bytes) {
5839
- for (let start = 0; start <= 2 && start < bytes.length; start++) {
5840
- for (let end = 0; end <= 2 && bytes.length - end > start; end++) {
5841
- const slice = bytes.slice(start, bytes.length - end);
5842
- if (slice.length < 6) continue;
5843
- try {
5844
- const out = new TextDecoder("utf-8", { fatal: true }).decode(new Uint8Array(slice));
5845
- if (/[一-鿿]/.test(out)) return out;
5846
- } catch {
5847
- }
5848
- }
5849
- }
5850
- return null;
5851
- }
5852
- function recoverGbkMojibake(text) {
5853
- if (!text) return null;
5854
- const map = gbkReverseMap();
5855
- if (map.size === 0) return null;
5856
- let run = [];
5857
- let best = null;
5858
- const flush2 = () => {
5859
- if (run.length >= 4) {
5860
- const bytes = [];
5861
- for (const p of run) bytes.push(p[0], p[1]);
5862
- const out = decodeRunAsChinese(bytes);
5863
- if (out && (!best || out.length > best.length)) best = out;
5864
- }
5865
- run = [];
5866
- };
5867
- for (const ch of text) {
5868
- const pair = ch.codePointAt(0) < 128 ? void 0 : map.get(ch);
5869
- if (pair) run.push(pair);
5870
- else flush2();
5871
- }
5872
- flush2();
5873
- return best;
5874
- }
5875
- function looksLikeGbkMojibake(text) {
5876
- if (recoverGbkMojibake(text) !== null) return true;
5877
- let hits = 0;
5878
- for (const ch of GBK_MOJIBAKE_FINGERPRINTS) {
5879
- if (text.includes(ch) && ++hits >= 2) return true;
5880
- }
5881
- return false;
5882
- }
5883
- function killProcessTree(proc) {
5884
- if (proc.killed || proc.exitCode !== null) return;
5885
- if (IS_WINDOWS && proc.pid) {
5886
- try {
5887
- const tk = spawn("taskkill", ["/PID", String(proc.pid), "/T", "/F"], {
5888
- windowsHide: true,
5889
- stdio: "ignore"
5890
- });
5891
- tk.on("exit", () => {
5892
- try {
5893
- proc.kill();
5894
- } catch {
5895
- }
5896
- });
5897
- tk.on("error", () => {
5898
- try {
5899
- proc.kill();
5900
- } catch {
5901
- }
5902
- });
5903
- } catch {
5904
- try {
5905
- proc.kill();
5906
- } catch {
5907
- }
5908
- }
5909
- return;
5910
- }
5911
- try {
5912
- proc.kill("SIGTERM");
5913
- } catch {
5914
- }
5915
- setTimeout(() => {
5916
- if (!proc.killed && proc.exitCode === null) {
5917
- try {
5918
- proc.kill("SIGKILL");
5919
- } catch {
5920
- }
5921
- }
5922
- }, 500).unref();
5923
- }
5924
-
5925
5847
  // src/tools/shell-pool.ts
5926
- import { spawn as spawn2 } from "child_process";
5927
- import { platform as platform2 } from "os";
5928
- var IS_WINDOWS2 = platform2() === "win32";
5848
+ import { spawn } from "child_process";
5849
+ import { platform } from "os";
5850
+ var IS_WINDOWS2 = platform() === "win32";
5929
5851
  var SHELL = IS_WINDOWS2 ? "powershell.exe" : process.env["SHELL"] ?? "/bin/bash";
5930
5852
  var pool = /* @__PURE__ */ new Map();
5931
5853
  var IDLE_EVICT_MS = 5 * 6e4;
5932
5854
  var evictionTimer = null;
5933
5855
  function spawnShell(cwd) {
5934
5856
  const shellArgs = IS_WINDOWS2 ? ["-NoProfile", "-NonInteractive", "-Command", "-"] : ["-s"];
5935
- const proc = spawn2(SHELL, shellArgs, {
5857
+ const proc = spawn(SHELL, shellArgs, {
5936
5858
  cwd,
5937
5859
  windowsHide: true,
5938
5860
  stdio: ["pipe", "pipe", "pipe"]
@@ -6123,7 +6045,7 @@ function cleanupPoolSession(sessionKey) {
6123
6045
  }
6124
6046
 
6125
6047
  // src/tools/builtin/bash.ts
6126
- var IS_WINDOWS3 = platform3() === "win32";
6048
+ var IS_WINDOWS3 = platform2() === "win32";
6127
6049
  var SHELL2 = IS_WINDOWS3 ? "powershell.exe" : process.env["SHELL"] ?? "/bin/bash";
6128
6050
  var cwdBySession = /* @__PURE__ */ new Map();
6129
6051
  function getCwd() {
@@ -6706,7 +6628,7 @@ function pushBashUndoEntries(beforeSnapshot, parsedTargetsBefore, cwd) {
6706
6628
  function runShell(command, opts) {
6707
6629
  return new Promise((resolvePromise) => {
6708
6630
  const shellArgs = IS_WINDOWS3 ? ["-NoProfile", "-NonInteractive", "-Command", WIN_UTF8_PREAMBLE + command] : ["-c", command];
6709
- const child = spawn3(SHELL2, shellArgs, {
6631
+ const child = spawn2(SHELL2, shellArgs, {
6710
6632
  cwd: opts.cwd,
6711
6633
  env: opts.env,
6712
6634
  windowsHide: true,
@@ -6785,16 +6707,16 @@ function updateCwdFromCommand(command, baseCwd) {
6785
6707
  }
6786
6708
 
6787
6709
  // src/tools/builtin/read-file.ts
6788
- import { readFileSync as readFileSync6, existsSync as existsSync7, statSync as statSync3, readdirSync as readdirSync3 } from "fs";
6710
+ import { readFileSync as readFileSync5, existsSync as existsSync7, statSync as statSync3, readdirSync as readdirSync3 } from "fs";
6789
6711
  import { execFileSync } from "child_process";
6790
6712
  import { extname, resolve as resolve2, basename, sep, dirname as dirname2 } from "path";
6791
6713
  import { homedir as homedir2 } from "os";
6792
6714
 
6793
6715
  // src/tools/builtin/notebook-utils.ts
6794
- function cellSourceToString(source) {
6795
- if (!source) return "";
6796
- if (Array.isArray(source)) return source.join("");
6797
- return source;
6716
+ function cellSourceToString(source2) {
6717
+ if (!source2) return "";
6718
+ if (Array.isArray(source2)) return source2.join("");
6719
+ return source2;
6798
6720
  }
6799
6721
  function stringToCellSource(content) {
6800
6722
  if (content.length === 0) return [];
@@ -6849,16 +6771,16 @@ function renderNotebookAsText(nb, filePath) {
6849
6771
  nb.cells.forEach((cell, idx) => {
6850
6772
  const cellNum = idx + 1;
6851
6773
  const type = cell.cell_type;
6852
- const source = cellSourceToString(cell.source);
6774
+ const source2 = cellSourceToString(cell.source);
6853
6775
  if (type === "markdown") {
6854
6776
  lines.push(`## Cell ${cellNum} [markdown]`);
6855
- lines.push(source);
6777
+ lines.push(source2);
6856
6778
  lines.push("");
6857
6779
  } else if (type === "code") {
6858
6780
  const execLabel = cell.execution_count != null ? ` (execution #${cell.execution_count})` : "";
6859
6781
  lines.push(`## Cell ${cellNum} [code${execLabel}]`);
6860
6782
  lines.push("```" + language);
6861
- lines.push(source);
6783
+ lines.push(source2);
6862
6784
  lines.push("```");
6863
6785
  const outputText = extractOutputText(cell.outputs);
6864
6786
  if (outputText) {
@@ -6870,7 +6792,7 @@ function renderNotebookAsText(nb, filePath) {
6870
6792
  lines.push("");
6871
6793
  } else if (type === "raw") {
6872
6794
  lines.push(`## Cell ${cellNum} [raw]`);
6873
- lines.push(source);
6795
+ lines.push(source2);
6874
6796
  lines.push("");
6875
6797
  }
6876
6798
  });
@@ -6895,6 +6817,7 @@ function createCell(cellType, content) {
6895
6817
  // src/tools/builtin/read-file.ts
6896
6818
  var MAX_FILE_BYTES = 10 * 1024 * 1024;
6897
6819
  function getHardRefusal(normalizedPath) {
6820
+ if (isCredentialVault(normalizedPath)) return "[Refused] Credential vault files cannot be read by tools.";
6898
6821
  const home2 = homedir2();
6899
6822
  const p = normalizedPath.toLowerCase();
6900
6823
  const base = basename(normalizedPath).toLowerCase();
@@ -7069,7 +6992,7 @@ var readFileTool = {
7069
6992
  const offsetLine = typeof rawOffset === "number" && Number.isFinite(rawOffset) && rawOffset > 0 ? Math.floor(rawOffset) : void 0;
7070
6993
  const limitLines = typeof rawLimit === "number" && Number.isFinite(rawLimit) && rawLimit > 0 ? Math.floor(rawLimit) : void 0;
7071
6994
  if (!filePath) throw new ToolError("read_file", "path is required");
7072
- const normalizedPath = resolve2(filePath);
6995
+ const normalizedPath = canonicalPath(filePath);
7073
6996
  const refusal = getHardRefusal(normalizedPath);
7074
6997
  if (refusal) return refusal;
7075
6998
  if (!existsSync7(normalizedPath)) {
@@ -7136,7 +7059,7 @@ Suggestion: read existing text versions (.md / .txt) in the project, or install
7136
7059
  }
7137
7060
  if (ext === ".ipynb") {
7138
7061
  try {
7139
- const raw = readFileSync6(normalizedPath, "utf-8");
7062
+ const raw = readFileSync5(normalizedPath, "utf-8");
7140
7063
  const nb = parseNotebook(raw);
7141
7064
  return renderNotebookAsText(nb, filePath);
7142
7065
  } catch (err) {
@@ -7154,8 +7077,8 @@ Do NOT claim you inspected this image \u2014 you have not seen it.`;
7154
7077
  return `[Binary file: ${filePath} (${ext})]
7155
7078
  This is a binary file and cannot be read as text. If there is a text version (.md / .txt) in the project, please read that instead.`;
7156
7079
  }
7157
- const { readFile: readFile2 } = await import("fs/promises");
7158
- const buf = size > 1048576 ? await readFile2(normalizedPath) : readFileSync6(normalizedPath);
7080
+ const { readFile } = await import("fs/promises");
7081
+ const buf = size > 1048576 ? await readFile(normalizedPath) : readFileSync5(normalizedPath);
7159
7082
  if (encoding === "base64") {
7160
7083
  return `[File: ${filePath} | base64]
7161
7084
 
@@ -7193,12 +7116,12 @@ ${content}`;
7193
7116
  };
7194
7117
 
7195
7118
  // src/tools/builtin/write-file.ts
7196
- import { appendFileSync, mkdirSync as mkdirSync6, readFileSync as readFileSync10, statSync as statSync4 } from "fs";
7119
+ import { appendFileSync, mkdirSync as mkdirSync6, readFileSync as readFileSync8, statSync as statSync4 } from "fs";
7197
7120
  import { dirname as dirname5 } from "path";
7198
7121
 
7199
7122
  // src/tools/executor.ts
7200
7123
  import chalk3 from "chalk";
7201
- import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
7124
+ import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
7202
7125
  import { tmpdir } from "os";
7203
7126
 
7204
7127
  // src/core/readline-internal.ts
@@ -7438,7 +7361,7 @@ function simpleDiff(oldLines, newLines) {
7438
7361
  // src/tools/hooks.ts
7439
7362
  import { execSync } from "child_process";
7440
7363
  import { createHash } from "crypto";
7441
- import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync7 } from "fs";
7364
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync6 } from "fs";
7442
7365
  import { join as join5 } from "path";
7443
7366
  var TRUST_FILE = "hooks-trust.json";
7444
7367
  var DEFAULT_TIMEOUT_MS = 5e3;
@@ -7454,7 +7377,7 @@ function loadHookTrustStore(configDir) {
7454
7377
  const file = hookTrustPath(configDir);
7455
7378
  if (!existsSync8(file)) return { records: [] };
7456
7379
  try {
7457
- const parsed = JSON.parse(readFileSync7(file, "utf-8"));
7380
+ const parsed = JSON.parse(readFileSync6(file, "utf-8"));
7458
7381
  return { records: Array.isArray(parsed.records) ? parsed.records : [] };
7459
7382
  } catch {
7460
7383
  return { records: [] };
@@ -7468,9 +7391,9 @@ function hashHook(event, command) {
7468
7391
  return createHash("sha256").update(`${event}
7469
7392
  ${command}`, "utf-8").digest("hex");
7470
7393
  }
7471
- function isTrusted(source, event, hash, store) {
7472
- if (source === "user" || source === "managed") return true;
7473
- return store.records.some((r) => r.source === source && r.event === event && r.hash === hash);
7394
+ function isTrusted(source2, event, hash, store) {
7395
+ if (source2 === "user" || source2 === "managed") return true;
7396
+ return store.records.some((r) => r.source === source2 && r.event === event && r.hash === hash);
7474
7397
  }
7475
7398
  function listHooks(config, configDir) {
7476
7399
  if (!config || config.enabled === false) return [];
@@ -7478,15 +7401,15 @@ function listHooks(config, configDir) {
7478
7401
  const out = [];
7479
7402
  const push = (event, hook, index) => {
7480
7403
  if (!hook.command) return;
7481
- const source = hook.source ?? "user";
7404
+ const source2 = hook.source ?? "user";
7482
7405
  const hash = hashHook(event, hook.command);
7483
7406
  out.push({
7484
7407
  id: `${event}:${index}`,
7485
7408
  event,
7486
7409
  command: hook.command,
7487
- source,
7410
+ source: source2,
7488
7411
  hash,
7489
- trusted: isTrusted(source, event, hash, store),
7412
+ trusted: isTrusted(source2, event, hash, store),
7490
7413
  disabled: hook.disabled === true,
7491
7414
  description: hook.description
7492
7415
  });
@@ -7562,15 +7485,15 @@ function runLifecycleHooks(config, event, payload, opts) {
7562
7485
  for (let i = 0; i < hooks.length; i++) {
7563
7486
  const hook = hooks[i];
7564
7487
  if (!hook.command || hook.disabled) continue;
7565
- const source = hook.source ?? "user";
7488
+ const source2 = hook.source ?? "user";
7566
7489
  const hash = hashHook(event, hook.command);
7567
7490
  const descriptor = {
7568
7491
  id: `${event}:${i}`,
7569
7492
  event,
7570
7493
  command: hook.command,
7571
- source,
7494
+ source: source2,
7572
7495
  hash,
7573
- trusted: isTrusted(source, event, hash, store),
7496
+ trusted: isTrusted(source2, event, hash, store),
7574
7497
  disabled: false,
7575
7498
  description: hook.description
7576
7499
  };
@@ -7719,7 +7642,8 @@ var READ_ONLY_TOOLS = /* @__PURE__ */ new Set([
7719
7642
  "recall_memory"
7720
7643
  ]);
7721
7644
  function normalizePathForPermission(value) {
7722
- return value.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
7645
+ const normalized = value.replace(/\\/g, "/").replace(/\/+$/, "");
7646
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
7723
7647
  }
7724
7648
  function canonicalizeWithNearestParent(value) {
7725
7649
  const missing = [];
@@ -7807,7 +7731,7 @@ function profileHardDecision(profileName, toolName, args, dangerLevel, ctx) {
7807
7731
  return { action: "deny", reason: "file write target is outside workspace/temp roots", profileName, hardBoundary: true };
7808
7732
  }
7809
7733
  }
7810
- if (toolName === "bash" || toolName === "run_interactive" || toolName === "task_create") {
7734
+ if (toolName === "bash" || toolName === "run_interactive" || toolName === "task_create" || toolName === "run_tests") {
7811
7735
  return {
7812
7736
  action: "confirm",
7813
7737
  reason: "shell/process writes cannot be proven to stay inside workspace/temp roots",
@@ -7909,9 +7833,9 @@ function checkNetworkPolicy(toolName, args, policy) {
7909
7833
  if (!policy?.enabled) return null;
7910
7834
  let action = networkToolAction(toolName, policy);
7911
7835
  let reason = "network access requires confirmation";
7912
- if (toolName === "bash") {
7836
+ if (["bash", "run_tests", "run_interactive", "task_create"].includes(toolName)) {
7913
7837
  const command = String(args["command"] ?? "");
7914
- if (!isShellNetworkCommand(command)) return null;
7838
+ if (toolName === "bash" && !isShellNetworkCommand(command) && dangerIsReadOnly(command)) return null;
7915
7839
  action = policy.tools?.shell ?? policy.defaultAction ?? "confirm";
7916
7840
  reason = "shell command may access the network";
7917
7841
  }
@@ -7947,6 +7871,9 @@ function checkNetworkPolicy(toolName, args, policy) {
7947
7871
  if (!permissionAction) return null;
7948
7872
  return { action: permissionAction, reason, profileName: "networkPolicy" };
7949
7873
  }
7874
+ function dangerIsReadOnly(command) {
7875
+ return /^(?:pwd|echo [\w .-]+|git status(?: --short)?)$/.test(command.trim());
7876
+ }
7950
7877
 
7951
7878
  // src/tools/truncate.ts
7952
7879
  var DEFAULT_MAX_TOOL_OUTPUT_CHARS = 12e3;
@@ -8173,8 +8100,7 @@ function getTopUsedTools(limit = 5) {
8173
8100
  }
8174
8101
 
8175
8102
  // src/tools/action-classifier.ts
8176
- import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
8177
- import { isAbsolute as isAbsolute2, join as join7, resolve as resolve4 } from "path";
8103
+ import { isAbsolute as isAbsolute2, resolve as resolve4 } from "path";
8178
8104
  var RECENT_DENIED_LIMIT = 50;
8179
8105
  var recentlyDenied = [];
8180
8106
  function recordRecentlyDeniedAutoAction(call, classification) {
@@ -8194,7 +8120,8 @@ function clearRecentlyDeniedAutoActions() {
8194
8120
  recentlyDenied.splice(0, recentlyDenied.length);
8195
8121
  }
8196
8122
  function normalizePath(value) {
8197
- return value.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
8123
+ const normalized = canonicalPath(value).replace(/\\/g, "/").replace(/\/+$/, "");
8124
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
8198
8125
  }
8199
8126
  function isInside(root, target) {
8200
8127
  const absoluteRoot = resolve4(root);
@@ -8207,56 +8134,6 @@ function getPathArg(call) {
8207
8134
  const raw = call.arguments.path ?? call.arguments.filePath ?? call.arguments.outputPath ?? call.arguments.file;
8208
8135
  return typeof raw === "string" && raw.trim() ? raw.trim() : null;
8209
8136
  }
8210
- function splitShellWords(command) {
8211
- const out = [];
8212
- const re = /"([^"]*)"|'([^']*)'|([^\s]+)/g;
8213
- let m;
8214
- while (m = re.exec(command)) out.push(m[1] ?? m[2] ?? m[3] ?? "");
8215
- return out.filter(Boolean);
8216
- }
8217
- function readPackageManifest(root) {
8218
- const p = join7(root, "package.json");
8219
- if (!existsSync10(p)) return null;
8220
- try {
8221
- return JSON.parse(readFileSync8(p, "utf-8"));
8222
- } catch {
8223
- return null;
8224
- }
8225
- }
8226
- function manifestHasDependency(root, name) {
8227
- const manifest = readPackageManifest(root);
8228
- if (!manifest) return false;
8229
- for (const key of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
8230
- const deps = manifest[key];
8231
- if (deps && typeof deps === "object" && Object.prototype.hasOwnProperty.call(deps, name)) return true;
8232
- }
8233
- return false;
8234
- }
8235
- function lockfileMentions(root, name) {
8236
- for (const file of ["package-lock.json", "pnpm-lock.yaml", "yarn.lock"]) {
8237
- const p = join7(root, file);
8238
- if (!existsSync10(p)) continue;
8239
- try {
8240
- if (readFileSync8(p, "utf-8").includes(name)) return true;
8241
- } catch {
8242
- }
8243
- }
8244
- return false;
8245
- }
8246
- function getInstallPackages(command) {
8247
- const words = splitShellWords(command);
8248
- const cmd = words[0]?.toLowerCase();
8249
- const sub = words[1]?.toLowerCase();
8250
- if (!cmd || !sub) return [];
8251
- const isInstall = cmd === "npm" && (sub === "install" || sub === "i" || sub === "add") || (cmd === "pnpm" || cmd === "yarn") && (sub === "add" || sub === "install");
8252
- if (!isInstall) return [];
8253
- return words.slice(2).filter((w) => !w.startsWith("-")).filter((w) => w !== "." && w !== "./").map((w) => w.replace(/^@([^/]+)\/([^@]+)(?:@.+)?$/, "@$1/$2").replace(/^([^@]+)@.+$/, "$1"));
8254
- }
8255
- function isPlainGitPush(command) {
8256
- if (!/^\s*git\s+push\b/i.test(command)) return false;
8257
- if (/--force|-f\b|\+[^\s]+/i.test(command)) return false;
8258
- return !/[;&|`$<>\n\r]/.test(command);
8259
- }
8260
8137
  function hasSecretExfiltrationShape(command) {
8261
8138
  return /(?:\.env|config\.json|id_rsa|id_ed25519|AWS_SECRET|AICLI_API_KEY|OPENAI_API_KEY|ANTHROPIC_API_KEY)/i.test(command) && /\b(curl|wget|scp|rsync|nc|netcat|Invoke-WebRequest|Invoke-RestMethod)\b/i.test(command);
8262
8139
  }
@@ -8278,7 +8155,7 @@ var RuleBasedActionClassifier = class {
8278
8155
  }
8279
8156
  return { decision: "confirm", reason: "destructive actions require confirmation", ruleId: "auto.destructive-confirm", confidence: "high" };
8280
8157
  }
8281
- classifyShell(call, dangerLevel, ctx) {
8158
+ classifyShell(call, dangerLevel, _ctx) {
8282
8159
  const command = String(call.arguments.command ?? "");
8283
8160
  if (/\bcurl\b[^\n|;&]*\|\s*(?:sudo\s+)?(?:bash|sh|zsh)\b/i.test(command)) {
8284
8161
  return { decision: "deny", reason: "curl pipe shell execution is blocked in auto mode", ruleId: "deny.curl-pipe-shell", confidence: "high" };
@@ -8304,18 +8181,7 @@ var RuleBasedActionClassifier = class {
8304
8181
  if (/\b(npx|pnpm\s+dlx|yarn\s+dlx)\s+(claude|codex|aider|cursor-agent|gemini)\b/i.test(command)) {
8305
8182
  return { decision: "confirm", reason: "third-party agent loop requires confirmation", ruleId: "confirm.third-party-agent", confidence: "high" };
8306
8183
  }
8307
- if (isPlainGitPush(command)) {
8308
- return { decision: "auto-approve", reason: "plain git push without force", ruleId: "auto.git-push", confidence: "high" };
8309
- }
8310
- const pkgs = getInstallPackages(command);
8311
- if (pkgs.length > 0) {
8312
- const allDeclared = pkgs.every((p) => manifestHasDependency(ctx.workspaceRoot, p) || lockfileMentions(ctx.workspaceRoot, p));
8313
- return allDeclared ? { decision: "auto-approve", reason: "installing dependencies already declared in manifest/lockfile", ruleId: "auto.declared-dependency-install", confidence: "medium" } : { decision: "confirm", reason: "dependency install includes undeclared packages", ruleId: "confirm.undeclared-dependency-install", confidence: "medium" };
8314
- }
8315
- if (/\b(curl|wget|Invoke-WebRequest|Invoke-RestMethod)\b/i.test(command) && !/[>|;&`$]/.test(command)) {
8316
- return { decision: "auto-approve", reason: "read-only HTTP shell command", ruleId: "auto.readonly-http-shell", confidence: "medium" };
8317
- }
8318
- if (dangerLevel === "safe") {
8184
+ if (dangerLevel === "safe" && getDangerLevel(call.name, call.arguments) === "safe") {
8319
8185
  return { decision: "auto-approve", reason: "safe shell action", ruleId: "auto.safe-shell", confidence: "medium" };
8320
8186
  }
8321
8187
  return { decision: "confirm", reason: "shell action is not proven low-risk", ruleId: "confirm.shell-default", confidence: "medium" };
@@ -8784,10 +8650,10 @@ var ToolExecutor = class {
8784
8650
  if (call.name === "write_file") {
8785
8651
  const { path: filePath, content: chunk, append } = resolveWriteFileArgs(call.arguments);
8786
8652
  if (!filePath) return;
8787
- if (existsSync11(filePath)) {
8653
+ if (existsSync10(filePath)) {
8788
8654
  let oldContent;
8789
8655
  try {
8790
- oldContent = readFileSync9(filePath, "utf-8");
8656
+ oldContent = readFileSync7(filePath, "utf-8");
8791
8657
  } catch {
8792
8658
  return;
8793
8659
  }
@@ -8811,7 +8677,7 @@ var ToolExecutor = class {
8811
8677
  }
8812
8678
  } else if (call.name === "edit_file") {
8813
8679
  const filePath = String(call.arguments["path"] ?? "");
8814
- if (!filePath || !existsSync11(filePath)) return;
8680
+ if (!filePath || !existsSync10(filePath)) return;
8815
8681
  const oldStr = call.arguments["old_str"];
8816
8682
  const newStr = call.arguments["new_str"];
8817
8683
  if (oldStr !== void 0) {
@@ -8838,7 +8704,7 @@ var ToolExecutor = class {
8838
8704
  const to = Number(call.arguments["delete_to_line"] ?? from);
8839
8705
  let fileContent;
8840
8706
  try {
8841
- fileContent = readFileSync9(filePath, "utf-8");
8707
+ fileContent = readFileSync7(filePath, "utf-8");
8842
8708
  } catch {
8843
8709
  return;
8844
8710
  }
@@ -8934,7 +8800,12 @@ import { resolve as resolve5, sep as sep2, basename as basename3 } from "path";
8934
8800
  import { homedir as homedir4 } from "os";
8935
8801
  var home = homedir4();
8936
8802
  function norm(p) {
8937
- const abs = resolve5(p);
8803
+ let abs;
8804
+ try {
8805
+ abs = canonicalPath(p);
8806
+ } catch {
8807
+ abs = resolve5(p);
8808
+ }
8938
8809
  return process.platform === "win32" ? abs.toLowerCase() : abs;
8939
8810
  }
8940
8811
  function homeRel(p) {
@@ -8948,6 +8819,7 @@ function homeRel(p) {
8948
8819
  }
8949
8820
  function classifyWritePath(path3) {
8950
8821
  if (!path3) return { sensitive: false };
8822
+ if (isCredentialVault(path3)) return { sensitive: true, reason: "credential vault" };
8951
8823
  const abs = norm(path3);
8952
8824
  const base = basename3(abs);
8953
8825
  const rel = homeRel(path3);
@@ -9030,7 +8902,7 @@ function countTextLines(text) {
9030
8902
 
9031
8903
  // src/tools/builtin/write-file.ts
9032
8904
  function countFileLines(filePath) {
9033
- return countTextLines(readFileSync10(filePath, "utf-8"));
8905
+ return countTextLines(readFileSync8(filePath, "utf-8"));
9034
8906
  }
9035
8907
  var writeFileTool = {
9036
8908
  definition: {
@@ -9094,7 +8966,7 @@ Do NOT split a long document into many write_file(append=true) calls. That patte
9094
8966
  const mode = appendMode ? "appended" : "written";
9095
8967
  void (async () => {
9096
8968
  try {
9097
- const { updateFile } = await import("./indexer-FPKY7LCT.js");
8969
+ const { updateFile } = await import("./indexer-VRXDZ74A.js");
9098
8970
  await updateFile(process.cwd(), filePath);
9099
8971
  } catch {
9100
8972
  }
@@ -9113,7 +8985,7 @@ Do NOT split a long document into many write_file(append=true) calls. That patte
9113
8985
  };
9114
8986
 
9115
8987
  // src/tools/builtin/edit-file.ts
9116
- import { readFileSync as readFileSync11, existsSync as existsSync12 } from "fs";
8988
+ import { readFileSync as readFileSync9, existsSync as existsSync11 } from "fs";
9117
8989
 
9118
8990
  // src/tools/builtin/patch-apply.ts
9119
8991
  function parseUnifiedDiff(patch) {
@@ -9481,7 +9353,7 @@ Note: Path can be absolute or relative to cwd.`,
9481
9353
  const filePath = String(args["path"] ?? args["file"] ?? "");
9482
9354
  const encoding = args["encoding"] ?? "utf-8";
9483
9355
  if (!filePath) throw new ToolError("edit_file", "path is required");
9484
- if (!existsSync12(filePath)) throw new ToolError("edit_file", `File not found: ${filePath}`);
9356
+ if (!existsSync11(filePath)) throw new ToolError("edit_file", `File not found: ${filePath}`);
9485
9357
  const verdict = classifyWritePath(filePath);
9486
9358
  if (verdict.sensitive && subAgentGuard.active) {
9487
9359
  throw new ToolError(
@@ -9489,7 +9361,7 @@ Note: Path can be absolute or relative to cwd.`,
9489
9361
  `Refused: sub-agents cannot edit sensitive paths \u2014 ${verdict.reason}. If this is genuinely needed, ask the parent agent to perform the edit so the user can approve it.`
9490
9362
  );
9491
9363
  }
9492
- const original = readFileSync11(filePath, encoding);
9364
+ const original = readFileSync9(filePath, encoding);
9493
9365
  if (args["patch"] !== void 0) {
9494
9366
  const patchText = String(args["patch"] ?? "");
9495
9367
  const stopOnError = args["stop_on_error"] !== false;
@@ -9666,8 +9538,8 @@ function truncatePreview(str, maxLen = 80) {
9666
9538
  }
9667
9539
 
9668
9540
  // src/tools/builtin/list-dir.ts
9669
- import { readdirSync as readdirSync4, statSync as statSync5, existsSync as existsSync13 } from "fs";
9670
- import { join as join8, basename as basename4 } from "path";
9541
+ import { readdirSync as readdirSync4, statSync as statSync5, existsSync as existsSync12 } from "fs";
9542
+ import { join as join7, basename as basename4 } from "path";
9671
9543
  var listDirTool = {
9672
9544
  definition: {
9673
9545
  name: "list_dir",
@@ -9689,7 +9561,7 @@ var listDirTool = {
9689
9561
  async execute(args) {
9690
9562
  const dirPath = String(args["path"] ?? process.cwd());
9691
9563
  const recursive = Boolean(args["recursive"] ?? false);
9692
- if (!existsSync13(dirPath)) {
9564
+ if (!existsSync12(dirPath)) {
9693
9565
  const targetName = basename4(dirPath).toLowerCase();
9694
9566
  const cwd = process.cwd();
9695
9567
  const suggestions = [];
@@ -9747,11 +9619,11 @@ function listRecursive(basePath, indent, recursive, lines) {
9747
9619
  if (entry.isDirectory()) {
9748
9620
  lines.push(`${indent}\u{1F4C1} ${entry.name}/`);
9749
9621
  if (recursive) {
9750
- listRecursive(join8(basePath, entry.name), indent + " ", true, lines);
9622
+ listRecursive(join7(basePath, entry.name), indent + " ", true, lines);
9751
9623
  }
9752
9624
  } else {
9753
9625
  try {
9754
- const stat = statSync5(join8(basePath, entry.name));
9626
+ const stat = statSync5(join7(basePath, entry.name));
9755
9627
  const size = formatSize(stat.size);
9756
9628
  lines.push(`${indent}\u{1F4C4} ${entry.name} (${size})`);
9757
9629
  } catch {
@@ -9766,10 +9638,77 @@ function formatSize(bytes) {
9766
9638
  return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
9767
9639
  }
9768
9640
 
9641
+ // src/tools/regex-search.ts
9642
+ import { Worker } from "worker_threads";
9643
+ var source = String.raw`
9644
+ const { parentPort, workerData: d } = require('node:worker_threads');
9645
+ const fs = require('node:fs');
9646
+ let re;
9647
+ try { re = new RegExp(d.pattern, d.ignoreCase ? 'i' : ''); }
9648
+ catch { re = new RegExp(d.pattern.replace(/[.*+?^$()|[\]\\]/g, '\\$&'), d.ignoreCase ? 'i' : ''); }
9649
+ const results = [];
9650
+ for (const f of d.files) {
9651
+ if (results.length >= d.maxResults) break;
9652
+ let fd;
9653
+ try {
9654
+ fd = fs.openSync(f.fullPath, 'r');
9655
+ const buf = Buffer.alloc(1000001);
9656
+ let size = 0;
9657
+ while (size < buf.length) { const n = fs.readSync(fd, buf, size, buf.length - size, null); if (!n) break; size += n; }
9658
+ if (size > 1000000 || buf.subarray(0, Math.min(size,512)).includes(0)) continue;
9659
+ const lines = buf.subarray(0,size).toString('utf8').split('\n');
9660
+ for (let i=0; i<lines.length && results.length<d.maxResults; i++) {
9661
+ if (!re.test(lines[i])) continue;
9662
+ const match = { file:f.relPath.replace(/\\/g,'/'), lineNumber:i+1, lineText:lines[i].slice(0,10000) };
9663
+ if (d.context) {
9664
+ match.contextBefore=[]; match.contextAfter=[];
9665
+ for(let j=Math.max(0,i-d.context);j<i;j++) match.contextBefore.push([j+1,lines[j].slice(0,10000)]);
9666
+ for(let j=i+1;j<=Math.min(lines.length-1,i+d.context);j++) match.contextAfter.push([j+1,lines[j].slice(0,10000)]);
9667
+ }
9668
+ results.push(match);
9669
+ }
9670
+ } catch {} finally { if (fd !== undefined) fs.closeSync(fd); }
9671
+ }
9672
+ parentPort.postMessage(results);
9673
+ `;
9674
+ var active = 0;
9675
+ async function searchInWorker(files, pattern, ignoreCase, context, maxResults) {
9676
+ const signal = currentAbortSignal();
9677
+ if (signal.aborted) throw new Error("Search cancelled");
9678
+ if (active >= 4) throw new Error("Search capacity reached; wait for another search to finish");
9679
+ active++;
9680
+ let worker;
9681
+ try {
9682
+ return await new Promise((resolve13, reject) => {
9683
+ worker = new Worker(source, { eval: true, workerData: { files, pattern, ignoreCase, context, maxResults }, resourceLimits: { maxOldGenerationSizeMb: 64 } });
9684
+ let settled = false;
9685
+ const finish = (error, results) => {
9686
+ if (settled) return;
9687
+ settled = true;
9688
+ clearTimeout(timer);
9689
+ signal.removeEventListener("abort", abort);
9690
+ if (error) reject(error);
9691
+ else resolve13(results ?? []);
9692
+ };
9693
+ const timer = setTimeout(() => finish(new Error("Search exceeded 2 second CPU/time budget; simplify the pattern or narrow the path")), 2e3);
9694
+ const abort = () => finish(new Error("Search cancelled"));
9695
+ signal.addEventListener("abort", abort, { once: true });
9696
+ if (signal.aborted) abort();
9697
+ worker.once("message", (results) => finish(void 0, results));
9698
+ worker.once("error", (error) => finish(error));
9699
+ worker.once("exit", (code) => {
9700
+ if (!settled) finish(new Error(`Search worker exited (${code})`));
9701
+ });
9702
+ });
9703
+ } finally {
9704
+ if (worker) await worker.terminate();
9705
+ active--;
9706
+ }
9707
+ }
9708
+
9769
9709
  // src/tools/builtin/grep-files.ts
9770
- import { readdirSync as readdirSync5, readFileSync as readFileSync12, statSync as statSync6, existsSync as existsSync14 } from "fs";
9771
- import { readFile } from "fs/promises";
9772
- import { join as join9, relative } from "path";
9710
+ import { readdirSync as readdirSync5, statSync as statSync6, existsSync as existsSync13 } from "fs";
9711
+ import { join as join8, relative } from "path";
9773
9712
  var grepFilesTool = {
9774
9713
  definition: {
9775
9714
  name: "grep_files",
@@ -9817,43 +9756,24 @@ Supports regex. Automatically skips node_modules, dist, .git directories.`,
9817
9756
  const rootPath = String(args["path"] ?? process.cwd());
9818
9757
  const filePattern = args["file_pattern"] ? String(args["file_pattern"]) : void 0;
9819
9758
  const ignoreCase = Boolean(args["ignore_case"] ?? false);
9820
- const contextLines = Math.max(0, Number(args["context_lines"] ?? 0));
9821
- const maxResults = Math.max(1, Number(args["max_results"] ?? 50));
9759
+ const contextLines = Math.min(10, Math.max(0, Number(args["context_lines"]) || 0));
9760
+ const maxResults = Math.min(200, Math.max(1, Number(args["max_results"]) || 50));
9822
9761
  if (!pattern) throw new ToolError("grep_files", "pattern is required");
9823
- if (!existsSync14(rootPath)) throw new ToolError("grep_files", `Path not found: ${rootPath}`);
9762
+ if (!existsSync13(rootPath)) throw new ToolError("grep_files", `Path not found: ${rootPath}`);
9824
9763
  const MAX_PATTERN_LENGTH = 1e3;
9825
- if (pattern.length > MAX_PATTERN_LENGTH) {
9764
+ if (pattern.length > MAX_PATTERN_LENGTH || (filePattern?.length ?? 0) > MAX_PATTERN_LENGTH) {
9826
9765
  throw new ToolError("grep_files", `Pattern too long (${pattern.length} chars, max ${MAX_PATTERN_LENGTH}). Use a shorter pattern.`);
9827
9766
  }
9828
- let regex;
9829
- try {
9830
- regex = new RegExp(pattern, ignoreCase ? "gi" : "g");
9831
- } catch {
9832
- const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9833
- regex = new RegExp(escaped, ignoreCase ? "gi" : "g");
9834
- }
9835
- const results = [];
9836
- const stat = statSync6(rootPath);
9837
- if (stat.isFile()) {
9838
- searchInFile(rootPath, rootPath, regex, contextLines, maxResults, results);
9839
- } else {
9840
- const filePaths = collectFilePaths(rootPath, filePattern, maxResults);
9841
- const BATCH_SIZE = 16;
9842
- for (let i = 0; i < filePaths.length && results.length < maxResults; i += BATCH_SIZE) {
9843
- const batch = filePaths.slice(i, i + BATCH_SIZE);
9844
- const batchResults = await Promise.all(
9845
- batch.map(
9846
- ({ fullPath, relPath }) => searchInFileAsync(fullPath, relPath, regex, contextLines, maxResults - results.length)
9847
- )
9848
- );
9849
- for (const br of batchResults) {
9850
- for (const r of br) {
9851
- if (results.length >= maxResults) break;
9852
- results.push(r);
9853
- }
9854
- }
9767
+ assertReadablePath(rootPath);
9768
+ const paths = statSync6(rootPath).isFile() ? [{ fullPath: rootPath, relPath: rootPath }] : collectFilePaths(rootPath, filePattern, maxResults);
9769
+ const files = paths.filter((f) => {
9770
+ try {
9771
+ return !isCredentialVault(f.fullPath);
9772
+ } catch {
9773
+ return false;
9855
9774
  }
9856
- }
9775
+ });
9776
+ const results = await searchInWorker(files, pattern, ignoreCase, contextLines, maxResults);
9857
9777
  if (results.length === 0) {
9858
9778
  return `No matches found for pattern: ${pattern}
9859
9779
  Searched in: ${rootPath}${filePattern ? `
@@ -9893,8 +9813,23 @@ File filter: ${filePattern}` : ""}`;
9893
9813
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "dist-cjs", "release", ".next", "__pycache__", ".cache", "coverage", ".nyc_output"]);
9894
9814
  var BINARY_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".ico", ".woff", ".woff2", ".ttf", ".eot", ".otf", ".mp3", ".mp4", ".wav", ".zip", ".tar", ".gz", ".rar", ".7z", ".exe", ".dll", ".so", ".dylib", ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx"]);
9895
9815
  function matchesFilePattern(filename, pattern) {
9896
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
9897
- return new RegExp(`^${escaped}$`, "i").test(filename);
9816
+ filename = filename.toLowerCase();
9817
+ pattern = pattern.toLowerCase();
9818
+ let i = 0, j = 0, star = -1, restart = 0;
9819
+ while (i < filename.length) {
9820
+ if (pattern[j] === filename[i]) {
9821
+ i++;
9822
+ j++;
9823
+ } else if (pattern[j] === "*") {
9824
+ star = j++;
9825
+ restart = i;
9826
+ } else if (star >= 0) {
9827
+ j = star + 1;
9828
+ i = ++restart;
9829
+ } else return false;
9830
+ }
9831
+ while (pattern[j] === "*") j++;
9832
+ return j === pattern.length;
9898
9833
  }
9899
9834
  function isBinary(filename) {
9900
9835
  const ext = filename.slice(filename.lastIndexOf(".")).toLowerCase();
@@ -9902,8 +9837,9 @@ function isBinary(filename) {
9902
9837
  }
9903
9838
  function collectFilePaths(rootPath, filePattern, maxFiles) {
9904
9839
  const paths = [];
9905
- function walk(dirPath) {
9906
- if (paths.length >= maxFiles) return;
9840
+ let visited = 0;
9841
+ function walk(dirPath, depth = 0) {
9842
+ if (paths.length >= maxFiles || depth > 20 || visited >= 2e4) return;
9907
9843
  let entries;
9908
9844
  try {
9909
9845
  entries = readdirSync5(dirPath, { withFileTypes: true });
@@ -9911,14 +9847,14 @@ function collectFilePaths(rootPath, filePattern, maxFiles) {
9911
9847
  return;
9912
9848
  }
9913
9849
  for (const entry of entries) {
9914
- if (paths.length >= maxFiles) return;
9850
+ if (paths.length >= maxFiles || ++visited > 2e4) return;
9915
9851
  if (entry.isDirectory()) {
9916
9852
  if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
9917
- walk(join9(dirPath, entry.name));
9853
+ walk(join8(dirPath, entry.name), depth + 1);
9918
9854
  } else if (entry.isFile()) {
9919
9855
  if (isBinary(entry.name)) continue;
9920
9856
  if (filePattern && !matchesFilePattern(entry.name, filePattern)) continue;
9921
- const fullPath = join9(dirPath, entry.name);
9857
+ const fullPath = join8(dirPath, entry.name);
9922
9858
  try {
9923
9859
  if (statSync6(fullPath).size > 1e6) continue;
9924
9860
  } catch {
@@ -9931,81 +9867,10 @@ function collectFilePaths(rootPath, filePattern, maxFiles) {
9931
9867
  walk(rootPath);
9932
9868
  return paths;
9933
9869
  }
9934
- async function searchInFileAsync(fullPath, displayPath2, regex, contextLines, maxResults) {
9935
- let content;
9936
- try {
9937
- content = await readFile(fullPath, "utf-8");
9938
- } catch {
9939
- return [];
9940
- }
9941
- const results = [];
9942
- const lines = content.split("\n");
9943
- regex.lastIndex = 0;
9944
- for (let i = 0; i < lines.length && results.length < maxResults; i++) {
9945
- regex.lastIndex = 0;
9946
- if (regex.test(lines[i])) {
9947
- const match = {
9948
- file: displayPath2.replace(/\\/g, "/"),
9949
- lineNumber: i + 1,
9950
- lineText: lines[i]
9951
- };
9952
- if (contextLines > 0) {
9953
- match.contextBefore = [];
9954
- for (let j = Math.max(0, i - contextLines); j < i; j++) {
9955
- match.contextBefore.push([j + 1, lines[j]]);
9956
- }
9957
- match.contextAfter = [];
9958
- for (let j = i + 1; j <= Math.min(lines.length - 1, i + contextLines); j++) {
9959
- match.contextAfter.push([j + 1, lines[j]]);
9960
- }
9961
- }
9962
- results.push(match);
9963
- }
9964
- }
9965
- return results;
9966
- }
9967
- function searchInFile(fullPath, displayPath2, regex, contextLines, maxResults, results) {
9968
- try {
9969
- const stat = statSync6(fullPath);
9970
- if (stat.size > 1e6) return;
9971
- } catch {
9972
- return;
9973
- }
9974
- let content;
9975
- try {
9976
- content = readFileSync12(fullPath, "utf-8");
9977
- } catch {
9978
- return;
9979
- }
9980
- const lines = content.split("\n");
9981
- regex.lastIndex = 0;
9982
- for (let i = 0; i < lines.length; i++) {
9983
- if (results.length >= maxResults) return;
9984
- regex.lastIndex = 0;
9985
- if (regex.test(lines[i])) {
9986
- const result = {
9987
- file: displayPath2,
9988
- lineNumber: i + 1,
9989
- lineText: lines[i].trimEnd()
9990
- };
9991
- if (contextLines > 0) {
9992
- result.contextBefore = [];
9993
- result.contextAfter = [];
9994
- for (let c = Math.max(0, i - contextLines); c < i; c++) {
9995
- result.contextBefore.push([c + 1, lines[c].trimEnd()]);
9996
- }
9997
- for (let c = i + 1; c <= Math.min(lines.length - 1, i + contextLines); c++) {
9998
- result.contextAfter.push([c + 1, lines[c].trimEnd()]);
9999
- }
10000
- }
10001
- results.push(result);
10002
- }
10003
- }
10004
- }
10005
9870
 
10006
9871
  // src/tools/builtin/glob-files.ts
10007
- import { readdirSync as readdirSync6, statSync as statSync7, existsSync as existsSync15 } from "fs";
10008
- import { join as join10, relative as relative2, basename as basename5 } from "path";
9872
+ import { readdirSync as readdirSync6, statSync as statSync7, existsSync as existsSync14 } from "fs";
9873
+ import { join as join9, relative as relative2, basename as basename5 } from "path";
10009
9874
  var globFilesTool = {
10010
9875
  definition: {
10011
9876
  name: "glob_files",
@@ -10038,7 +9903,7 @@ Results sorted by most recent modification time. Automatically skips node_module
10038
9903
  const rootPath = String(args["path"] ?? process.cwd());
10039
9904
  const maxResults = Math.max(1, Number(args["max_results"] ?? 100));
10040
9905
  if (!pattern) throw new ToolError("glob_files", "pattern is required");
10041
- if (!existsSync15(rootPath)) throw new ToolError("glob_files", `Path not found: ${rootPath}`);
9906
+ if (!existsSync14(rootPath)) throw new ToolError("glob_files", `Path not found: ${rootPath}`);
10042
9907
  const regex = globToRegex(pattern);
10043
9908
  const matches2 = [];
10044
9909
  collectMatchingFiles(rootPath, rootPath, regex, matches2, maxResults);
@@ -10115,7 +9980,7 @@ function collectMatchingFiles(dirPath, rootPath, regex, results, maxResults) {
10115
9980
  }
10116
9981
  for (const entry of entries) {
10117
9982
  if (results.length >= maxResults) break;
10118
- const fullPath = join10(dirPath, entry.name);
9983
+ const fullPath = join9(dirPath, entry.name);
10119
9984
  if (entry.isDirectory()) {
10120
9985
  if (SKIP_DIRS2.has(entry.name) || entry.name.startsWith(".")) continue;
10121
9986
  collectMatchingFiles(fullPath, rootPath, regex, results, maxResults);
@@ -10134,9 +9999,9 @@ function collectMatchingFiles(dirPath, rootPath, regex, results, maxResults) {
10134
9999
  }
10135
10000
 
10136
10001
  // src/tools/builtin/run-interactive.ts
10137
- import { spawn as spawn4 } from "child_process";
10138
- import { platform as platform4 } from "os";
10139
- var IS_WINDOWS4 = platform4() === "win32";
10002
+ import { spawn as spawn3 } from "child_process";
10003
+ import { platform as platform3 } from "os";
10004
+ var IS_WINDOWS4 = platform3() === "win32";
10140
10005
  var runInteractiveTool = {
10141
10006
  definition: {
10142
10007
  name: "run_interactive",
@@ -10198,7 +10063,7 @@ var runInteractiveTool = {
10198
10063
  };
10199
10064
  const prefixWarnings = [argsTypeWarning, stdinTypeWarning].filter(Boolean).join("");
10200
10065
  return new Promise((resolve13) => {
10201
- const child = spawn4(executable, cmdArgs.map(String), {
10066
+ const child = spawn3(executable, cmdArgs.map(String), {
10202
10067
  cwd: process.cwd(),
10203
10068
  env,
10204
10069
  stdio: ["pipe", "pipe", "pipe"]
@@ -10468,8 +10333,8 @@ var webFetchTool = {
10468
10333
  if (e.message.startsWith("Blocked:")) throw e;
10469
10334
  throw new NetworkError(`Invalid URL: "${url}"`);
10470
10335
  }
10471
- const controller2 = new AbortController();
10472
- const timeoutId = setTimeout(() => controller2.abort(), 2e4);
10336
+ const controller = new AbortController();
10337
+ const timeoutId = setTimeout(() => controller.abort(), 2e4);
10473
10338
  let rawHtml;
10474
10339
  let finalUrl;
10475
10340
  let contentType;
@@ -10490,7 +10355,7 @@ var webFetchTool = {
10490
10355
  }
10491
10356
  await resolveAndCheck(parsedHop.hostname);
10492
10357
  const r = await fetch(currentUrl, {
10493
- signal: controller2.signal,
10358
+ signal: controller.signal,
10494
10359
  headers: FETCH_HEADERS,
10495
10360
  redirect: "manual",
10496
10361
  // 手动控制重定向
@@ -10853,8 +10718,8 @@ function parseGoogleResults(html, maxResults) {
10853
10718
  return results;
10854
10719
  }
10855
10720
  async function fetchHtml(url) {
10856
- const controller2 = new AbortController();
10857
- const timer = setTimeout(() => controller2.abort(), REQUEST_TIMEOUT_MS);
10721
+ const controller = new AbortController();
10722
+ const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
10858
10723
  try {
10859
10724
  const res = await fetch(url, {
10860
10725
  method: "GET",
@@ -10863,7 +10728,7 @@ async function fetchHtml(url) {
10863
10728
  "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9",
10864
10729
  "Accept-Language": "en-US,en;q=0.9,zh-CN;q=0.8"
10865
10730
  },
10866
- signal: controller2.signal
10731
+ signal: controller.signal
10867
10732
  });
10868
10733
  if (!res.ok) {
10869
10734
  throw new NetworkError(`HTTP ${res.status} ${res.statusText}`, res.status);
@@ -10957,10 +10822,8 @@ Any of these triggers means use save_last_response, NOT write_file:
10957
10822
  };
10958
10823
 
10959
10824
  // src/tools/builtin/save-memory.ts
10960
- import { join as join11 } from "path";
10961
- import { homedir as homedir5 } from "os";
10962
10825
  function getConfigDir() {
10963
- return join11(homedir5(), CONFIG_DIR_NAME);
10826
+ return getExecutionConfigDir();
10964
10827
  }
10965
10828
  function parseSensitivity(value) {
10966
10829
  return value === "low" || value === "medium" || value === "high" ? value : void 0;
@@ -11224,15 +11087,15 @@ var googleSearchTool = {
11224
11087
  url.searchParams.set("cx", cx);
11225
11088
  url.searchParams.set("q", query);
11226
11089
  url.searchParams.set("num", String(numResults));
11227
- const controller2 = new AbortController();
11228
- const timeout = setTimeout(() => controller2.abort(), REQUEST_TIMEOUT_MS2);
11090
+ const controller = new AbortController();
11091
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS2);
11229
11092
  try {
11230
11093
  const response = await fetch(url.toString(), {
11231
11094
  method: "GET",
11232
11095
  headers: {
11233
11096
  "Accept": "application/json"
11234
11097
  },
11235
- signal: controller2.signal
11098
+ signal: controller.signal
11236
11099
  });
11237
11100
  if (!response.ok) {
11238
11101
  const errorBody = await response.text().catch(() => "");
@@ -11316,13 +11179,13 @@ function isToolCapableProvider(provider) {
11316
11179
  }
11317
11180
 
11318
11181
  // src/agents/agent-config.ts
11319
- import { existsSync as existsSync17, readdirSync as readdirSync8, readFileSync as readFileSync14 } from "fs";
11320
- import { join as join13 } from "path";
11321
- import { homedir as homedir6 } from "os";
11182
+ import { existsSync as existsSync16, readdirSync as readdirSync8, readFileSync as readFileSync11 } from "fs";
11183
+ import { join as join11 } from "path";
11184
+ import { homedir as homedir5 } from "os";
11322
11185
 
11323
11186
  // src/plugins/plugin-manager.ts
11324
- import { existsSync as existsSync16, mkdirSync as mkdirSync8, readdirSync as readdirSync7, readFileSync as readFileSync13, realpathSync as realpathSync2, cpSync, rmSync, statSync as statSync8 } from "fs";
11325
- import { basename as basename6, dirname as dirname7, join as join12, relative as relative3, resolve as resolve6, sep as sep3 } from "path";
11187
+ import { existsSync as existsSync15, mkdirSync as mkdirSync8, readdirSync as readdirSync7, readFileSync as readFileSync10, realpathSync as realpathSync2, cpSync, rmSync, statSync as statSync8 } from "fs";
11188
+ import { basename as basename6, dirname as dirname7, join as join10, relative as relative3, resolve as resolve6, sep as sep3 } from "path";
11326
11189
  import { createHash as createHash2 } from "crypto";
11327
11190
  import { z as z2 } from "zod";
11328
11191
  var MANIFEST_RELATIVE = ".aicli-plugin/plugin.json";
@@ -11382,19 +11245,19 @@ var PluginManifestSchema = z2.object({
11382
11245
  permissionHints: z2.array(z2.string()).default([])
11383
11246
  }).strict();
11384
11247
  function pluginRoot(configDir) {
11385
- return join12(configDir, PLUGINS_DIR_NAME);
11248
+ return join10(configDir, PLUGINS_DIR_NAME);
11386
11249
  }
11387
11250
  function pluginManifestPath(pluginDir) {
11388
- return join12(pluginDir, MANIFEST_RELATIVE);
11251
+ return join10(pluginDir, MANIFEST_RELATIVE);
11389
11252
  }
11390
11253
  function statePath(configDir) {
11391
- return join12(pluginRoot(configDir), STATE_FILE);
11254
+ return join10(pluginRoot(configDir), STATE_FILE);
11392
11255
  }
11393
11256
  function loadState(configDir) {
11394
11257
  const file = statePath(configDir);
11395
- if (!existsSync16(file)) return { version: 2, plugins: [] };
11258
+ if (!existsSync15(file)) return { version: 2, plugins: [] };
11396
11259
  try {
11397
- const parsed = JSON.parse(readFileSync13(file, "utf-8"));
11260
+ const parsed = JSON.parse(readFileSync10(file, "utf-8"));
11398
11261
  const rawPlugins = Array.isArray(parsed.plugins) ? parsed.plugins : [];
11399
11262
  return { version: 2, plugins: rawPlugins.map((p) => migrateEntry(p)) };
11400
11263
  } catch {
@@ -11452,7 +11315,7 @@ function extractCommandPaths(command, pluginDir) {
11452
11315
  try {
11453
11316
  const abs = resolve6(resolvedDir, ref);
11454
11317
  if (!abs.startsWith(resolvedDir + sep3)) continue;
11455
- if (existsSync16(abs)) {
11318
+ if (existsSync15(abs)) {
11456
11319
  const real = realpathSync2(abs);
11457
11320
  if (!real.startsWith(resolvedDir + sep3)) continue;
11458
11321
  valid.push(relative3(resolvedDir, real));
@@ -11484,8 +11347,8 @@ function hashPluginContent(pluginDir, manifest) {
11484
11347
  }
11485
11348
  const sorted = [...assetRels].sort();
11486
11349
  const hasher = createHash2("sha256");
11487
- const manifestPath = join12(resolvedDir, MANIFEST_RELATIVE);
11488
- hasher.update(normalizeForHash(readFileSync13(manifestPath)));
11350
+ const manifestPath = join10(resolvedDir, MANIFEST_RELATIVE);
11351
+ hasher.update(normalizeForHash(readFileSync10(manifestPath)));
11489
11352
  for (const rel of sorted) {
11490
11353
  const abs = resolve6(resolvedDir, rel);
11491
11354
  if (!abs.startsWith(resolvedDir + sep3)) {
@@ -11505,7 +11368,7 @@ function hashPluginContent(pluginDir, manifest) {
11505
11368
  if (!realPath.startsWith(resolvedDir + sep3)) {
11506
11369
  throw new Error(`plugin asset symlink escapes plugin dir: ${rel} \u2192 ${realPath}`);
11507
11370
  }
11508
- hasher.update(normalizeForHash(readFileSync13(abs)));
11371
+ hasher.update(normalizeForHash(readFileSync10(abs)));
11509
11372
  }
11510
11373
  return hasher.digest("hex");
11511
11374
  }
@@ -11513,22 +11376,22 @@ function effectiveHash(entry) {
11513
11376
  return entry.contentHash ?? entry.hash ?? "";
11514
11377
  }
11515
11378
  function readPluginManifest(manifestPath) {
11516
- return PluginManifestSchema.parse(JSON.parse(readFileSync13(manifestPath, "utf-8")));
11379
+ return PluginManifestSchema.parse(JSON.parse(readFileSync10(manifestPath, "utf-8")));
11517
11380
  }
11518
11381
  function findPluginSourceDir(inputPath) {
11519
11382
  const abs = resolve6(inputPath);
11520
11383
  const direct = statSync8(abs).isDirectory() ? abs : dirname7(abs);
11521
- if (existsSync16(pluginManifestPath(direct))) return direct;
11522
- const nested = join12(direct, ".aicli-plugin");
11523
- if (existsSync16(join12(nested, "plugin.json"))) return direct;
11384
+ if (existsSync15(pluginManifestPath(direct))) return direct;
11385
+ const nested = join10(direct, ".aicli-plugin");
11386
+ if (existsSync15(join10(nested, "plugin.json"))) return direct;
11524
11387
  throw new Error(`plugin manifest not found: ${MANIFEST_RELATIVE}`);
11525
11388
  }
11526
11389
  function installPlugin(configDir, inputPath) {
11527
11390
  const sourceDir = findPluginSourceDir(inputPath);
11528
11391
  const manifest = readPluginManifest(pluginManifestPath(sourceDir));
11529
- const targetDir = join12(pluginRoot(configDir), manifest.name);
11392
+ const targetDir = join10(pluginRoot(configDir), manifest.name);
11530
11393
  mkdirSync8(pluginRoot(configDir), { recursive: true });
11531
- if (existsSync16(targetDir)) rmSync(targetDir, { recursive: true, force: true });
11394
+ if (existsSync15(targetDir)) rmSync(targetDir, { recursive: true, force: true });
11532
11395
  cpSync(sourceDir, targetDir, { recursive: true });
11533
11396
  const manifestPath = pluginManifestPath(targetDir);
11534
11397
  const contentHash = hashPluginContent(targetDir, manifest);
@@ -11586,18 +11449,18 @@ function getInstalledPlugin(configDir, name) {
11586
11449
  }
11587
11450
  function listInstalledPlugins(configDir) {
11588
11451
  const root = pluginRoot(configDir);
11589
- if (!existsSync16(root)) return [];
11452
+ if (!existsSync15(root)) return [];
11590
11453
  const state2 = loadState(configDir);
11591
11454
  const out = [];
11592
11455
  for (const entry of readdirSync7(root)) {
11593
- const dir = join12(root, entry);
11456
+ const dir = join10(root, entry);
11594
11457
  try {
11595
11458
  if (!statSync8(dir).isDirectory()) continue;
11596
11459
  } catch {
11597
11460
  continue;
11598
11461
  }
11599
11462
  const manifestPath = pluginManifestPath(dir);
11600
- if (!existsSync16(manifestPath)) continue;
11463
+ if (!existsSync15(manifestPath)) continue;
11601
11464
  try {
11602
11465
  const manifest = readPluginManifest(manifestPath);
11603
11466
  const contentHash = hashPluginContent(dir, manifest);
@@ -11637,7 +11500,7 @@ function uniqueDirs(plugin, rels) {
11637
11500
  for (const rel of rels) {
11638
11501
  const abs = resolve6(plugin.dir, rel);
11639
11502
  if (!abs.startsWith(resolve6(plugin.dir))) continue;
11640
- if (existsSync16(abs)) dirs.add(statSync8(abs).isDirectory() ? abs : dirname7(abs));
11503
+ if (existsSync15(abs)) dirs.add(statSync8(abs).isDirectory() ? abs : dirname7(abs));
11641
11504
  }
11642
11505
  return [...dirs];
11643
11506
  }
@@ -11777,13 +11640,13 @@ function normalizeName(name) {
11777
11640
  function isStringArray(value) {
11778
11641
  return Array.isArray(value) && value.every((item) => typeof item === "string");
11779
11642
  }
11780
- function parseAgentConfig(raw, source, path3) {
11643
+ function parseAgentConfig(raw, source2, path3) {
11781
11644
  if (!raw || typeof raw !== "object") return null;
11782
11645
  const obj = raw;
11783
11646
  if (typeof obj.name !== "string" || obj.name.trim() === "") return null;
11784
11647
  const cfg = {
11785
11648
  name: obj.name.trim(),
11786
- source,
11649
+ source: source2,
11787
11650
  path: path3
11788
11651
  };
11789
11652
  if (typeof obj.description === "string") cfg.description = obj.description;
@@ -11799,15 +11662,15 @@ function parseAgentConfig(raw, source, path3) {
11799
11662
  if (typeof obj.contextPolicy === "string") cfg.contextPolicy = obj.contextPolicy;
11800
11663
  return cfg;
11801
11664
  }
11802
- function loadAgentDir(dir, source) {
11803
- if (!existsSync17(dir)) return [];
11665
+ function loadAgentDir(dir, source2) {
11666
+ if (!existsSync16(dir)) return [];
11804
11667
  const out = [];
11805
11668
  for (const file of readdirSync8(dir)) {
11806
11669
  if (!file.endsWith(".json")) continue;
11807
- const path3 = join13(dir, file);
11670
+ const path3 = join11(dir, file);
11808
11671
  try {
11809
- const parsed = JSON.parse(readFileSync14(path3, "utf-8"));
11810
- const cfg = parseAgentConfig(parsed, source, path3);
11672
+ const parsed = JSON.parse(readFileSync11(path3, "utf-8"));
11673
+ const cfg = parseAgentConfig(parsed, source2, path3);
11811
11674
  if (cfg) out.push(cfg);
11812
11675
  } catch {
11813
11676
  }
@@ -11816,8 +11679,8 @@ function loadAgentDir(dir, source) {
11816
11679
  }
11817
11680
  function getAgentDirs(configDir, cwd = process.cwd()) {
11818
11681
  return {
11819
- user: join13(configDir ?? join13(homedir6(), CONFIG_DIR_NAME), "agents"),
11820
- project: join13(cwd, CONFIG_DIR_NAME, "agents")
11682
+ user: join11(configDir ?? join11(homedir5(), CONFIG_DIR_NAME), "agents"),
11683
+ project: join11(cwd, CONFIG_DIR_NAME, "agents")
11821
11684
  };
11822
11685
  }
11823
11686
  function listAgentConfigs(configDir, cwd = process.cwd()) {
@@ -12320,9 +12183,8 @@ var spawnAgentTool = {
12320
12183
  };
12321
12184
 
12322
12185
  // src/tools/builtin/task-manager.ts
12323
- import { spawn as spawn5 } from "child_process";
12186
+ import { spawn as spawn4 } from "child_process";
12324
12187
  import { randomUUID } from "crypto";
12325
- import { platform as platform5 } from "os";
12326
12188
  var MAX_OUTPUT_CHARS = 1e4;
12327
12189
  var MAX_OUTPUT_BYTES = 64 * 1024;
12328
12190
  var MAX_TASKS = 20;
@@ -12341,14 +12203,16 @@ function decodeField(chunks) {
12341
12203
  }
12342
12204
  function appendOutput(task, field, chunk) {
12343
12205
  const chunks = field === "stdout" ? task._stdoutChunks : task._stderrChunks;
12344
- chunks.push(chunk);
12206
+ chunks.push(chunk.length > MAX_OUTPUT_BYTES ? chunk.subarray(-MAX_OUTPUT_BYTES) : chunk);
12345
12207
  capChunks(chunks);
12346
12208
  task[field] = decodeField(chunks);
12347
12209
  }
12348
12210
  function createTask(command, description) {
12211
+ if ([...tasks.values()].filter((t) => t._active && t.owner === getExecutionConfigDir()).length >= 10) throw new Error("Background task limit reached (10 running per user).");
12212
+ if ([...tasks.values()].filter((t) => t._active).length >= MAX_TASKS) throw new Error("Background task limit reached (20 running). Stop a task before creating another.");
12349
12213
  if (tasks.size >= MAX_TASKS) {
12350
12214
  for (const [id2, t] of tasks) {
12351
- if (t.status !== "running") {
12215
+ if (!t._active) {
12352
12216
  tasks.delete(id2);
12353
12217
  break;
12354
12218
  }
@@ -12356,16 +12220,19 @@ function createTask(command, description) {
12356
12220
  }
12357
12221
  const id = randomUUID().slice(0, 8);
12358
12222
  const { shell, args } = shellInvocation(command);
12359
- const proc = spawn5(shell, args, {
12223
+ const proc = spawn4(shell, args, {
12360
12224
  stdio: ["ignore", "pipe", "pipe"],
12361
- detached: false,
12225
+ detached: process.platform !== "win32",
12226
+ windowsHide: true,
12362
12227
  env: { ...process.env, PYTHONUTF8: "1", PYTHONIOENCODING: "utf-8" }
12363
12228
  });
12364
12229
  const task = {
12365
12230
  id,
12231
+ owner: getExecutionConfigDir(),
12366
12232
  command,
12367
12233
  description,
12368
12234
  process: proc,
12235
+ _active: true,
12369
12236
  startTime: Date.now(),
12370
12237
  endTime: null,
12371
12238
  status: "running",
@@ -12378,11 +12245,13 @@ function createTask(command, description) {
12378
12245
  proc.stdout?.on("data", (chunk) => appendOutput(task, "stdout", chunk));
12379
12246
  proc.stderr?.on("data", (chunk) => appendOutput(task, "stderr", chunk));
12380
12247
  proc.on("close", (code) => {
12248
+ task._active = false;
12381
12249
  task.endTime = Date.now();
12382
12250
  task.exitCode = code;
12383
- task.status = code === 0 ? "completed" : "failed";
12251
+ if (task.status !== "stopped") task.status = code === 0 ? "completed" : "failed";
12384
12252
  });
12385
12253
  proc.on("error", (err) => {
12254
+ task._active = false;
12386
12255
  task.endTime = Date.now();
12387
12256
  task.status = "failed";
12388
12257
  task.stderr += `
@@ -12392,43 +12261,20 @@ Process error: ${err.message}`;
12392
12261
  return task;
12393
12262
  }
12394
12263
  function listTasks() {
12395
- return [...tasks.values()].map(
12396
- ({ process: _p, _stdoutChunks: _o, _stderrChunks: _e, ...rest }) => rest
12264
+ return [...tasks.values()].filter((t) => t.owner === getExecutionConfigDir()).map(
12265
+ ({ process: _p, _stdoutChunks: _o, _stderrChunks: _e, _active: _a, ...rest }) => rest
12397
12266
  );
12398
12267
  }
12399
12268
  function getTaskOutput(id) {
12400
12269
  const task = tasks.get(id);
12401
- if (!task) return null;
12270
+ if (!task || task.owner !== getExecutionConfigDir()) return null;
12402
12271
  return { stdout: task.stdout, stderr: task.stderr, status: task.status };
12403
12272
  }
12404
12273
  function stopTask(id) {
12405
12274
  const task = tasks.get(id);
12406
- if (!task || task.status !== "running") return false;
12275
+ if (!task || task.owner !== getExecutionConfigDir() || task.status !== "running") return false;
12407
12276
  try {
12408
- const proc = task.process;
12409
- if (platform5() === "win32" && proc.pid) {
12410
- try {
12411
- spawn5("taskkill", ["/PID", String(proc.pid), "/T", "/F"], {
12412
- windowsHide: true,
12413
- stdio: "ignore"
12414
- });
12415
- } catch {
12416
- }
12417
- try {
12418
- proc.kill();
12419
- } catch {
12420
- }
12421
- } else {
12422
- proc.kill("SIGTERM");
12423
- setTimeout(() => {
12424
- if (proc.exitCode === null && proc.signalCode === null) {
12425
- try {
12426
- proc.kill("SIGKILL");
12427
- } catch {
12428
- }
12429
- }
12430
- }, 3e3).unref();
12431
- }
12277
+ killProcessTree(task.process, true);
12432
12278
  task.status = "stopped";
12433
12279
  task.endTime = Date.now();
12434
12280
  return true;
@@ -12536,14 +12382,14 @@ var taskStopTool = {
12536
12382
 
12537
12383
  // src/tools/builtin/git-tools.ts
12538
12384
  import { execFileSync as execFileSync2 } from "child_process";
12539
- import { existsSync as existsSync18 } from "fs";
12540
- import { join as join14 } from "path";
12385
+ import { existsSync as existsSync17 } from "fs";
12386
+ import { join as join12 } from "path";
12541
12387
  function assertGitRepo(cwd) {
12542
12388
  let dir = cwd;
12543
12389
  const root = dir.split(/[\\/]/)[0] + (dir.includes("\\") ? "\\" : "/");
12544
12390
  while (dir && dir !== root) {
12545
- if (existsSync18(join14(dir, ".git"))) return;
12546
- const parent = join14(dir, "..");
12391
+ if (existsSync17(join12(dir, ".git"))) return;
12392
+ const parent = join12(dir, "..");
12547
12393
  if (parent === dir) break;
12548
12394
  dir = parent;
12549
12395
  }
@@ -12808,7 +12654,7 @@ ${commitOutput.trim()}`;
12808
12654
  };
12809
12655
 
12810
12656
  // src/tools/builtin/notebook-edit.ts
12811
- import { readFileSync as readFileSync15, existsSync as existsSync19 } from "fs";
12657
+ import { readFileSync as readFileSync12, existsSync as existsSync18 } from "fs";
12812
12658
  import { writeFile } from "fs/promises";
12813
12659
  import { resolve as resolve7, extname as extname2 } from "path";
12814
12660
  var notebookEditTool = {
@@ -12863,10 +12709,10 @@ var notebookEditTool = {
12863
12709
  if (extname2(absPath).toLowerCase() !== ".ipynb") {
12864
12710
  throw new ToolError("notebook_edit", "path must point to a .ipynb file");
12865
12711
  }
12866
- if (!existsSync19(absPath)) {
12712
+ if (!existsSync18(absPath)) {
12867
12713
  throw new ToolError("notebook_edit", `Notebook not found: ${filePath}`);
12868
12714
  }
12869
- const raw = readFileSync15(absPath, "utf-8");
12715
+ const raw = readFileSync12(absPath, "utf-8");
12870
12716
  const nb = parseNotebook(raw);
12871
12717
  const cellIdx0 = cellIndexRaw - 1;
12872
12718
  undoStack.push(absPath, `notebook_edit (${action}): ${filePath}`);
@@ -13192,8 +13038,6 @@ ${lines.join("\n")}`;
13192
13038
  };
13193
13039
 
13194
13040
  // src/tools/builtin/recall-memory.ts
13195
- import { join as join15 } from "path";
13196
- import { homedir as homedir7 } from "os";
13197
13041
  function formatHit(h, i) {
13198
13042
  const ts = h.chunk.timestamp.slice(0, 16).replace("T", " ");
13199
13043
  const title = h.chunk.sessionTitle ? ` \xB7 ${h.chunk.sessionTitle}` : "";
@@ -13243,7 +13087,7 @@ var recallMemoryTool = {
13243
13087
  const excludeCurrent = Boolean(args["excludeCurrentSession"]);
13244
13088
  const currentId = args["currentSessionId"] ? String(args["currentSessionId"]) : void 0;
13245
13089
  const minScore = args["minScore"] !== void 0 ? Number(args["minScore"]) : 0.25;
13246
- const configDir = join15(homedir7(), CONFIG_DIR_NAME);
13090
+ const configDir = getExecutionConfigDir();
13247
13091
  let persistentSection = "";
13248
13092
  try {
13249
13093
  const persistentHits = searchPersistentMemories(configDir, query, process.cwd(), 3);
@@ -13297,8 +13141,8 @@ function estimateToolDefinitionTokens(def) {
13297
13141
 
13298
13142
  // src/tools/registry.ts
13299
13143
  import { pathToFileURL } from "url";
13300
- import { existsSync as existsSync20, mkdirSync as mkdirSync9, readdirSync as readdirSync9 } from "fs";
13301
- import { join as join16 } from "path";
13144
+ import { existsSync as existsSync19, mkdirSync as mkdirSync9, readdirSync as readdirSync9 } from "fs";
13145
+ import { join as join13 } from "path";
13302
13146
  var ToolRegistry = class {
13303
13147
  tools = /* @__PURE__ */ new Map();
13304
13148
  pluginToolNames = /* @__PURE__ */ new Set();
@@ -13459,7 +13303,7 @@ var ToolRegistry = class {
13459
13303
  * Returns the number of successfully loaded plugins.
13460
13304
  */
13461
13305
  async loadPlugins(pluginsDir, allowPlugins = false) {
13462
- if (!existsSync20(pluginsDir)) {
13306
+ if (!existsSync19(pluginsDir)) {
13463
13307
  try {
13464
13308
  mkdirSync9(pluginsDir, { recursive: true });
13465
13309
  } catch {
@@ -13485,12 +13329,12 @@ var ToolRegistry = class {
13485
13329
  process.stderr.write(
13486
13330
  `
13487
13331
  [plugins] \u26A0 Loading ${files.length} plugin(s) with FULL system privileges:
13488
- ` + files.map((f) => ` + ${join16(pluginsDir, f)}`).join("\n") + "\n\n"
13332
+ ` + files.map((f) => ` + ${join13(pluginsDir, f)}`).join("\n") + "\n\n"
13489
13333
  );
13490
13334
  let loaded = 0;
13491
13335
  for (const file of files) {
13492
13336
  try {
13493
- const fileUrl = pathToFileURL(join16(pluginsDir, file)).href;
13337
+ const fileUrl = pathToFileURL(join13(pluginsDir, file)).href;
13494
13338
  const mod = await import(fileUrl);
13495
13339
  const tool = mod.tool ?? mod.default?.tool ?? mod.default;
13496
13340
  if (!tool || typeof tool.execute !== "function" || !tool.definition?.name) {
@@ -13518,7 +13362,7 @@ var ToolRegistry = class {
13518
13362
  };
13519
13363
 
13520
13364
  // src/mcp/client.ts
13521
- import { spawn as spawn6 } from "child_process";
13365
+ import { spawn as spawn5 } from "child_process";
13522
13366
  var McpClient = class {
13523
13367
  serverId;
13524
13368
  config;
@@ -13556,7 +13400,7 @@ var McpClient = class {
13556
13400
  async connect() {
13557
13401
  const timeout = this.config.timeout ?? MCP_CONNECT_TIMEOUT;
13558
13402
  try {
13559
- this.process = spawn6(this.config.command, this.config.args ?? [], {
13403
+ this.process = spawn5(this.config.command, this.config.args ?? [], {
13560
13404
  stdio: ["pipe", "pipe", "pipe"],
13561
13405
  env: { ...process.env, ...this.config.env },
13562
13406
  // Windows 上 npx 等是 .cmd 脚本,需要 shell 模式
@@ -14035,11 +13879,11 @@ var McpManager = class {
14035
13879
  };
14036
13880
 
14037
13881
  // src/skills/manager.ts
14038
- import { existsSync as existsSync21, readdirSync as readdirSync10, mkdirSync as mkdirSync10, statSync as statSync9 } from "fs";
14039
- import { join as join17 } from "path";
13882
+ import { existsSync as existsSync20, readdirSync as readdirSync10, mkdirSync as mkdirSync10, statSync as statSync9 } from "fs";
13883
+ import { join as join14 } from "path";
14040
13884
 
14041
13885
  // src/skills/types.ts
14042
- import { readFileSync as readFileSync16 } from "fs";
13886
+ import { readFileSync as readFileSync13 } from "fs";
14043
13887
  import { basename as basename7 } from "path";
14044
13888
  function parseSimpleYaml(yaml) {
14045
13889
  const result = {};
@@ -14061,7 +13905,7 @@ function parseYamlArray(value) {
14061
13905
  function parseSkillFile(filePath) {
14062
13906
  let raw;
14063
13907
  try {
14064
- raw = readFileSync16(filePath, "utf-8");
13908
+ raw = readFileSync13(filePath, "utf-8");
14065
13909
  } catch {
14066
13910
  return null;
14067
13911
  }
@@ -14109,7 +13953,7 @@ var SkillManager = class {
14109
13953
  return this.skills.size;
14110
13954
  }
14111
13955
  loadSkillDir(dir, createIfMissing) {
14112
- if (!existsSync21(dir)) {
13956
+ if (!existsSync20(dir)) {
14113
13957
  if (createIfMissing) {
14114
13958
  try {
14115
13959
  mkdirSync10(dir, { recursive: true });
@@ -14126,14 +13970,14 @@ var SkillManager = class {
14126
13970
  }
14127
13971
  for (const entry of entries) {
14128
13972
  let filePath;
14129
- const fullPath = join17(dir, entry);
13973
+ const fullPath = join14(dir, entry);
14130
13974
  if (entry.endsWith(".md")) {
14131
13975
  filePath = fullPath;
14132
13976
  } else {
14133
13977
  try {
14134
13978
  if (statSync9(fullPath).isDirectory()) {
14135
- const skillMd = join17(fullPath, "SKILL.md");
14136
- if (existsSync21(skillMd)) filePath = skillMd;
13979
+ const skillMd = join14(fullPath, "SKILL.md");
13980
+ if (existsSync20(skillMd)) filePath = skillMd;
14137
13981
  else continue;
14138
13982
  } else {
14139
13983
  continue;
@@ -14246,7 +14090,7 @@ function sendSocketMessage(ws, message) {
14246
14090
  }
14247
14091
 
14248
14092
  // src/web/tool-executor-web.ts
14249
- import { existsSync as existsSync22, readFileSync as readFileSync17 } from "fs";
14093
+ import { existsSync as existsSync21, readFileSync as readFileSync14 } from "fs";
14250
14094
  var ToolExecutorWeb = class _ToolExecutorWeb {
14251
14095
  constructor(registry, ws) {
14252
14096
  this.registry = registry;
@@ -14393,9 +14237,9 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
14393
14237
  getDiffPreview(call) {
14394
14238
  if (call.name === "write_file") {
14395
14239
  const { path: filePath, content: chunk, append } = resolveWriteFileArgs(call.arguments);
14396
- if (filePath && existsSync22(filePath)) {
14240
+ if (filePath && existsSync21(filePath)) {
14397
14241
  try {
14398
- const old = readFileSync17(filePath, "utf-8");
14242
+ const old = readFileSync14(filePath, "utf-8");
14399
14243
  return renderDiff(old, append ? old + chunk : chunk, { filePath });
14400
14244
  } catch {
14401
14245
  }
@@ -14631,16 +14475,16 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
14631
14475
  };
14632
14476
 
14633
14477
  // src/repl/dev-state.ts
14634
- import { existsSync as existsSync23, readFileSync as readFileSync18, unlinkSync as unlinkSync5, mkdirSync as mkdirSync11 } from "fs";
14635
- import { join as join18 } from "path";
14636
- import { homedir as homedir8 } from "os";
14478
+ import { existsSync as existsSync22, readFileSync as readFileSync15, unlinkSync as unlinkSync5, mkdirSync as mkdirSync11 } from "fs";
14479
+ import { join as join15 } from "path";
14480
+ import { homedir as homedir6 } from "os";
14637
14481
  function getDevStatePath() {
14638
- return join18(homedir8(), CONFIG_DIR_NAME, DEV_STATE_FILE_NAME);
14482
+ return join15(homedir6(), CONFIG_DIR_NAME, DEV_STATE_FILE_NAME);
14639
14483
  }
14640
14484
  function loadDevState() {
14641
14485
  const path3 = getDevStatePath();
14642
- if (!existsSync23(path3)) return null;
14643
- const content = readFileSync18(path3, "utf-8").trim();
14486
+ if (!existsSync22(path3)) return null;
14487
+ const content = readFileSync15(path3, "utf-8").trim();
14644
14488
  return content || null;
14645
14489
  }
14646
14490
 
@@ -15010,8 +14854,8 @@ var checkpointDeletedMessage = (name) => `\u2713 Deleted checkpoint "${name}"`;
15010
14854
  var checkpointNotFoundMessage = (name) => `Checkpoint "${name}" not found.`;
15011
14855
 
15012
14856
  // src/core/context-files.ts
15013
- import { existsSync as existsSync24, readFileSync as readFileSync19 } from "fs";
15014
- import { join as join19, relative as relative4, resolve as resolve8 } from "path";
14857
+ import { existsSync as existsSync23, readFileSync as readFileSync16 } from "fs";
14858
+ import { join as join16, relative as relative4, resolve as resolve8 } from "path";
15015
14859
  function uniqueNonEmpty(values) {
15016
14860
  const seen = /* @__PURE__ */ new Set();
15017
14861
  const out = [];
@@ -15041,7 +14885,7 @@ function readContextFile(level, filePath, cwd, maxBytes) {
15041
14885
  const name = filePath.split(/[\\/]/).pop() ?? filePath;
15042
14886
  const shown = displayPath(filePath, cwd);
15043
14887
  try {
15044
- const raw = readFileSync19(filePath);
14888
+ const raw = readFileSync16(filePath);
15045
14889
  const byteCount = raw.byteLength;
15046
14890
  if (byteCount === 0) {
15047
14891
  return {
@@ -15088,8 +14932,8 @@ function readContextFile(level, filePath, cwd, maxBytes) {
15088
14932
  function findFirstContextFile(level, dir, cwd, candidates, maxBytes) {
15089
14933
  const skipped = [];
15090
14934
  for (const candidate of candidates) {
15091
- const filePath = join19(dir, candidate);
15092
- if (!existsSync24(filePath)) continue;
14935
+ const filePath = join16(dir, candidate);
14936
+ if (!existsSync23(filePath)) continue;
15093
14937
  const result = readContextFile(level, filePath, cwd, maxBytes);
15094
14938
  if (result.skipped) skipped.push(result.skipped);
15095
14939
  if (result.layer) return { layer: result.layer, skipped };
@@ -15116,7 +14960,7 @@ function loadContextFiles(options) {
15116
14960
  });
15117
14961
  return { layers: [], mergedContent: "", skipped, totalChars: 0, totalBytes: 0, maxBytes, candidates };
15118
14962
  }
15119
- if (!existsSync24(filePath)) {
14963
+ if (!existsSync23(filePath)) {
15120
14964
  skipped.push({
15121
14965
  level: "single",
15122
14966
  filePath,
@@ -15229,8 +15073,8 @@ import { mkdirSync as mkdirSync15, createWriteStream as createWriteStream2 } fro
15229
15073
  import { dirname as dirname10 } from "path";
15230
15074
 
15231
15075
  // src/repl/commands/project-init.ts
15232
- import { existsSync as existsSync25, readFileSync as readFileSync20, readdirSync as readdirSync11, statSync as statSync11 } from "fs";
15233
- import { join as join20 } from "path";
15076
+ import { existsSync as existsSync24, readFileSync as readFileSync17, readdirSync as readdirSync11, statSync as statSync11 } from "fs";
15077
+ import { join as join17 } from "path";
15234
15078
  var SCAN_SKIP_DIRS = /* @__PURE__ */ new Set([
15235
15079
  "node_modules",
15236
15080
  ".git",
@@ -15274,11 +15118,11 @@ function scanDirTree(dir, maxDepth = 2, maxEntries = 80) {
15274
15118
  const sorted = filtered.sort((a, b) => {
15275
15119
  let aIsDir = false, bIsDir = false;
15276
15120
  try {
15277
- aIsDir = statSync11(join20(d, a)).isDirectory();
15121
+ aIsDir = statSync11(join17(d, a)).isDirectory();
15278
15122
  } catch {
15279
15123
  }
15280
15124
  try {
15281
- bIsDir = statSync11(join20(d, b)).isDirectory();
15125
+ bIsDir = statSync11(join17(d, b)).isDirectory();
15282
15126
  } catch {
15283
15127
  }
15284
15128
  if (aIsDir !== bIsDir) return aIsDir ? -1 : 1;
@@ -15286,7 +15130,7 @@ function scanDirTree(dir, maxDepth = 2, maxEntries = 80) {
15286
15130
  });
15287
15131
  for (let i = 0; i < sorted.length && count < maxEntries; i++) {
15288
15132
  const name = sorted[i];
15289
- const fullPath = join20(d, name);
15133
+ const fullPath = join17(d, name);
15290
15134
  const isLast = i === sorted.length - 1;
15291
15135
  const connector = isLast ? "+-- " : "|-- ";
15292
15136
  let isDir;
@@ -15313,7 +15157,7 @@ function scanProject(cwd) {
15313
15157
  configFiles: [],
15314
15158
  directoryStructure: ""
15315
15159
  };
15316
- const check = (file) => existsSync25(join20(cwd, file));
15160
+ const check = (file) => existsSync24(join17(cwd, file));
15317
15161
  const configCandidates = [
15318
15162
  "package.json",
15319
15163
  "tsconfig.json",
@@ -15340,7 +15184,7 @@ function scanProject(cwd) {
15340
15184
  info.type = "node";
15341
15185
  info.language = check("tsconfig.json") ? "TypeScript" : "JavaScript";
15342
15186
  try {
15343
- const pkg = JSON.parse(readFileSync20(join20(cwd, "package.json"), "utf-8"));
15187
+ const pkg = JSON.parse(readFileSync17(join17(cwd, "package.json"), "utf-8"));
15344
15188
  const scripts = pkg.scripts ?? {};
15345
15189
  info.buildCommand = scripts.build ? "npm run build" : void 0;
15346
15190
  info.testCommand = scripts.test ? "npm test" : void 0;
@@ -16108,7 +15952,7 @@ function resolveRoleProviders(roles, lookup, defaultProvider, defaultModel, avai
16108
15952
  }
16109
15953
 
16110
15954
  // src/hub/persist.ts
16111
- import { join as join21 } from "path";
15955
+ import { join as join18 } from "path";
16112
15956
  function discussionToMessages(state2) {
16113
15957
  const out = [];
16114
15958
  const t0 = state2.messages[0]?.timestamp ?? /* @__PURE__ */ new Date();
@@ -16146,7 +15990,7 @@ async function persistDiscussion(state2, config, defaultProvider, defaultModel)
16146
15990
  session.title = `[Hub] ${state2.topic.slice(0, 48)}`.replace(/\n/g, " ");
16147
15991
  session.titleAiGenerated = true;
16148
15992
  await sm.save();
16149
- return { id: session.id, path: join21(config.getHistoryDir(), `${session.id}.json`) };
15993
+ return { id: session.id, path: join18(config.getHistoryDir(), `${session.id}.json`) };
16150
15994
  }
16151
15995
 
16152
15996
  // src/core/think-command.ts
@@ -16267,10 +16111,10 @@ var PLAN_MODE_TOOL_HINT = [
16267
16111
  "read_file \xB7 list_dir \xB7 grep_files \xB7 glob_files",
16268
16112
  "web_fetch \xB7 google_search \xB7 ask_user \xB7 write_todos"
16269
16113
  ];
16270
- function applyPlanCommand(sub, active) {
16271
- const key = (sub ?? "enter").toLowerCase() === "toggle" ? active ? "exit" : "enter" : (sub ?? "enter").toLowerCase();
16114
+ function applyPlanCommand(sub, active2) {
16115
+ const key = (sub ?? "enter").toLowerCase() === "toggle" ? active2 ? "exit" : "enter" : (sub ?? "enter").toLowerCase();
16272
16116
  if (key === "enter") {
16273
- if (active) {
16117
+ if (active2) {
16274
16118
  return {
16275
16119
  planMode: true,
16276
16120
  changed: false,
@@ -16293,7 +16137,7 @@ function applyPlanCommand(sub, active) {
16293
16137
  };
16294
16138
  }
16295
16139
  if (key === "execute") {
16296
- if (!active) {
16140
+ if (!active2) {
16297
16141
  return { planMode: false, changed: false, status: "not-active", summary: "Not in Plan Mode. Enter first with /plan", details: [] };
16298
16142
  }
16299
16143
  return {
@@ -16305,22 +16149,22 @@ function applyPlanCommand(sub, active) {
16305
16149
  };
16306
16150
  }
16307
16151
  if (key === "exit" || key === "cancel") {
16308
- if (!active) {
16152
+ if (!active2) {
16309
16153
  return { planMode: false, changed: false, status: "not-active", summary: "Not in Plan Mode.", details: [] };
16310
16154
  }
16311
16155
  return { planMode: false, changed: true, status: "cancelled", summary: "Plan Mode cancelled. Returning to normal mode.", details: [] };
16312
16156
  }
16313
16157
  if (key === "status" || key === "show") {
16314
16158
  return {
16315
- planMode: active,
16159
+ planMode: active2,
16316
16160
  changed: false,
16317
16161
  status: "status",
16318
- summary: `Plan Mode: ${active ? "ACTIVE \u{1F4CB}" : "inactive"}`,
16319
- details: active ? ["Tools restricted to read-only set.", "Use /plan execute to start executing, or /plan exit to cancel."] : []
16162
+ summary: `Plan Mode: ${active2 ? "ACTIVE \u{1F4CB}" : "inactive"}`,
16163
+ details: active2 ? ["Tools restricted to read-only set.", "Use /plan execute to start executing, or /plan exit to cancel."] : []
16320
16164
  };
16321
16165
  }
16322
16166
  return {
16323
- planMode: active,
16167
+ planMode: active2,
16324
16168
  changed: false,
16325
16169
  status: "unknown",
16326
16170
  summary: `Unknown /plan option "${sub}" \u2014 expected execute | exit | status`,
@@ -16592,8 +16436,8 @@ function buildSessionCostReport(input) {
16592
16436
  const cacheHitPct = usage.cacheReadTokens > 0 ? Math.round(usage.cacheReadTokens / (usage.cacheReadTokens + usage.inputTokens) * 100) : null;
16593
16437
  return { hasUsage: true, totalTokens, cacheHitPct, cost, costSource, savings };
16594
16438
  }
16595
- function costSourceNote(source) {
16596
- switch (source) {
16439
+ function costSourceNote(source2) {
16440
+ switch (source2) {
16597
16441
  case "accrued-partial":
16598
16442
  return "(partial \u2014 some turns had unknown pricing)";
16599
16443
  case "estimated":
@@ -16604,8 +16448,8 @@ function costSourceNote(source) {
16604
16448
  }
16605
16449
 
16606
16450
  // src/web/commands/_shared.ts
16607
- import { existsSync as existsSync28, readFileSync as readFileSync22, writeFileSync as writeFileSync3, mkdirSync as mkdirSync13, readdirSync as readdirSync13, statSync as statSync14, createWriteStream } from "fs";
16608
- import { join as join24, resolve as resolve9, dirname as dirname8 } from "path";
16451
+ import { existsSync as existsSync27, readFileSync as readFileSync19, writeFileSync as writeFileSync3, mkdirSync as mkdirSync13, readdirSync as readdirSync13, statSync as statSync14, createWriteStream } from "fs";
16452
+ import { join as join21, resolve as resolve9, dirname as dirname8 } from "path";
16609
16453
 
16610
16454
  // src/cli/review-prompts.ts
16611
16455
  function buildReviewPrompt(diff, gitContextStr, detailed) {
@@ -16677,34 +16521,34 @@ function readGitDiff(options = {}) {
16677
16521
  }
16678
16522
 
16679
16523
  // src/diagnostics/doctor-report.ts
16680
- import { existsSync as existsSync27, statSync as statSync13 } from "fs";
16681
- import { join as join23 } from "path";
16682
- import { arch as arch2, platform as platform7, release as release2 } from "os";
16524
+ import { existsSync as existsSync26, statSync as statSync13 } from "fs";
16525
+ import { join as join20 } from "path";
16526
+ import { arch as arch2, platform as platform5, release as release2 } from "os";
16683
16527
 
16684
16528
  // src/diagnostics/crash-log.ts
16685
16529
  import {
16686
- existsSync as existsSync26,
16530
+ existsSync as existsSync25,
16687
16531
  mkdirSync as mkdirSync12,
16688
16532
  readdirSync as readdirSync12,
16689
- readFileSync as readFileSync21,
16533
+ readFileSync as readFileSync18,
16690
16534
  statSync as statSync12,
16691
16535
  unlinkSync as unlinkSync6,
16692
16536
  writeFileSync as writeFileSync2
16693
16537
  } from "fs";
16694
- import { join as join22 } from "path";
16695
- import { homedir as homedir9, platform as platform6, release, arch } from "os";
16538
+ import { join as join19 } from "path";
16539
+ import { homedir as homedir7, platform as platform4, release, arch } from "os";
16696
16540
  var LOGS_DIR_NAME = "logs";
16697
16541
  var CRASH_LOG_PREFIX = "crash-";
16698
16542
  function getLogsDir(configDir) {
16699
- const base = configDir ?? join22(homedir9(), CONFIG_DIR_NAME);
16700
- return join22(base, LOGS_DIR_NAME);
16543
+ const base = configDir ?? join19(homedir7(), CONFIG_DIR_NAME);
16544
+ return join19(base, LOGS_DIR_NAME);
16701
16545
  }
16702
16546
  function truncate(s, n) {
16703
16547
  return s.length <= n ? s : s.slice(0, n) + "\u2026";
16704
16548
  }
16705
16549
  function listRecentCrashes(limit = 10, configDir) {
16706
16550
  const dir = getLogsDir(configDir);
16707
- if (!existsSync26(dir)) return [];
16551
+ if (!existsSync25(dir)) return [];
16708
16552
  const files = [];
16709
16553
  let names;
16710
16554
  try {
@@ -16714,10 +16558,10 @@ function listRecentCrashes(limit = 10, configDir) {
16714
16558
  }
16715
16559
  for (const name of names) {
16716
16560
  if (!name.startsWith(CRASH_LOG_PREFIX) || !name.endsWith(".log")) continue;
16717
- const full = join22(dir, name);
16561
+ const full = join19(dir, name);
16718
16562
  try {
16719
16563
  const st = statSync12(full);
16720
- const head = readFileSync21(full, "utf-8").split("\n").slice(0, 20);
16564
+ const head = readFileSync18(full, "utf-8").split("\n").slice(0, 20);
16721
16565
  const tsLine = head.find((l) => l.startsWith("timestamp:"));
16722
16566
  const kindLine = head.find((l) => l.startsWith("kind:"));
16723
16567
  const msgIdx = head.findIndex((l) => l.trim() === "## message");
@@ -16736,8 +16580,8 @@ function listRecentCrashes(limit = 10, configDir) {
16736
16580
  return files.slice(0, limit);
16737
16581
  }
16738
16582
  function getConfigDirUsage(configDir) {
16739
- const base = configDir ?? join22(homedir9(), CONFIG_DIR_NAME);
16740
- if (!existsSync26(base)) return { totalBytes: 0, entries: [] };
16583
+ const base = configDir ?? join19(homedir7(), CONFIG_DIR_NAME);
16584
+ if (!existsSync25(base)) return { totalBytes: 0, entries: [] };
16741
16585
  const entries = [];
16742
16586
  let total = 0;
16743
16587
  let names;
@@ -16747,7 +16591,7 @@ function getConfigDirUsage(configDir) {
16747
16591
  return { totalBytes: 0, entries: [] };
16748
16592
  }
16749
16593
  for (const name of names) {
16750
- const full = join22(base, name);
16594
+ const full = join19(base, name);
16751
16595
  const bytes = dirSize(full);
16752
16596
  entries.push({ name, bytes });
16753
16597
  total += bytes;
@@ -16762,7 +16606,7 @@ function dirSize(path3) {
16762
16606
  if (!st.isDirectory()) return 0;
16763
16607
  let sum = 0;
16764
16608
  for (const name of readdirSync12(path3)) {
16765
- sum += dirSize(join22(path3, name));
16609
+ sum += dirSize(join19(path3, name));
16766
16610
  }
16767
16611
  return sum;
16768
16612
  } catch {
@@ -16772,7 +16616,7 @@ function dirSize(path3) {
16772
16616
 
16773
16617
  // src/diagnostics/doctor-report.ts
16774
16618
  function checkFile(label, path3) {
16775
- const exists = existsSync27(path3);
16619
+ const exists = existsSync26(path3);
16776
16620
  let sizeBytes = null;
16777
16621
  if (exists) {
16778
16622
  try {
@@ -16809,18 +16653,18 @@ function buildDoctorReport(options) {
16809
16653
  version: VERSION,
16810
16654
  npmCheck: options.npmCheck,
16811
16655
  node: process.version,
16812
- platform: `${platform7()} ${release2()} (${arch2()})`,
16656
+ platform: `${platform5()} ${release2()} (${arch2()})`,
16813
16657
  cwd,
16814
16658
  projectRoot,
16815
16659
  configDir,
16816
16660
  providers: options.providers.listAll().map((p) => ({ id: p.id, displayName: p.displayName, configured: p.configured })),
16817
16661
  files: [
16818
- checkFile("config.json", join23(configDir, "config.json")),
16819
- checkFile("memory.md", join23(configDir, MEMORY_FILE_NAME)),
16820
- checkFile("memory.jsonl", join23(configDir, "memory.jsonl")),
16821
- checkFile("dev-state.md", join23(configDir, DEV_STATE_FILE_NAME)),
16822
- checkFile("hooks-trust.json", join23(configDir, "hooks-trust.json")),
16823
- checkFile("plugin-state.json", join23(configDir, "plugins", "plugin-state.json"))
16662
+ checkFile("config.json", join20(configDir, "config.json")),
16663
+ checkFile("memory.md", join20(configDir, MEMORY_FILE_NAME)),
16664
+ checkFile("memory.jsonl", join20(configDir, "memory.jsonl")),
16665
+ checkFile("dev-state.md", join20(configDir, DEV_STATE_FILE_NAME)),
16666
+ checkFile("hooks-trust.json", join20(configDir, "hooks-trust.json")),
16667
+ checkFile("plugin-state.json", join20(configDir, "plugins", "plugin-state.json"))
16824
16668
  ],
16825
16669
  context: {
16826
16670
  enabled: contextSetting !== false,
@@ -16855,7 +16699,7 @@ function buildDoctorReport(options) {
16855
16699
  disabled: hooks.filter((h) => h.disabled).length
16856
16700
  },
16857
16701
  plugins: {
16858
- root: join23(configDir, "plugins"),
16702
+ root: join20(configDir, "plugins"),
16859
16703
  installed: plugins.length,
16860
16704
  active: activeAssets.plugins.length,
16861
16705
  invalid: plugins.filter((p) => !p.valid).length,
@@ -16870,7 +16714,7 @@ function buildDoctorReport(options) {
16870
16714
  mcp: {
16871
16715
  enabled: config.get("mcpEnabled"),
16872
16716
  configured: Object.keys(mcpServers).length,
16873
- projectConfigExists: existsSync27(join23(projectRoot, MCP_PROJECT_CONFIG_NAME)),
16717
+ projectConfigExists: existsSync26(join20(projectRoot, MCP_PROJECT_CONFIG_NAME)),
16874
16718
  statuses: options.mcpStatuses ?? []
16875
16719
  },
16876
16720
  recentCrashes: listRecentCrashes(5, configDir),
@@ -17392,7 +17236,7 @@ Cache: write=${cacheCreate} read=${cacheRead}` : "";
17392
17236
  Cost: ${formatCost(cost)}` : "";
17393
17237
  let memoryLine = "";
17394
17238
  try {
17395
- const { countPendingMemories: countPendingMemories2 } = await import("./persistent-memory-GJE7HKVK.js");
17239
+ const { countPendingMemories: countPendingMemories2 } = await import("./persistent-memory-UNLCXUWH.js");
17396
17240
  const pending = countPendingMemories2(ctx.config.getConfigDir());
17397
17241
  if (pending > 0) memoryLine = `
17398
17242
  Memory: \u26A0 ${pending} pending approval \u2014 see the Memory panel or /memory`;
@@ -18015,7 +17859,7 @@ async function handleRewind(args, ctx) {
18015
17859
  ctx.send({ type: "error", message: intent.message });
18016
17860
  return;
18017
17861
  }
18018
- const { fileCheckpoints: fileCheckpoints2 } = await import("./file-checkpoint-CGH6OJVI.js");
17862
+ const { fileCheckpoints: fileCheckpoints2 } = await import("./file-checkpoint-64A2AXHE.js");
18019
17863
  if (intent.kind === "list") {
18020
17864
  const rows = buildRewindList(session.messages, fileCheckpoints2.getMessageIndices());
18021
17865
  ctx.send({
@@ -18072,9 +17916,9 @@ function parseAgentCommand(args) {
18072
17916
  function formatAgentList(agents, preferred) {
18073
17917
  const lines = [`Agents (${agents.length})`, ""];
18074
17918
  for (const agent of agents) {
18075
- const active = agent.name === preferred ? " *" : " ";
17919
+ const active2 = agent.name === preferred ? " *" : " ";
18076
17920
  const model = [agent.provider, agent.model].filter(Boolean).join("/") || "inherit";
18077
- lines.push(`${active} ${agent.name} [${agent.source ?? "builtin"}] \u2014 ${agent.description ?? ""}`);
17921
+ lines.push(`${active2} ${agent.name} [${agent.source ?? "builtin"}] \u2014 ${agent.description ?? ""}`);
18078
17922
  lines.push(
18079
17923
  ` model: ${model} \xB7 permission: ${agent.permissionProfile ?? "workspace-write"} \xB7 maxRounds: ${agent.maxToolRounds ?? "inherit"}`
18080
17924
  );
@@ -18133,7 +17977,7 @@ async function handlePlugin(args, ctx) {
18133
17977
  lines.push("");
18134
17978
  }
18135
17979
  if (pluginTools.length > 0) {
18136
- const legacyDir = join24(configDir, PLUGINS_DIR_NAME);
17980
+ const legacyDir = join21(configDir, PLUGINS_DIR_NAME);
18137
17981
  lines.push(`**Legacy JS tools** (${pluginTools.length}):`);
18138
17982
  lines.push(`Dir: ${legacyDir}`);
18139
17983
  for (const t of pluginTools) lines.push(` \u2022 ${t.definition.name} \u2014 ${t.definition.description}`);
@@ -18215,14 +18059,14 @@ async function handleSkill(args, ctx) {
18215
18059
  return;
18216
18060
  }
18217
18061
  const intent = parseSkillCommand(args);
18218
- const active = ctx.skillManager.getActive();
18062
+ const active2 = ctx.skillManager.getActive();
18219
18063
  if (intent.kind === "list") {
18220
18064
  const skills = ctx.skillManager.listSkills();
18221
18065
  if (skills.length === 0) {
18222
18066
  ctx.send({ type: "info", message: "No skills available. Add .md files to ~/.aicli/skills/" });
18223
18067
  } else {
18224
18068
  const lines = skills.map((s) => {
18225
- const isActive = active?.meta.name === s.meta.name;
18069
+ const isActive = active2?.meta.name === s.meta.name;
18226
18070
  const marker = isActive ? " \u2705 (active)" : "";
18227
18071
  return ` ${s.meta.name}${marker} \u2014 ${s.meta.description || "(no description)"}`;
18228
18072
  });
@@ -18238,12 +18082,12 @@ ${lines.join("\n")}` });
18238
18082
  return;
18239
18083
  }
18240
18084
  const note = skillHistoryNote(
18241
- active?.meta.name ?? "(none)",
18085
+ active2?.meta.name ?? "(none)",
18242
18086
  intent.kind === "deactivate" ? "(none)" : intent.name,
18243
18087
  ctx.sessions.current?.messages.length ?? 0
18244
18088
  );
18245
18089
  if (intent.kind === "deactivate") {
18246
- if (!active) {
18090
+ if (!active2) {
18247
18091
  ctx.send({ type: "info", message: "No skill is currently active." });
18248
18092
  return;
18249
18093
  }
@@ -18254,7 +18098,7 @@ ${note}` : "\u{1F50C} Skill deactivated." });
18254
18098
  ctx.sendStatus();
18255
18099
  return;
18256
18100
  }
18257
- if (active?.meta.name === intent.name) {
18101
+ if (active2?.meta.name === intent.name) {
18258
18102
  ctx.send({ type: "info", message: `Skill '${intent.name}' is already active.` });
18259
18103
  return;
18260
18104
  }
@@ -18273,8 +18117,8 @@ ${note}` : head });
18273
18117
  }
18274
18118
  async function handleCommands(_args, ctx) {
18275
18119
  const configDir = ctx.config.getConfigDir();
18276
- const commandsDir = join24(configDir, CUSTOM_COMMANDS_DIR_NAME);
18277
- if (!existsSync28(commandsDir)) {
18120
+ const commandsDir = join21(configDir, CUSTOM_COMMANDS_DIR_NAME);
18121
+ if (!existsSync27(commandsDir)) {
18278
18122
  ctx.send({ type: "info", message: `No custom commands directory.
18279
18123
  Create: ${commandsDir}/ with .md files.` });
18280
18124
  return;
@@ -18380,7 +18224,7 @@ async function handleTest(args, ctx) {
18380
18224
  const isCommand = argStr.includes(" ") || /^(mvn|gradle|npm|pytest|cargo|go)\b/.test(argStr);
18381
18225
  testArgs = isCommand ? { command: argStr } : { filter: argStr };
18382
18226
  }
18383
- const runTests = ctx.runTests ?? (await import("./run-tests-V3N7YV3P.js")).executeTests;
18227
+ const runTests = ctx.runTests ?? (await import("./run-tests-KRNMFXAN.js")).executeTests;
18384
18228
  const report = await runTests(testArgs);
18385
18229
  ctx.send({ type: "info", message: report });
18386
18230
  } catch (err) {
@@ -18404,9 +18248,9 @@ async function handleDiff(args, ctx) {
18404
18248
  let modifiedFiles = 0;
18405
18249
  const diffLines2 = [];
18406
18250
  for (const [filePath, { earliest }] of fileMap) {
18407
- const currentContent = existsSync28(filePath) ? (() => {
18251
+ const currentContent = existsSync27(filePath) ? (() => {
18408
18252
  try {
18409
- return readFileSync22(filePath, "utf-8");
18253
+ return readFileSync19(filePath, "utf-8");
18410
18254
  } catch {
18411
18255
  return null;
18412
18256
  }
@@ -18441,9 +18285,9 @@ async function handleDiff(args, ctx) {
18441
18285
  }
18442
18286
  async function handleInit(args, ctx) {
18443
18287
  const cwd = process.cwd();
18444
- const targetPath = join24(cwd, "AICLI.md");
18288
+ const targetPath = join21(cwd, "AICLI.md");
18445
18289
  const force = args.includes("--force");
18446
- if (existsSync28(targetPath) && !force) {
18290
+ if (existsSync27(targetPath) && !force) {
18447
18291
  ctx.send({ type: "info", message: `AICLI.md already exists at ${targetPath}
18448
18292
  Use /init --force to overwrite.` });
18449
18293
  return;
@@ -18672,9 +18516,9 @@ async function handleContext(_args, ctx) {
18672
18516
  async function handleIndex(args, ctx) {
18673
18517
  const sub = (args[0] ?? "status").toLowerCase();
18674
18518
  const root = process.cwd();
18675
- const { loadIndex: loadIndex2, clearIndex } = await import("./store-SIKHCUYJ.js");
18676
- const { indexProject: indexProject2 } = await import("./indexer-FPKY7LCT.js");
18677
- const { loadVectorStore, clearVectorStore } = await import("./vector-store-JAXWDZYW.js");
18519
+ const { loadIndex: loadIndex2, clearIndex } = await import("./store-2N6PPPBP.js");
18520
+ const { indexProject: indexProject2 } = await import("./indexer-VRXDZ74A.js");
18521
+ const { loadVectorStore, clearVectorStore } = await import("./vector-store-T2DVUMY4.js");
18678
18522
  if (sub === "status") {
18679
18523
  const idx = loadIndex2(root);
18680
18524
  const vec = loadVectorStore(root);
@@ -18724,7 +18568,7 @@ async function handleIndex(args, ctx) {
18724
18568
  message: `Building semantic index for ${idx.symbolCount} symbols\u2026 (first run downloads ~117 MB model)`
18725
18569
  });
18726
18570
  try {
18727
- const { rebuildSemanticIndex } = await import("./semantic-QOBAXEQ4.js");
18571
+ const { rebuildSemanticIndex } = await import("./semantic-TRQAZSRY.js");
18728
18572
  const stats = await rebuildSemanticIndex(root);
18729
18573
  const first = stats.modelFirstLoadMs ? ` (model load+first batch ${stats.modelFirstLoadMs}ms)` : "";
18730
18574
  ctx.send({
@@ -18755,7 +18599,7 @@ async function handleMemory(args, ctx) {
18755
18599
  if (intent.kind === "rebuild") {
18756
18600
  ctx.send({ type: "info", message: rebuildStartMessage(intent.full) });
18757
18601
  try {
18758
- const { buildChatIndex: buildChatIndex2 } = await import("./chat-index-DGUOUUQW.js");
18602
+ const { buildChatIndex: buildChatIndex2 } = await import("./chat-index-RLVLQRRC.js");
18759
18603
  const stats = await buildChatIndex2({ full: intent.full });
18760
18604
  ctx.send({ type: "info", message: rebuildDoneMessage(stats) });
18761
18605
  } catch (err) {
@@ -18764,14 +18608,14 @@ async function handleMemory(args, ctx) {
18764
18608
  return;
18765
18609
  }
18766
18610
  if (intent.kind === "index-clear") {
18767
- const { clearChatIndex: clearChatIndex2 } = await import("./chat-index-DGUOUUQW.js");
18611
+ const { clearChatIndex: clearChatIndex2 } = await import("./chat-index-RLVLQRRC.js");
18768
18612
  clearChatIndex2();
18769
18613
  ctx.send({ type: "info", message: "Chat memory index cleared (persistent memory untouched)." });
18770
18614
  return;
18771
18615
  }
18772
18616
  if (intent.kind === "status") {
18773
- const { getChatIndexStatus: getChatIndexStatus2 } = await import("./chat-index-DGUOUUQW.js");
18774
- const { listMemoryEntries: listMemoryEntries2, isMemoryExpired: isMemoryExpired2, formatMemoryForPrompt: formatMemoryForPrompt2 } = await import("./persistent-memory-GJE7HKVK.js");
18617
+ const { getChatIndexStatus: getChatIndexStatus2 } = await import("./chat-index-RLVLQRRC.js");
18618
+ const { listMemoryEntries: listMemoryEntries2, isMemoryExpired: isMemoryExpired2, formatMemoryForPrompt: formatMemoryForPrompt2 } = await import("./persistent-memory-UNLCXUWH.js");
18775
18619
  const memories = listMemoryEntries2(configDir, { includeExpired: true, includeRejected: true });
18776
18620
  const injected = formatMemoryForPrompt2(configDir, process.cwd(), ctx.config.get("memory")?.maxChars);
18777
18621
  const view = buildMemoryStatusView({
@@ -18794,8 +18638,8 @@ async function handleMemory(args, ctx) {
18794
18638
  return;
18795
18639
  }
18796
18640
  if (intent.kind === "recall") {
18797
- const { searchPersistentMemories: searchPersistentMemories2, touchMemoryReferences: touchMemoryReferences2 } = await import("./persistent-memory-GJE7HKVK.js");
18798
- const { searchChatMemory: searchChatMemory2 } = await import("./chat-index-DGUOUUQW.js");
18641
+ const { searchPersistentMemories: searchPersistentMemories2, touchMemoryReferences: touchMemoryReferences2 } = await import("./persistent-memory-UNLCXUWH.js");
18642
+ const { searchChatMemory: searchChatMemory2 } = await import("./chat-index-RLVLQRRC.js");
18799
18643
  const persistentHits = searchPersistentMemories2(configDir, intent.query, process.cwd(), 3);
18800
18644
  if (persistentHits.length > 0) {
18801
18645
  touchMemoryReferences2(configDir, persistentHits.map((h) => h.entry.id));
@@ -18915,7 +18759,7 @@ async function handleMemory(args, ctx) {
18915
18759
  ctx.handleMemoryManage(intent.action, args[1], intent.action === "expire" ? args[2] : void 0);
18916
18760
  } else if (intent.kind === "export") {
18917
18761
  if (args[1] === "md") {
18918
- const { exportMemoryEntries: exportMemoryEntries2 } = await import("./persistent-memory-GJE7HKVK.js");
18762
+ const { exportMemoryEntries: exportMemoryEntries2 } = await import("./persistent-memory-UNLCXUWH.js");
18919
18763
  ctx.send({
18920
18764
  type: "export_data",
18921
18765
  format: "md",
@@ -18953,7 +18797,7 @@ Use /add-dir remove to clear.` : "No directories added.\nUsage: /add-dir <path>
18953
18797
  return;
18954
18798
  }
18955
18799
  const dirPath = resolve9(sub);
18956
- if (!existsSync28(dirPath)) {
18800
+ if (!existsSync27(dirPath)) {
18957
18801
  ctx.send({ type: "error", message: `Directory not found: ${dirPath}` });
18958
18802
  return;
18959
18803
  }
@@ -19317,7 +19161,7 @@ async function handleAgent(args, ctx) {
19317
19161
 
19318
19162
  // src/repl/commands/utils.ts
19319
19163
  import { execSync as execSync2 } from "child_process";
19320
- import { platform as platform8 } from "os";
19164
+ import { platform as platform6 } from "os";
19321
19165
 
19322
19166
  // src/repl/commands/constants.ts
19323
19167
  var SESSION_PAGE_SIZE = 20;
@@ -19330,7 +19174,7 @@ function fmtCtx(tokens) {
19330
19174
  return `${tokens}`;
19331
19175
  }
19332
19176
  function copyToClipboard(text) {
19333
- const plat = platform8();
19177
+ const plat = platform6();
19334
19178
  if (plat === "win32") {
19335
19179
  execSync2("clip", { input: text, stdio: ["pipe", "ignore", "ignore"] });
19336
19180
  } else if (plat === "darwin") {
@@ -19917,16 +19761,16 @@ No tools match "${filter}".
19917
19761
  usage: "/mcp [reconnect [serverId] | trust-project]",
19918
19762
  async execute(args, ctx) {
19919
19763
  if (args[0] === "trust-project") {
19920
- const { join: join29 } = await import("path");
19921
- const { existsSync: existsSync33 } = await import("fs");
19764
+ const { join: join26 } = await import("path");
19765
+ const { existsSync: existsSync32 } = await import("fs");
19922
19766
  const { getGitRoot: getGitRoot2 } = await import("./git-context-VNCA2GU7.js");
19923
- const { MCP_PROJECT_CONFIG_NAME: MCP_PROJECT_CONFIG_NAME2 } = await import("./constants-5CMEFZQB.js");
19924
- const { approveProject, hashMcpFile } = await import("./project-trust-MUG325AW.js");
19767
+ const { MCP_PROJECT_CONFIG_NAME: MCP_PROJECT_CONFIG_NAME2 } = await import("./constants-FG27DQMS.js");
19768
+ const { approveProject, hashMcpFile } = await import("./project-trust-6CJB5ZMX.js");
19925
19769
  const cwd = process.cwd();
19926
19770
  const projectRoot = getGitRoot2(cwd) ?? cwd;
19927
- const mcpPath = join29(projectRoot, MCP_PROJECT_CONFIG_NAME2);
19771
+ const mcpPath = join26(projectRoot, MCP_PROJECT_CONFIG_NAME2);
19928
19772
  console.log();
19929
- if (!existsSync33(mcpPath)) {
19773
+ if (!existsSync32(mcpPath)) {
19930
19774
  console.log(theme.dim(` No .mcp.json in ${projectRoot}.`));
19931
19775
  console.log();
19932
19776
  return;
@@ -19999,8 +19843,8 @@ No tools match "${filter}".
19999
19843
  const errorStr = s.error ? theme.error(`
20000
19844
  Error: ${s.error}`) : "";
20001
19845
  const serverLabel = s.serverName !== s.serverId ? ` (${s.serverName})` : "";
20002
- const source = ctx.getMcpServerSource(s.serverId);
20003
- const sourceTag = source === "project" ? theme.accent(" [project]") : theme.dim(" [global]");
19846
+ const source2 = ctx.getMcpServerSource(s.serverId);
19847
+ const sourceTag = source2 === "project" ? theme.accent(" [project]") : theme.dim(" [global]");
20004
19848
  console.log(` ${theme.accent(s.serverId)}${theme.dim(serverLabel)}${sourceTag} [${statusBadge}]${toolCountStr}${errorStr}`);
20005
19849
  if (s.connected) {
20006
19850
  const mcpTools = ctx.tools.listMcpTools().filter(
@@ -20028,7 +19872,7 @@ No tools match "${filter}".
20028
19872
  }
20029
19873
  if (!sub || sub === "list") {
20030
19874
  const skills = manager.listSkills();
20031
- const active = manager.getActive();
19875
+ const active2 = manager.getActive();
20032
19876
  console.log();
20033
19877
  console.log(theme.heading(" \u{1F3AF} Agent Skills"));
20034
19878
  console.log(theme.dim(" " + "\u2500".repeat(50)));
@@ -20038,7 +19882,7 @@ No tools match "${filter}".
20038
19882
  console.log(theme.dim(" Format: YAML frontmatter (name/description/tools) + prompt body"));
20039
19883
  } else {
20040
19884
  for (const s of skills) {
20041
- const isActive = active?.meta.name === s.meta.name;
19885
+ const isActive = active2?.meta.name === s.meta.name;
20042
19886
  const badge = isActive ? theme.success(" \u2713 active") : "";
20043
19887
  const toolInfo = s.meta.tools ? theme.dim(` [${s.meta.tools.join(", ")}]`) : "";
20044
19888
  console.log(
@@ -20126,7 +19970,7 @@ No tools match "${filter}".
20126
19970
  }
20127
19971
 
20128
19972
  // src/repl/commands/security-commands.ts
20129
- import { platform as platform9 } from "os";
19973
+ import { platform as platform7 } from "os";
20130
19974
  import chalk6 from "chalk";
20131
19975
  function createSecurityCommands() {
20132
19976
  return [
@@ -20207,7 +20051,7 @@ ${outcome.summary}
20207
20051
  usage: "/bug [--copy]",
20208
20052
  execute(args, ctx) {
20209
20053
  const doCopy = args.includes("--copy");
20210
- const os = platform9();
20054
+ const os = platform7();
20211
20055
  const nodeVer = process.version;
20212
20056
  const provider = ctx.getCurrentProvider();
20213
20057
  const model = ctx.getCurrentModel();
@@ -20361,12 +20205,12 @@ ${outcome.summary}
20361
20205
  if (scanAll) {
20362
20206
  const metas = ctx.sessions.listSessions();
20363
20207
  console.log(theme.info(` Scanning ${metas.length} session(s)\u2026`));
20364
- const { readFileSync: readFileSync26 } = await import("fs");
20365
- const { join: join29 } = await import("path");
20208
+ const { readFileSync: readFileSync23 } = await import("fs");
20209
+ const { join: join26 } = await import("path");
20366
20210
  const historyDir = ctx.config.getHistoryDir();
20367
20211
  for (const m of metas) {
20368
20212
  try {
20369
- const content = readFileSync26(join29(historyDir, `${m.id}.json`), "utf-8");
20213
+ const content = readFileSync23(join26(historyDir, `${m.id}.json`), "utf-8");
20370
20214
  const hits2 = scanString(content, opts);
20371
20215
  if (hits2.length) {
20372
20216
  filesWithHits++;
@@ -20413,8 +20257,8 @@ ${outcome.summary}
20413
20257
  }
20414
20258
 
20415
20259
  // src/repl/commands/review-commands.ts
20416
- import { existsSync as existsSync29, readFileSync as readFileSync23, writeFileSync as writeFileSync4 } from "fs";
20417
- import { join as join25 } from "path";
20260
+ import { existsSync as existsSync28, readFileSync as readFileSync20, writeFileSync as writeFileSync4 } from "fs";
20261
+ import { join as join22 } from "path";
20418
20262
  function createReviewCommands() {
20419
20263
  return [
20420
20264
  {
@@ -20588,11 +20432,11 @@ function createReviewCommands() {
20588
20432
  Branches (${branches.length}):
20589
20433
  `));
20590
20434
  for (const b of branches) {
20591
- const active = b.id === session.activeBranchId ? theme.success("\u25CF ") : " ";
20435
+ const active2 = b.id === session.activeBranchId ? theme.success("\u25CF ") : " ";
20592
20436
  const parent = b.parentBranchId ? theme.dim(` \u2190 ${b.parentBranchId}@${b.parentMessageIndex}`) : "";
20593
20437
  const count = b.id === session.activeBranchId ? session.messages.length : session.getBranchMessages(b.id)?.length ?? 0;
20594
20438
  console.log(
20595
- ` ${active}${theme.accent(b.id.padEnd(10))} ${b.title.padEnd(20)} ` + theme.dim(`(${count} msgs)`) + parent
20439
+ ` ${active2}${theme.accent(b.id.padEnd(10))} ${b.title.padEnd(20)} ` + theme.dim(`(${count} msgs)`) + parent
20596
20440
  );
20597
20441
  }
20598
20442
  console.log();
@@ -20705,9 +20549,9 @@ function createReviewCommands() {
20705
20549
  ctx.renderer.renderError(`Branch "${id}" not found.`);
20706
20550
  return;
20707
20551
  }
20708
- const active = session.activeBranchId;
20552
+ const active2 = session.activeBranchId;
20709
20553
  console.log(`
20710
- ${theme.info("diff")} ${id} (source) \u2194 ${active} (active)`);
20554
+ ${theme.info("diff")} ${id} (source) \u2194 ${active2} (active)`);
20711
20555
  console.log(theme.dim(` Common prefix: ${d.commonPrefix} message(s)`));
20712
20556
  if (d.sourceOnly.length === 0 && d.targetOnly.length === 0) {
20713
20557
  console.log(theme.success(" Branches are identical.\n"));
@@ -20726,7 +20570,7 @@ function createReviewCommands() {
20726
20570
  }
20727
20571
  if (d.targetOnly.length > 0) {
20728
20572
  console.log(theme.dim(`
20729
- Only in ${active} (active) (${d.targetOnly.length}):`));
20573
+ Only in ${active2} (active) (${d.targetOnly.length}):`));
20730
20574
  d.targetOnly.forEach((m, i) => {
20731
20575
  console.log(` ${theme.dim(`[${d.commonPrefix + i}]`)} ${m.role}: ${preview(m)}`);
20732
20576
  });
@@ -20773,9 +20617,9 @@ function createReviewCommands() {
20773
20617
  usage: "/init [--force]",
20774
20618
  async execute(args, ctx) {
20775
20619
  const cwd = process.cwd();
20776
- const targetPath = join25(cwd, "AICLI.md");
20620
+ const targetPath = join22(cwd, "AICLI.md");
20777
20621
  const force = args.includes("--force");
20778
- if (existsSync29(targetPath) && !force) {
20622
+ if (existsSync28(targetPath) && !force) {
20779
20623
  ctx.renderer.printInfo(`AICLI.md already exists at ${targetPath}`);
20780
20624
  ctx.renderer.printInfo("Use /init --force to overwrite, or edit it manually.");
20781
20625
  return;
@@ -20839,7 +20683,7 @@ function createReviewCommands() {
20839
20683
  usage: "/test [command|filter]",
20840
20684
  async execute(args, ctx) {
20841
20685
  try {
20842
- const { executeTests } = await import("./run-tests-V3N7YV3P.js");
20686
+ const { executeTests } = await import("./run-tests-KRNMFXAN.js");
20843
20687
  const argStr = args.join(" ").trim();
20844
20688
  let testArgs = {};
20845
20689
  if (argStr) {
@@ -20874,9 +20718,9 @@ function createReviewCommands() {
20874
20718
  let newFiles = 0;
20875
20719
  let modifiedFiles = 0;
20876
20720
  for (const [filePath, { earliest }] of fileMap) {
20877
- const currentContent = existsSync29(filePath) ? (() => {
20721
+ const currentContent = existsSync28(filePath) ? (() => {
20878
20722
  try {
20879
- return readFileSync23(filePath, "utf-8");
20723
+ return readFileSync20(filePath, "utf-8");
20880
20724
  } catch {
20881
20725
  return null;
20882
20726
  }
@@ -21090,9 +20934,9 @@ function createMemoryCommands() {
21090
20934
  async execute(args, ctx) {
21091
20935
  const sub = (args[0] ?? "status").toLowerCase();
21092
20936
  const root = process.cwd();
21093
- const { loadIndex: loadIndex2, clearIndex } = await import("./store-SIKHCUYJ.js");
21094
- const { indexProject: indexProject2 } = await import("./indexer-FPKY7LCT.js");
21095
- const { loadVectorStore, clearVectorStore } = await import("./vector-store-JAXWDZYW.js");
20937
+ const { loadIndex: loadIndex2, clearIndex } = await import("./store-2N6PPPBP.js");
20938
+ const { indexProject: indexProject2 } = await import("./indexer-VRXDZ74A.js");
20939
+ const { loadVectorStore, clearVectorStore } = await import("./vector-store-T2DVUMY4.js");
21096
20940
  if (sub === "status") {
21097
20941
  const idx = loadIndex2(root);
21098
20942
  const vec = loadVectorStore(root);
@@ -21152,7 +20996,7 @@ function createMemoryCommands() {
21152
20996
  }
21153
20997
  console.log(theme.dim(` Building semantic index for ${idx.symbolCount} symbols\u2026`));
21154
20998
  console.log(theme.dim(" (First run downloads ~117 MB embedding model to ~/.aicli/models/)"));
21155
- const { rebuildSemanticIndex } = await import("./semantic-QOBAXEQ4.js");
20999
+ const { rebuildSemanticIndex } = await import("./semantic-TRQAZSRY.js");
21156
21000
  try {
21157
21001
  const stats = await rebuildSemanticIndex(root, {
21158
21002
  onProgress: (done, total) => {
@@ -21351,14 +21195,14 @@ ${view.chatHeading}
21351
21195
  console.log(theme.heading("\n\u{1F4DD} Governed Persistent Memory:") + filterNote + "\n" + theme.dim("\u2500".repeat(72)));
21352
21196
  for (const entry of entries) {
21353
21197
  const status = isMemoryExpired(entry) ? "expired" : entry.approved ? "approved" : "pending";
21354
- const source = [
21198
+ const source2 = [
21355
21199
  entry.source ?? "unknown",
21356
21200
  entry.sourceSession ? `session ${entry.sourceSession.slice(0, 8)}` : "",
21357
21201
  entry.sourceProject ? `project ${entry.sourceProject}` : ""
21358
21202
  ].filter(Boolean).join(" \xB7 ");
21359
21203
  console.log(theme.accent(` ${entry.id.slice(0, 8)} ${status} scope:${entry.scope} sensitivity:${entry.sensitivity}`));
21360
21204
  const lastRef = entry.lastReferencedAt ? ` \xB7 last-referenced: ${entry.lastReferencedAt.slice(0, 10)}` : "";
21361
- console.log(theme.dim(` source: ${source} \xB7 created: ${entry.createdAt.replace("T", " ").slice(0, 19)}${lastRef}${entry.expiresAt ? ` \xB7 expires: ${entry.expiresAt}` : ""}`));
21205
+ console.log(theme.dim(` source: ${source2} \xB7 created: ${entry.createdAt.replace("T", " ").slice(0, 19)}${lastRef}${entry.expiresAt ? ` \xB7 expires: ${entry.expiresAt}` : ""}`));
21362
21206
  console.log(" " + entry.content.replace(/\n/g, "\n "));
21363
21207
  console.log();
21364
21208
  }
@@ -22039,16 +21883,16 @@ function createSessionCommands() {
22039
21883
 
22040
21884
  // src/repl/commands/misc-commands.ts
22041
21885
  import { basename as basename9 } from "path";
22042
- import { platform as platform10 } from "os";
21886
+ import { platform as platform8 } from "os";
22043
21887
 
22044
21888
  // src/repl/clipboard.ts
22045
21889
  import { execSync as execSync3 } from "child_process";
22046
- import { existsSync as existsSync30, statSync as statSync15, unlinkSync as unlinkSync7 } from "fs";
21890
+ import { existsSync as existsSync29, statSync as statSync15, unlinkSync as unlinkSync7 } from "fs";
22047
21891
  import { tmpdir as tmpdir3 } from "os";
22048
- import { join as join26 } from "path";
21892
+ import { join as join23 } from "path";
22049
21893
  var CLIPBOARD_TIMEOUT = 5e3;
22050
21894
  function readClipboardImage() {
22051
- const outPath = join26(tmpdir3(), `aicli-paste-${Date.now()}.png`);
21895
+ const outPath = join23(tmpdir3(), `aicli-paste-${Date.now()}.png`);
22052
21896
  try {
22053
21897
  if (process.platform === "win32") {
22054
21898
  _readWin32(outPath);
@@ -22061,7 +21905,7 @@ function readClipboardImage() {
22061
21905
  _cleanup(outPath);
22062
21906
  return null;
22063
21907
  }
22064
- if (!existsSync30(outPath) || statSync15(outPath).size === 0) {
21908
+ if (!existsSync29(outPath) || statSync15(outPath).size === 0) {
22065
21909
  _cleanup(outPath);
22066
21910
  return null;
22067
21911
  }
@@ -22114,7 +21958,7 @@ function _readLinux(outPath) {
22114
21958
  }
22115
21959
  function _cleanup(path3) {
22116
21960
  try {
22117
- if (existsSync30(path3)) unlinkSync7(path3);
21961
+ if (existsSync29(path3)) unlinkSync7(path3);
22118
21962
  } catch {
22119
21963
  }
22120
21964
  }
@@ -22361,7 +22205,7 @@ No commands match "${filter}".
22361
22205
  ctx.renderer.printSuccess(`Copied to clipboard (${lines} lines, ${chars} chars)`);
22362
22206
  } catch (err) {
22363
22207
  const msg = err instanceof Error ? err.message : String(err);
22364
- if (platform10() === "linux" && msg.includes("not found")) {
22208
+ if (platform8() === "linux" && msg.includes("not found")) {
22365
22209
  ctx.renderer.renderError("Clipboard tool not found. Install xclip or xsel: sudo apt install xclip");
22366
22210
  } else {
22367
22211
  ctx.renderer.renderError(`Failed to copy: ${msg}`);
@@ -22956,7 +22800,18 @@ var SessionHandler = class _SessionHandler {
22956
22800
  }))
22957
22801
  });
22958
22802
  }
22803
+ executionState = /* @__PURE__ */ new Map();
22804
+ connectionAbort = new AbortController();
22959
22805
  async handleMessage(raw) {
22806
+ if (this.connectionAbort.signal.aborted && /"type"\s*:\s*"(?:chat|command|hub_start)"/.test(raw)) this.connectionAbort = new AbortController();
22807
+ return runWithExecutionContext({
22808
+ configDir: this.config.getConfigDir(),
22809
+ sessionKey: this._bashSessionKey,
22810
+ state: this.executionState,
22811
+ signal: this.connectionAbort.signal
22812
+ }, () => this.handleMessageScoped(raw));
22813
+ }
22814
+ async handleMessageScoped(raw) {
22960
22815
  if (this.disconnected) return;
22961
22816
  let msg;
22962
22817
  try {
@@ -22967,6 +22822,7 @@ var SessionHandler = class _SessionHandler {
22967
22822
  }
22968
22823
  switch (msg.type) {
22969
22824
  case "chat": {
22825
+ if (this.connectionAbort.signal.aborted) this.connectionAbort = new AbortController();
22970
22826
  if (msg.messageId) {
22971
22827
  if (typeof msg.messageId !== "string" || msg.messageId.length > 200) throw new Error("Invalid message ID");
22972
22828
  const scope = this.config.getConfigDir();
@@ -23027,6 +22883,7 @@ var SessionHandler = class _SessionHandler {
23027
22883
  return;
23028
22884
  }
23029
22885
  case "abort":
22886
+ this.connectionAbort.abort();
23030
22887
  if (this.abortController) {
23031
22888
  this.abortController.abort();
23032
22889
  }
@@ -23050,8 +22907,16 @@ var SessionHandler = class _SessionHandler {
23050
22907
  }
23051
22908
  }
23052
22909
  onDisconnect(closeSocket = true) {
22910
+ runWithExecutionContext({
22911
+ configDir: this.config.getConfigDir(),
22912
+ sessionKey: this._bashSessionKey,
22913
+ state: this.executionState
22914
+ }, () => this.disconnectScoped(closeSocket));
22915
+ }
22916
+ disconnectScoped(closeSocket) {
23053
22917
  if (this.disconnected) return;
23054
22918
  this.disconnected = true;
22919
+ this.connectionAbort.abort();
23055
22920
  if (closeSocket && this.ws.readyState === this.ws.OPEN) this.ws.close(1001, "Connection closed; reconnect to restore session");
23056
22921
  runLifecycleHooks(mergePluginHooks(this.config.get("hooks") ?? void 0, this.config.getConfigDir()), "Stop", { sessionId: this.sessions.current?.id }, { configDir: this.config.getConfigDir() });
23057
22922
  this.toolExecutor.cancelAll();
@@ -24030,20 +23895,20 @@ ${summaryContent}`,
24030
23895
  this.send({ type: "error", message: "Usage: /memory show --stale <days> (positive number)" });
24031
23896
  return;
24032
23897
  }
24033
- const source = filters.orphaned ? listOrphanedProjectMemories(configDir) : listMemoryEntries(configDir, { includeExpired: true, includeRejected: true });
24034
- const { entries, applied } = filterMemoryEntries(source, filters);
23898
+ const source2 = filters.orphaned ? listOrphanedProjectMemories(configDir) : listMemoryEntries(configDir, { includeExpired: true, includeRejected: true });
23899
+ const { entries, applied } = filterMemoryEntries(source2, filters);
24035
23900
  const filterNote = applied.length > 0 ? ` (filtered: ${applied.join(", ")})` : "";
24036
23901
  const content = entries.length === 0 ? applied.length > 0 ? `(no entries match [${applied.join(", ")}])` : "(empty \u2014 no persistent memory entries yet)" : `Governed persistent memory${filterNote}
24037
23902
 
24038
23903
  ` + entries.map((entry) => {
24039
23904
  const status = isMemoryExpired(entry) ? "expired" : entry.approved ? "approved" : "pending";
24040
- const source2 = [
23905
+ const source3 = [
24041
23906
  entry.source ?? "unknown",
24042
23907
  entry.sourceSession ? `session ${entry.sourceSession.slice(0, 8)}` : "",
24043
23908
  entry.sourceProject ? `project ${entry.sourceProject}` : ""
24044
23909
  ].filter(Boolean).join(" \xB7 ");
24045
23910
  return `## ${entry.id.slice(0, 8)} \xB7 ${status} \xB7 scope:${entry.scope} \xB7 sensitivity:${entry.sensitivity}
24046
- Source: ${source2}
23911
+ Source: ${source3}
24047
23912
  ${entry.content}`;
24048
23913
  }).join("\n\n");
24049
23914
  this.send({
@@ -24153,7 +24018,7 @@ Backup saved: ${result.backupPath}` : "";
24153
24018
  return;
24154
24019
  }
24155
24020
  try {
24156
- const { searchChatMemory: searchChatMemory2, loadChatIndex: loadChatIndex2 } = await import("./chat-index-DGUOUUQW.js");
24021
+ const { searchChatMemory: searchChatMemory2, loadChatIndex: loadChatIndex2 } = await import("./chat-index-RLVLQRRC.js");
24157
24022
  const loaded = loadChatIndex2();
24158
24023
  if (!loaded || loaded.idx.chunks.length === 0) {
24159
24024
  this.send({ type: "memory_hits", query: q, hits: [], indexMissing: true });
@@ -24189,7 +24054,7 @@ Backup saved: ${result.backupPath}` : "";
24189
24054
  }
24190
24055
  async handleMemoryStatus() {
24191
24056
  try {
24192
- const { getChatIndexStatus: getChatIndexStatus2 } = await import("./chat-index-DGUOUUQW.js");
24057
+ const { getChatIndexStatus: getChatIndexStatus2 } = await import("./chat-index-RLVLQRRC.js");
24193
24058
  const s = getChatIndexStatus2();
24194
24059
  this.send({
24195
24060
  type: "memory_status",
@@ -24214,7 +24079,7 @@ Backup saved: ${result.backupPath}` : "";
24214
24079
  type: "info",
24215
24080
  message: full ? "\u{1F9E0} Rebuilding chat memory index (this may take a while on first run \u2014 ~117 MB embedder)." : "\u{1F9E0} Refreshing chat memory index (incremental)\u2026"
24216
24081
  });
24217
- const { buildChatIndex: buildChatIndex2 } = await import("./chat-index-DGUOUUQW.js");
24082
+ const { buildChatIndex: buildChatIndex2 } = await import("./chat-index-RLVLQRRC.js");
24218
24083
  const stats = await buildChatIndex2({
24219
24084
  full,
24220
24085
  onProgress: (p) => {
@@ -24372,9 +24237,9 @@ async function setupProxy(configProxy) {
24372
24237
  }
24373
24238
 
24374
24239
  // src/web/auth.ts
24375
- import { existsSync as existsSync31, readFileSync as readFileSync24, writeFileSync as writeFileSync6, mkdirSync as mkdirSync16, readdirSync as readdirSync14, copyFileSync } from "fs";
24376
- import { isAbsolute as isAbsolute3, join as join27, relative as relative5 } from "path";
24377
- import { createHmac, randomBytes, timingSafeEqual, pbkdf2Sync } from "crypto";
24240
+ import { existsSync as existsSync30, readFileSync as readFileSync21, mkdirSync as mkdirSync16, readdirSync as readdirSync14, openSync, closeSync, unlinkSync as unlinkSync8 } from "fs";
24241
+ import { isAbsolute as isAbsolute3, join as join24, relative as relative5 } from "path";
24242
+ import { createHmac, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2, pbkdf2Sync, pbkdf2 } from "crypto";
24378
24243
  var USERS_FILE = "users.json";
24379
24244
  var TOKEN_EXPIRY_HOURS = 24;
24380
24245
  var TOKEN_EXPIRY_MS = TOKEN_EXPIRY_HOURS * 3600 * 1e3;
@@ -24386,14 +24251,16 @@ var AuthManager = class {
24386
24251
  usersFile;
24387
24252
  baseDir;
24388
24253
  db;
24254
+ loadedRaw;
24389
24255
  constructor(baseDir) {
24390
24256
  this.baseDir = baseDir;
24391
- this.usersFile = join27(baseDir, USERS_FILE);
24257
+ this.usersFile = join24(baseDir, USERS_FILE);
24392
24258
  this.db = this.loadOrCreate();
24393
24259
  }
24394
24260
  // ── Public API ─────────────────────────────────────────────────
24395
24261
  /** Check if any users exist (first-run detection) */
24396
24262
  hasUsers() {
24263
+ this.refresh();
24397
24264
  return this.db.users.length > 0;
24398
24265
  }
24399
24266
  /** Check if auth is enabled (has users.json with at least 1 user) */
@@ -24402,6 +24269,18 @@ var AuthManager = class {
24402
24269
  }
24403
24270
  /** Register a new user. Returns error string or null on success. */
24404
24271
  register(username, password) {
24272
+ return this.registerPrepared(username, password);
24273
+ }
24274
+ async registerAsync(username, password) {
24275
+ if (typeof username !== "string" || typeof password !== "string" || password.length > 1024) return "Invalid username or password";
24276
+ if (!/^[a-z0-9_-]{2,32}$/i.test(username.trim()) || password.length < 4) return "Invalid username or password";
24277
+ const salt = randomBytes2(16).toString("hex");
24278
+ const hash = await hashPasswordAsync(password, salt);
24279
+ return this.registerPrepared(username, password, { salt, hash });
24280
+ }
24281
+ registerPrepared(username, password, prepared) {
24282
+ this.refresh();
24283
+ if (typeof username !== "string" || typeof password !== "string" || password.length > 1024) return "Invalid username or password";
24405
24284
  username = username.trim().toLowerCase();
24406
24285
  if (!username || username.length < 2 || username.length > 32) {
24407
24286
  return "Username must be 2-32 characters";
@@ -24415,11 +24294,12 @@ var AuthManager = class {
24415
24294
  if (this.db.users.find((u) => u.username === username)) {
24416
24295
  return "Username already exists";
24417
24296
  }
24418
- const salt = randomBytes(16).toString("hex");
24419
- const passwordHash = this.hashPassword(password, salt);
24420
- const dataDir = join27(USERS_DIR, username);
24421
- const fullDataDir = join27(this.baseDir, dataDir);
24422
- mkdirSync16(join27(fullDataDir, "history"), { recursive: true });
24297
+ const salt = prepared?.salt ?? randomBytes2(16).toString("hex");
24298
+ const passwordHash = prepared?.hash ?? this.hashPassword(password, salt);
24299
+ const dataDir = join24(USERS_DIR, username);
24300
+ const fullDataDir = join24(this.baseDir, dataDir);
24301
+ mkdirSync16(fullDataDir, { recursive: true, mode: 448 });
24302
+ mkdirSync16(join24(fullDataDir, "history"), { recursive: true, mode: 448 });
24423
24303
  const user = {
24424
24304
  username,
24425
24305
  passwordHash,
@@ -24442,6 +24322,8 @@ var AuthManager = class {
24442
24322
  * resets the counter.
24443
24323
  */
24444
24324
  login(username, password) {
24325
+ this.refresh();
24326
+ if (!validCredentials(username, password)) return null;
24445
24327
  username = username.trim().toLowerCase();
24446
24328
  const lockState = this.getLockState(username);
24447
24329
  if (lockState.locked) return null;
@@ -24454,30 +24336,62 @@ var AuthManager = class {
24454
24336
  const hash = isLegacy ? this.hashPasswordLegacy(password, user.salt) : this.hashPassword(password, user.salt);
24455
24337
  const a = Buffer.from(hash, "utf-8");
24456
24338
  const b = Buffer.from(user.passwordHash, "utf-8");
24457
- if (a.length !== b.length || !timingSafeEqual(a, b)) {
24339
+ if (a.length !== b.length || !timingSafeEqual2(a, b)) {
24458
24340
  this.recordFailedLogin(username);
24459
24341
  return null;
24460
24342
  }
24461
24343
  if (isLegacy) {
24462
- const newSalt = randomBytes(16).toString("hex");
24344
+ const newSalt = randomBytes2(16).toString("hex");
24463
24345
  user.passwordHash = this.hashPassword(password, newSalt);
24464
24346
  user.salt = newSalt;
24465
24347
  user.hashVersion = 2;
24466
24348
  this.save();
24467
24349
  }
24468
- loginAttempts.delete(username);
24350
+ loginAttempts.delete(this.attemptKey(username));
24469
24351
  return this.createToken(username);
24470
24352
  }
24471
24353
  /**
24472
24354
  * Returns current lockout state for a username and lazily expires it.
24473
24355
  * Exposed (read-only) for tests and the `aicli user` CLI status output.
24474
24356
  */
24357
+ async loginAsync(username, password) {
24358
+ this.refresh();
24359
+ if (!validCredentials(username, password)) return null;
24360
+ username = username.trim().toLowerCase();
24361
+ if (this.getLockState(username).locked) return null;
24362
+ const user = this.db.users.find((u) => u.username === username);
24363
+ if (!user) {
24364
+ this.recordFailedLogin(username);
24365
+ return null;
24366
+ }
24367
+ const snapshot = JSON.stringify(user);
24368
+ const legacy = !user.hashVersion || user.hashVersion < 2;
24369
+ const hash = legacy ? this.hashPasswordLegacy(password, user.salt) : await hashPasswordAsync(password, user.salt);
24370
+ const salt = legacy ? randomBytes2(16).toString("hex") : user.salt;
24371
+ const upgraded = legacy ? await hashPasswordAsync(password, salt) : hash;
24372
+ this.refresh();
24373
+ const current = this.db.users.find((u) => u.username === username);
24374
+ if (!current || JSON.stringify(current) !== snapshot || this.getLockState(username).locked) return null;
24375
+ const a = Buffer.from(hash), b = Buffer.from(current.passwordHash);
24376
+ if (a.length !== b.length || !timingSafeEqual2(a, b)) {
24377
+ this.recordFailedLogin(username);
24378
+ return null;
24379
+ }
24380
+ if (legacy) {
24381
+ current.salt = salt;
24382
+ current.passwordHash = upgraded;
24383
+ current.hashVersion = 2;
24384
+ this.save();
24385
+ }
24386
+ loginAttempts.delete(this.attemptKey(username));
24387
+ return this.createToken(username);
24388
+ }
24475
24389
  getLockState(username) {
24476
24390
  username = username.trim().toLowerCase();
24477
- const state2 = loginAttempts.get(username);
24391
+ const state2 = loginAttempts.get(this.attemptKey(username));
24478
24392
  if (!state2) return { locked: false, remainingMs: 0, fails: 0 };
24479
- if (state2.lockedUntil > 0 && Date.now() >= state2.lockedUntil) {
24480
- loginAttempts.delete(username);
24393
+ if (Date.now() >= state2.expiresAt) {
24394
+ loginAttempts.delete(this.attemptKey(username));
24481
24395
  return { locked: false, remainingMs: 0, fails: 0 };
24482
24396
  }
24483
24397
  if (state2.lockedUntil > 0) {
@@ -24485,8 +24399,15 @@ var AuthManager = class {
24485
24399
  }
24486
24400
  return { locked: false, remainingMs: 0, fails: state2.fails };
24487
24401
  }
24402
+ attemptKey(username) {
24403
+ return this.usersFile + "\0" + username;
24404
+ }
24488
24405
  recordFailedLogin(username) {
24489
- const state2 = loginAttempts.get(username) ?? { fails: 0, lockedUntil: 0 };
24406
+ username = this.attemptKey(username);
24407
+ const now = Date.now();
24408
+ for (const [key, value] of loginAttempts) if (value.expiresAt <= now) loginAttempts.delete(key);
24409
+ if (!loginAttempts.has(username) && loginAttempts.size >= 1e4) loginAttempts.delete(loginAttempts.keys().next().value);
24410
+ const state2 = loginAttempts.get(username) ?? { fails: 0, lockedUntil: 0, expiresAt: now + LOGIN_LOCKOUT_MS };
24490
24411
  state2.fails += 1;
24491
24412
  if (state2.fails >= LOGIN_MAX_FAILS) {
24492
24413
  state2.lockedUntil = Date.now() + LOGIN_LOCKOUT_MS;
@@ -24496,17 +24417,19 @@ var AuthManager = class {
24496
24417
  /** Verify a token. Returns username or null. */
24497
24418
  verifyToken(token) {
24498
24419
  try {
24420
+ this.refresh();
24421
+ if (typeof token !== "string" || token.length > 4096) return null;
24499
24422
  const parts = token.split(".");
24500
24423
  if (parts.length !== 2) return null;
24501
24424
  const [payloadB64, signature] = parts;
24502
24425
  const expectedSig = this.sign(payloadB64);
24503
24426
  const sigBuf = Buffer.from(signature ?? "", "utf-8");
24504
24427
  const expBuf = Buffer.from(expectedSig, "utf-8");
24505
- if (sigBuf.length !== expBuf.length || !timingSafeEqual(sigBuf, expBuf)) return null;
24428
+ if (sigBuf.length !== expBuf.length || !timingSafeEqual2(sigBuf, expBuf)) return null;
24506
24429
  const payload = JSON.parse(
24507
24430
  Buffer.from(payloadB64, "base64url").toString("utf-8")
24508
24431
  );
24509
- if (Date.now() > payload.exp) return null;
24432
+ if (!Number.isFinite(payload.exp) || Date.now() >= payload.exp) return null;
24510
24433
  const user = this.db.users.find((u) => u.username === payload.username);
24511
24434
  if (!user) return null;
24512
24435
  if (user.tokensRevokedBefore && (!payload.iat || payload.iat < user.tokensRevokedBefore)) {
@@ -24523,6 +24446,7 @@ var AuthManager = class {
24523
24446
  * v0.4.114). Returns true if the user existed.
24524
24447
  */
24525
24448
  logoutAll(username) {
24449
+ this.refresh();
24526
24450
  username = username.trim().toLowerCase();
24527
24451
  const user = this.db.users.find((u) => u.username === username);
24528
24452
  if (!user) return false;
@@ -24532,16 +24456,19 @@ var AuthManager = class {
24532
24456
  }
24533
24457
  /** Get user's data directory (absolute path) */
24534
24458
  getUserDataDir(username) {
24459
+ this.refresh();
24535
24460
  const user = this.db.users.find((u) => u.username === username);
24536
24461
  if (!user) throw new Error(`User not found: ${username}`);
24537
- return join27(this.baseDir, user.dataDir);
24462
+ return join24(this.baseDir, user.dataDir);
24538
24463
  }
24539
24464
  /** List all usernames */
24540
24465
  listUsers() {
24466
+ this.refresh();
24541
24467
  return this.db.users.map((u) => u.username);
24542
24468
  }
24543
24469
  /** Delete a user. Returns error string or null on success. */
24544
24470
  deleteUser(username) {
24471
+ this.refresh();
24545
24472
  username = username.trim().toLowerCase();
24546
24473
  const idx = this.db.users.findIndex((u) => u.username === username);
24547
24474
  if (idx === -1) return `User '${username}' not found`;
@@ -24551,13 +24478,14 @@ var AuthManager = class {
24551
24478
  }
24552
24479
  /** Reset a user's password. Returns error string or null on success. */
24553
24480
  resetPassword(username, newPassword) {
24481
+ this.refresh();
24554
24482
  username = username.trim().toLowerCase();
24555
24483
  const user = this.db.users.find((u) => u.username === username);
24556
24484
  if (!user) return `User '${username}' not found`;
24557
- if (!newPassword || newPassword.length < 4) {
24485
+ if (typeof newPassword !== "string" || newPassword.length > 1024 || newPassword.length < 4) {
24558
24486
  return "Password must be at least 4 characters";
24559
24487
  }
24560
- const salt = randomBytes(16).toString("hex");
24488
+ const salt = randomBytes2(16).toString("hex");
24561
24489
  user.passwordHash = this.hashPassword(newPassword, salt);
24562
24490
  user.salt = salt;
24563
24491
  user.hashVersion = 2;
@@ -24570,30 +24498,30 @@ var AuthManager = class {
24570
24498
  const err = this.register(username, password);
24571
24499
  if (err) return err;
24572
24500
  const userDir = this.getUserDataDir(username);
24573
- const globalConfig = join27(this.baseDir, "config.json");
24574
- if (existsSync31(globalConfig)) {
24501
+ const globalConfig = join24(this.baseDir, "config.json");
24502
+ if (existsSync30(globalConfig)) {
24575
24503
  try {
24576
- const content = readFileSync24(globalConfig, "utf-8");
24577
- writeFileSync6(join27(userDir, "config.json"), content, "utf-8");
24504
+ const content = readFileSync21(globalConfig, "utf-8");
24505
+ atomicWriteFileSync(join24(userDir, "config.json"), content, { mode: 384 });
24578
24506
  } catch {
24579
24507
  }
24580
24508
  }
24581
- const globalMemory = join27(this.baseDir, "memory.md");
24582
- if (existsSync31(globalMemory)) {
24509
+ const globalMemory = join24(this.baseDir, "memory.md");
24510
+ if (existsSync30(globalMemory)) {
24583
24511
  try {
24584
- const content = readFileSync24(globalMemory, "utf-8");
24585
- writeFileSync6(join27(userDir, "memory.md"), content, "utf-8");
24512
+ const content = readFileSync21(globalMemory, "utf-8");
24513
+ atomicWriteFileSync(join24(userDir, "memory.md"), content, { mode: 384 });
24586
24514
  } catch {
24587
24515
  }
24588
24516
  }
24589
- const globalHistory = join27(this.baseDir, "history");
24590
- if (existsSync31(globalHistory)) {
24517
+ const globalHistory = join24(this.baseDir, "history");
24518
+ if (existsSync30(globalHistory)) {
24591
24519
  try {
24592
24520
  const files = readdirSync14(globalHistory).filter((f) => f.endsWith(".json"));
24593
- const userHistory = join27(userDir, "history");
24521
+ const userHistory = join24(userDir, "history");
24594
24522
  for (const f of files) {
24595
24523
  try {
24596
- copyFileSync(join27(globalHistory, f), join27(userHistory, f));
24524
+ atomicWriteFileSync(join24(userHistory, f), readFileSync21(join24(globalHistory, f), "utf8"), { mode: 384 });
24597
24525
  } catch {
24598
24526
  }
24599
24527
  }
@@ -24603,13 +24531,22 @@ var AuthManager = class {
24603
24531
  return null;
24604
24532
  }
24605
24533
  // ── Private methods ────────────────────────────────────────────
24534
+ refresh() {
24535
+ const raw = readFileSync21(this.usersFile, "utf8");
24536
+ const parsed = JSON.parse(raw);
24537
+ if (!this.isValidUsersDB(parsed)) throw new Error("Authentication database is invalid; access refused");
24538
+ this.db = parsed;
24539
+ this.loadedRaw = raw;
24540
+ }
24606
24541
  loadOrCreate() {
24607
- if (existsSync31(this.usersFile)) {
24542
+ if (existsSync30(this.usersFile)) {
24608
24543
  try {
24609
- const parsed = JSON.parse(readFileSync24(this.usersFile, "utf-8"));
24544
+ const raw = readFileSync21(this.usersFile, "utf-8");
24545
+ const parsed = JSON.parse(raw);
24610
24546
  if (!this.isValidUsersDB(parsed)) {
24611
24547
  throw new Error("database structure is invalid");
24612
24548
  }
24549
+ this.loadedRaw = raw;
24613
24550
  return parsed;
24614
24551
  } catch (err) {
24615
24552
  const detail = err instanceof Error ? err.message : String(err);
@@ -24621,7 +24558,7 @@ var AuthManager = class {
24621
24558
  }
24622
24559
  const db = {
24623
24560
  version: 1,
24624
- secret: randomBytes(32).toString("hex"),
24561
+ secret: randomBytes2(32).toString("hex"),
24625
24562
  users: []
24626
24563
  };
24627
24564
  this.saveDB(db);
@@ -24640,7 +24577,7 @@ var AuthManager = class {
24640
24577
  if (typeof record.username !== "string" || !/^[a-z0-9_-]{2,32}$/.test(record.username) || usernames.has(record.username) || typeof record.passwordHash !== "string" || record.passwordHash.length === 0 || typeof record.salt !== "string" || record.salt.length === 0 || typeof record.createdAt !== "string" || typeof record.dataDir !== "string" || record.dataDir.length === 0 || isAbsolute3(record.dataDir)) {
24641
24578
  return false;
24642
24579
  }
24643
- const dataDirFromBase = relative5(this.baseDir, join27(this.baseDir, record.dataDir));
24580
+ const dataDirFromBase = relative5(this.baseDir, join24(this.baseDir, record.dataDir));
24644
24581
  if (dataDirFromBase.startsWith("..") || isAbsolute3(dataDirFromBase)) return false;
24645
24582
  if (record.hashVersion !== void 0 && record.hashVersion !== 1 && record.hashVersion !== 2) return false;
24646
24583
  if (record.tokensRevokedBefore !== void 0 && !Number.isFinite(record.tokensRevokedBefore)) return false;
@@ -24652,8 +24589,19 @@ var AuthManager = class {
24652
24589
  this.saveDB(this.db);
24653
24590
  }
24654
24591
  saveDB(db) {
24655
- mkdirSync16(this.baseDir, { recursive: true });
24656
- atomicWriteFileSync(this.usersFile, JSON.stringify(db, null, 2));
24592
+ mkdirSync16(this.baseDir, { recursive: true, mode: 448 });
24593
+ const lock = this.usersFile + ".lock";
24594
+ const fd = openSync(lock, "wx", 384);
24595
+ try {
24596
+ const current = existsSync30(this.usersFile) ? readFileSync21(this.usersFile, "utf8") : void 0;
24597
+ if (current !== this.loadedRaw) throw new Error("Authentication database changed concurrently; retry the operation");
24598
+ const raw = JSON.stringify(db, null, 2);
24599
+ atomicWriteFileSync(this.usersFile, raw, { mode: 384 });
24600
+ this.loadedRaw = raw;
24601
+ } finally {
24602
+ closeSync(fd);
24603
+ unlinkSync8(lock);
24604
+ }
24657
24605
  }
24658
24606
  /** Legacy hash — kept only for migrating old users (v0.2.x) */
24659
24607
  hashPasswordLegacy(password, salt) {
@@ -24677,6 +24625,19 @@ var AuthManager = class {
24677
24625
  return createHmac("sha256", this.db.secret).update(data).digest("base64url");
24678
24626
  }
24679
24627
  };
24628
+ function validCredentials(username, password) {
24629
+ return typeof username === "string" && /^[a-z0-9_-]{2,32}$/i.test(username.trim()) && typeof password === "string" && password.length > 0 && password.length <= 1024;
24630
+ }
24631
+ var activeHashes = 0;
24632
+ async function hashPasswordAsync(password, salt) {
24633
+ if (activeHashes >= 4) throw new Error("Authentication capacity reached; try again later");
24634
+ activeHashes++;
24635
+ try {
24636
+ return await new Promise((resolve13, reject) => pbkdf2(password, salt, 1e5, 64, "sha512", (error, hash) => error ? reject(error) : resolve13(hash.toString("hex"))));
24637
+ } finally {
24638
+ activeHashes--;
24639
+ }
24640
+ }
24680
24641
 
24681
24642
  // src/web/server.ts
24682
24643
  function getModuleDir() {
@@ -24696,6 +24657,10 @@ async function startWebServer(options = {}) {
24696
24657
  console.log(`\u{1F916} ai-cli Web UI v${VERSION}`);
24697
24658
  console.log(` Initializing...`);
24698
24659
  const config = new ConfigManager();
24660
+ const authManager = new AuthManager(config.getConfigDir());
24661
+ if (!authManager.isEnabled() && !["localhost", "127.0.0.1", "::1"].includes(host)) {
24662
+ throw new Error("Network listening requires authentication. Create a user with aicli user create first.");
24663
+ }
24699
24664
  await setupProxy(config.get("proxy"));
24700
24665
  const providers = new ProviderRegistry();
24701
24666
  const sessions = new SessionManager(config);
@@ -24729,7 +24694,7 @@ async function startWebServer(options = {}) {
24729
24694
  const projectMcpResolved = mcpEnabled ? resolveProjectMcpPath() : null;
24730
24695
  let projectMcpServers = {};
24731
24696
  if (projectMcpResolved) {
24732
- const { checkTrust } = await import("./project-trust-MUG325AW.js");
24697
+ const { checkTrust } = await import("./project-trust-6CJB5ZMX.js");
24733
24698
  const verdict = checkTrust(config.getConfigDir(), projectMcpResolved);
24734
24699
  if (verdict.trusted) {
24735
24700
  projectMcpServers = loadProjectMcpConfig() ?? {};
@@ -24756,8 +24721,8 @@ async function startWebServer(options = {}) {
24756
24721
  }
24757
24722
  }
24758
24723
  let skillManager = null;
24759
- const skillsDir = join28(config.getConfigDir(), SKILLS_DIR_NAME);
24760
- if (existsSync32(skillsDir)) {
24724
+ const skillsDir = join25(config.getConfigDir(), SKILLS_DIR_NAME);
24725
+ if (existsSync31(skillsDir)) {
24761
24726
  skillManager = new SkillManager(skillsDir, config.get("ui").skillSizeWarn);
24762
24727
  skillManager.loadSkills(pluginAssets.skillDirs);
24763
24728
  const count = skillManager.listSkills().length;
@@ -24776,6 +24741,16 @@ async function startWebServer(options = {}) {
24776
24741
  skillManager
24777
24742
  };
24778
24743
  const app = express();
24744
+ const access = new WebAccess(host, options.externalOrigin);
24745
+ const authLimits = new AuthRateLimiter();
24746
+ app.use((req, res, next) => {
24747
+ if (!access.accepts(req)) {
24748
+ res.status(403).json({ error: "Untrusted request origin or host" });
24749
+ return;
24750
+ }
24751
+ res.locals.externalSecure = !!options.externalOrigin?.startsWith("https://") && req.headers.host === new URL(options.externalOrigin).host;
24752
+ next();
24753
+ });
24779
24754
  app.use((_req, res, next) => {
24780
24755
  res.setHeader(
24781
24756
  "Content-Security-Policy",
@@ -24804,19 +24779,23 @@ async function startWebServer(options = {}) {
24804
24779
  const WS_MSG_RATE_PER_SEC = 30;
24805
24780
  const WS_MSG_BURST = 60;
24806
24781
  const WS_PREAUTH_TIMEOUT_MS = 3e4;
24807
- const wss = new WebSocketServer({ server, maxPayload: WS_MAX_PAYLOAD });
24808
- const authManager = new AuthManager(config.getConfigDir());
24782
+ const wss = new WebSocketServer({ server, maxPayload: WS_MAX_PAYLOAD, verifyClient: ({ req }) => {
24783
+ if (!access.accepts(req) || wss.clients.size >= 128) return false;
24784
+ if (req.headers.origin) return true;
24785
+ const cookie = parseCookie(req.headers.cookie ?? "");
24786
+ return equalToken(cookie.aicli_local, access.localToken) || !!authManager.verifyToken(cookie.aicli_token ?? "");
24787
+ } });
24809
24788
  if (authManager.isEnabled()) {
24810
24789
  console.log(` Auth: ${authManager.listUsers().length} user(s) registered`);
24811
24790
  } else {
24812
- console.log(` Auth: disabled (no users registered)`);
24813
- if (host === "0.0.0.0" || host === "::") {
24814
- console.log(" WARNING: Web UI is reachable from the network without authentication.");
24815
- console.log(" Create a user first for LAN/team use: aicli user create <name>");
24816
- }
24791
+ console.log(" Auth: local launch token (no users registered)");
24817
24792
  }
24818
24793
  const requireAuth = (req, res, next) => {
24819
- if (!authManager.isEnabled()) return next();
24794
+ if (!authManager.isEnabled()) {
24795
+ if (equalToken(parseCookie(req.headers.cookie ?? "").aicli_local, access.localToken)) return next();
24796
+ res.status(401).json({ error: "Open the launch URL printed by aicli to authorize this browser" });
24797
+ return;
24798
+ }
24820
24799
  const authHeader = req.headers.authorization;
24821
24800
  const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
24822
24801
  const cookieToken = parseCookie(req.headers.cookie ?? "")["aicli_token"];
@@ -24834,19 +24813,20 @@ async function startWebServer(options = {}) {
24834
24813
  next();
24835
24814
  };
24836
24815
  const moduleDir = getModuleDir();
24837
- let clientDir = join28(moduleDir, "web", "client");
24838
- if (!existsSync32(clientDir)) {
24839
- clientDir = join28(moduleDir, "client");
24816
+ let clientDir = join25(moduleDir, "web", "client");
24817
+ if (!existsSync31(clientDir)) {
24818
+ clientDir = join25(moduleDir, "client");
24840
24819
  }
24841
- if (!existsSync32(clientDir)) {
24842
- clientDir = join28(moduleDir, "..", "..", "src", "web", "client");
24820
+ if (!existsSync31(clientDir)) {
24821
+ clientDir = join25(moduleDir, "..", "..", "src", "web", "client");
24843
24822
  }
24844
- if (!existsSync32(clientDir)) {
24845
- clientDir = join28(process.cwd(), "src", "web", "client");
24823
+ if (!existsSync31(clientDir)) {
24824
+ clientDir = join25(process.cwd(), "src", "web", "client");
24846
24825
  }
24847
24826
  console.log(` Static files: ${clientDir}`);
24848
24827
  const PRECOMPRESSED_TYPES = {
24849
24828
  ".js": "text/javascript; charset=utf-8",
24829
+ ".mjs": "text/javascript; charset=utf-8",
24850
24830
  ".css": "text/css; charset=utf-8",
24851
24831
  ".html": "text/html; charset=utf-8",
24852
24832
  ".json": "application/json; charset=utf-8",
@@ -24861,14 +24841,14 @@ async function startWebServer(options = {}) {
24861
24841
  const encoding = /\bbr\b/.test(accept) ? "br" : /\bgzip\b/.test(accept) ? "gzip" : null;
24862
24842
  if (!encoding) return next();
24863
24843
  const target = resolvePath(clientDir, `.${req.path}`) + (encoding === "br" ? ".br" : ".gz");
24864
- if (!target.startsWith(clientDir) || !existsSync32(target)) return next();
24844
+ if (!target.startsWith(clientDir) || !existsSync31(target)) return next();
24865
24845
  res.setHeader("Content-Type", contentType);
24866
24846
  res.setHeader("Content-Encoding", encoding);
24867
24847
  res.setHeader("Vary", "Accept-Encoding");
24868
24848
  if (req.path.startsWith("/vendor/")) res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
24869
24849
  createReadStream(target).pipe(res);
24870
24850
  });
24871
- app.use("/vendor", express.static(join28(clientDir, "vendor"), { maxAge: "1y", immutable: true }));
24851
+ app.use("/vendor", express.static(join25(clientDir, "vendor"), { maxAge: "1y", immutable: true }));
24872
24852
  app.use(express.static(clientDir));
24873
24853
  app.get("/api/status", (_req, res) => {
24874
24854
  res.json({
@@ -24887,9 +24867,28 @@ async function startWebServer(options = {}) {
24887
24867
  }
24888
24868
  });
24889
24869
  });
24890
- app.use(express.json());
24891
- app.post("/api/auth/register", (req, res) => {
24870
+ app.use(express.json({ limit: "16kb" }));
24871
+ app.post("/api/auth/bootstrap", (req, res) => {
24872
+ if (!equalToken(req.body?.token, access.localToken)) {
24873
+ res.status(403).json({ error: "Invalid launch token" });
24874
+ return;
24875
+ }
24876
+ res.cookie("aicli_local", access.localToken, { httpOnly: true, sameSite: "strict", secure: isHttpsRequest(req) });
24877
+ res.json({ success: true });
24878
+ });
24879
+ app.use("/api/auth", (req, res, next) => {
24880
+ if (!authLimits.take(req.socket.remoteAddress ?? "unknown")) {
24881
+ res.status(429).json({ error: "Too many authentication attempts; try again later" });
24882
+ return;
24883
+ }
24884
+ next();
24885
+ });
24886
+ app.post("/api/auth/register", async (req, res) => {
24892
24887
  const firstRun = !authManager.hasUsers();
24888
+ if (firstRun && !equalToken(parseCookie(req.headers.cookie ?? "").aicli_local, access.localToken)) {
24889
+ res.status(403).json({ error: "Open the launch URL before enabling authentication" });
24890
+ return;
24891
+ }
24893
24892
  if (!firstRun) {
24894
24893
  const authHeader = req.headers.authorization;
24895
24894
  const token2 = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
@@ -24905,23 +24904,24 @@ async function startWebServer(options = {}) {
24905
24904
  res.status(400).json({ error: "Username and password required" });
24906
24905
  return;
24907
24906
  }
24908
- const err = authManager.register(username, password);
24907
+ const err = await authManager.registerAsync(username, password);
24909
24908
  if (err) {
24910
24909
  res.status(400).json({ error: err });
24911
24910
  return;
24912
24911
  }
24913
- const token = authManager.login(username, password);
24912
+ const token = await authManager.loginAsync(username, password);
24913
+ invalidateConnections();
24914
24914
  console.log(` \u2713 User registered via API: ${username}${firstRun ? " (first-run)" : ""}`);
24915
24915
  res.cookie("aicli_token", token, authCookieOptions(req));
24916
24916
  res.json({ success: true, username });
24917
24917
  });
24918
- app.post("/api/auth/login", (req, res) => {
24918
+ app.post("/api/auth/login", async (req, res) => {
24919
24919
  const { username, password } = req.body ?? {};
24920
24920
  if (!username || !password) {
24921
24921
  res.status(400).json({ error: "Username and password required" });
24922
24922
  return;
24923
24923
  }
24924
- const token = authManager.login(username, password);
24924
+ const token = await authManager.loginAsync(username, password);
24925
24925
  if (!token) {
24926
24926
  res.status(401).json({ error: "Invalid username or password" });
24927
24927
  return;
@@ -24930,13 +24930,16 @@ async function startWebServer(options = {}) {
24930
24930
  res.json({ success: true, username });
24931
24931
  });
24932
24932
  app.post("/api/auth/logout", (req, res) => {
24933
+ const user = authManager.verifyToken(parseCookie(req.headers.cookie ?? "").aicli_token ?? "");
24934
+ if (user) authManager.logoutAll(user);
24935
+ invalidateConnections();
24933
24936
  res.clearCookie("aicli_token", clearAuthCookieOptions(req));
24934
24937
  res.json({ success: true });
24935
24938
  });
24936
24939
  app.get("/api/files", requireAuth, (req, res) => {
24937
24940
  const cwd = process.cwd();
24938
24941
  const prefix = req.query.prefix || "";
24939
- const targetDir = join28(cwd, prefix);
24942
+ const targetDir = join25(cwd, prefix);
24940
24943
  try {
24941
24944
  const canonicalTarget = realpathSync3(resolve12(targetDir));
24942
24945
  const canonicalCwd = realpathSync3(resolve12(cwd));
@@ -24953,7 +24956,7 @@ async function startWebServer(options = {}) {
24953
24956
  const entries = readdirSync15(targetDir, { withFileTypes: true });
24954
24957
  const files = entries.filter((e) => !SKIP.has(e.name) && !e.name.startsWith(".")).slice(0, 50).map((e) => ({
24955
24958
  name: e.name,
24956
- path: relative6(cwd, join28(targetDir, e.name)).replace(/\\/g, "/"),
24959
+ path: relative6(cwd, join25(targetDir, e.name)).replace(/\\/g, "/"),
24957
24960
  isDir: e.isDirectory()
24958
24961
  }));
24959
24962
  res.json({ files });
@@ -24993,8 +24996,8 @@ async function startWebServer(options = {}) {
24993
24996
  try {
24994
24997
  const authUser = req._authUser;
24995
24998
  const histDir = authUser ? getUserShared(authUser).config.getHistoryDir() : config.getHistoryDir();
24996
- const filePath = join28(histDir, `${id}.json`);
24997
- if (!existsSync32(filePath)) {
24999
+ const filePath = join25(histDir, `${id}.json`);
25000
+ if (!existsSync31(filePath)) {
24998
25001
  res.status(404).json({ error: "Session not found" });
24999
25002
  return;
25000
25003
  }
@@ -25009,7 +25012,7 @@ async function startWebServer(options = {}) {
25009
25012
  res.status(404).json({ error: "Session not found" });
25010
25013
  return;
25011
25014
  }
25012
- const data = JSON.parse(readFileSync25(filePath, "utf-8"));
25015
+ const data = JSON.parse(readFileSync22(filePath, "utf-8"));
25013
25016
  res.json({ session: data });
25014
25017
  } catch (err) {
25015
25018
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
@@ -25022,9 +25025,10 @@ async function startWebServer(options = {}) {
25022
25025
  return;
25023
25026
  }
25024
25027
  const cwd = process.cwd();
25025
- const fullPath = resolve12(join28(cwd, filePath));
25028
+ const fullPath = resolve12(join25(cwd, filePath));
25026
25029
  try {
25027
25030
  const canonicalFull = realpathSync3(fullPath);
25031
+ assertReadablePath(canonicalFull);
25028
25032
  const canonicalCwd = realpathSync3(resolve12(cwd));
25029
25033
  if (!canonicalFull.startsWith(canonicalCwd + sep5) && canonicalFull !== canonicalCwd) {
25030
25034
  res.json({ error: "Access denied" });
@@ -25040,7 +25044,7 @@ async function startWebServer(options = {}) {
25040
25044
  res.json({ error: `File too large (${(stat.size / 1024).toFixed(0)} KB, max 512 KB)` });
25041
25045
  return;
25042
25046
  }
25043
- const content = readFileSync25(fullPath, "utf-8");
25047
+ const content = readFileSync22(fullPath, "utf-8");
25044
25048
  res.json({ content, size: stat.size });
25045
25049
  } catch {
25046
25050
  res.json({ error: "Cannot read file" });
@@ -25052,6 +25056,9 @@ async function startWebServer(options = {}) {
25052
25056
  if (cached) return cached;
25053
25057
  const userDataDir = authManager.getUserDataDir(username);
25054
25058
  const userConfig = new ConfigManager(userDataDir);
25059
+ for (const key of ["defaultPermissionProfile", "allowedPermissionProfiles", "permissionProfiles", "permissionRules", "defaultPermission", "networkPolicy"]) {
25060
+ userConfig.setTransient(key, structuredClone(config.get(key)));
25061
+ }
25055
25062
  if (userConfig.isFirstRun()) {
25056
25063
  userConfig.setTransient("defaultProvider", config.get("defaultProvider"));
25057
25064
  userConfig.setTransient("defaultModels", config.get("defaultModels"));
@@ -25062,7 +25069,7 @@ async function startWebServer(options = {}) {
25062
25069
  // 每用户各自的 config 目录 → 各自的 cost-history.json
25063
25070
  costTracker: new CostTracker(userConfig.getConfigDir()),
25064
25071
  providers,
25065
- // Shared — providers re-read API keys per call
25072
+ // Host-owned service credentials; shared explicitly, never per-user keys.
25066
25073
  sessions: userSessions,
25067
25074
  toolRegistry,
25068
25075
  // Shared
@@ -25075,6 +25082,21 @@ async function startWebServer(options = {}) {
25075
25082
  return userShared;
25076
25083
  }
25077
25084
  const handlers = /* @__PURE__ */ new Map();
25085
+ const validators = /* @__PURE__ */ new Set();
25086
+ function invalidateConnections() {
25087
+ for (const validate of validators) validate();
25088
+ }
25089
+ let authRefreshTimer;
25090
+ server.on("listening", () => {
25091
+ if (!authRefreshTimer) {
25092
+ authRefreshTimer = setInterval(invalidateConnections, 1e3);
25093
+ authRefreshTimer.unref();
25094
+ }
25095
+ });
25096
+ server.on("close", () => {
25097
+ clearInterval(authRefreshTimer);
25098
+ authRefreshTimer = void 0;
25099
+ });
25078
25100
  wss.on("connection", (ws, req) => {
25079
25101
  const urlStr = req.url ?? "";
25080
25102
  const qMark = urlStr.indexOf("?");
@@ -25082,6 +25104,8 @@ async function startWebServer(options = {}) {
25082
25104
  const tabId = params.get("tabId") || `tab-${Date.now()}`;
25083
25105
  const cookies = parseCookie(req.headers.cookie ?? "");
25084
25106
  const token = cookies["aicli_token"] || "";
25107
+ let activeToken = token;
25108
+ const localAuthorized = equalToken(cookies.aicli_local, access.localToken);
25085
25109
  let tokens = WS_MSG_BURST;
25086
25110
  let lastRefill = Date.now();
25087
25111
  const takeToken = () => {
@@ -25141,7 +25165,7 @@ async function startWebServer(options = {}) {
25141
25165
  authenticatedUser = authManager.verifyToken(token);
25142
25166
  }
25143
25167
  const authRequired = authManager.isEnabled();
25144
- if (!authRequired) {
25168
+ if (!authRequired && localAuthorized) {
25145
25169
  console.log(` \u2713 Tab connected: ${tabId.slice(0, 12)} (no auth) (total: ${handlers.size + 1})`);
25146
25170
  handler = attachHandler(shared);
25147
25171
  clearPreAuthTimer();
@@ -25159,7 +25183,26 @@ async function startWebServer(options = {}) {
25159
25183
  }));
25160
25184
  }
25161
25185
  }
25186
+ const validate = () => {
25187
+ if (!handler) return;
25188
+ let valid = false;
25189
+ try {
25190
+ valid = authManager.isEnabled() ? !!activeToken && authManager.verifyToken(activeToken) === authenticatedUser : localAuthorized && !authenticatedUser;
25191
+ } catch {
25192
+ }
25193
+ if (!valid) {
25194
+ handler.onDisconnect(false);
25195
+ if (handlers.get(handlerKey) === handler) handlers.delete(handlerKey);
25196
+ handler = null;
25197
+ ws.close(1008, "Authentication changed; reconnect");
25198
+ }
25199
+ };
25200
+ validators.add(validate);
25201
+ ws.on("close", () => validators.delete(validate));
25202
+ let authBusy = false;
25162
25203
  ws.on("message", async (data) => {
25204
+ validate();
25205
+ if (ws.readyState !== ws.OPEN) return;
25163
25206
  if (!takeToken()) {
25164
25207
  if (ws.readyState === ws.OPEN) {
25165
25208
  try {
@@ -25179,57 +25222,73 @@ async function startWebServer(options = {}) {
25179
25222
  return;
25180
25223
  }
25181
25224
  if (parsed.type === "auth") {
25182
- const { action, username, password } = parsed;
25183
- if (action === "register") {
25184
- if (authManager.hasUsers() && !authenticatedUser) {
25185
- ws.send(JSON.stringify({ type: "auth_result", success: false, error: "Registration is closed. Use `aicli user create` on the host to add users." }));
25225
+ if (authBusy) return;
25226
+ authBusy = true;
25227
+ try {
25228
+ if (!authLimits.take(req.socket.remoteAddress ?? "unknown")) {
25229
+ ws.close(1008, "Authentication rate limit");
25186
25230
  return;
25187
25231
  }
25188
- const err = authManager.register(username, password);
25189
- if (err) {
25190
- ws.send(JSON.stringify({ type: "auth_result", success: false, error: err }));
25232
+ const { action, password } = parsed;
25233
+ const username = typeof parsed.username === "string" ? parsed.username.trim().toLowerCase() : "";
25234
+ if (action === "register") {
25235
+ if (!authenticatedUser && (authManager.hasUsers() || !localAuthorized)) {
25236
+ ws.send(JSON.stringify({ type: "auth_result", success: false, error: "Registration is closed. Use `aicli user create` on the host to add users." }));
25237
+ return;
25238
+ }
25239
+ const err = await authManager.registerAsync(username, password);
25240
+ if (err) {
25241
+ ws.send(JSON.stringify({ type: "auth_result", success: false, error: err }));
25242
+ return;
25243
+ }
25244
+ activeToken = await authManager.loginAsync(username, password) ?? "";
25245
+ if (ws.readyState !== ws.OPEN || authManager.verifyToken(activeToken) !== username) return;
25246
+ authenticatedUser = username;
25247
+ const userShared = getUserShared(username);
25248
+ handler = attachHandler(userShared);
25249
+ clearPreAuthTimer();
25250
+ invalidateConnections();
25251
+ console.log(` \u2713 User registered & connected: ${username} (tab: ${tabId.slice(0, 12)})`);
25252
+ ws.send(JSON.stringify({ type: "auth_result", success: true, username }));
25191
25253
  return;
25192
25254
  }
25193
- authManager.login(username, password);
25194
- authenticatedUser = username;
25195
- const userShared = getUserShared(username);
25196
- handler = attachHandler(userShared);
25197
- clearPreAuthTimer();
25198
- console.log(` \u2713 User registered & connected: ${username} (tab: ${tabId.slice(0, 12)})`);
25199
- ws.send(JSON.stringify({ type: "auth_result", success: true, username }));
25200
- return;
25201
- }
25202
- if (action === "token") {
25203
- const { token: clientToken } = parsed;
25204
- const verifiedUser = authManager.verifyToken(clientToken);
25205
- if (!verifiedUser) {
25206
- ws.send(JSON.stringify({ type: "auth_result", success: false, error: "Invalid or expired token" }));
25255
+ if (action === "token") {
25256
+ const { token: clientToken } = parsed;
25257
+ const verifiedUser = authManager.verifyToken(clientToken);
25258
+ if (!verifiedUser) {
25259
+ ws.send(JSON.stringify({ type: "auth_result", success: false, error: "Invalid or expired token" }));
25260
+ return;
25261
+ }
25262
+ activeToken = clientToken;
25263
+ authenticatedUser = verifiedUser;
25264
+ const userShared = getUserShared(verifiedUser);
25265
+ handler = attachHandler(userShared);
25266
+ clearPreAuthTimer();
25267
+ console.log(` \u2713 Token auth: ${verifiedUser} (tab: ${tabId.slice(0, 12)})`);
25268
+ ws.send(JSON.stringify({ type: "auth_result", success: true, username: verifiedUser }));
25207
25269
  return;
25208
25270
  }
25209
- authenticatedUser = verifiedUser;
25210
- const userShared = getUserShared(verifiedUser);
25211
- handler = attachHandler(userShared);
25212
- clearPreAuthTimer();
25213
- console.log(` \u2713 Token auth: ${verifiedUser} (tab: ${tabId.slice(0, 12)})`);
25214
- ws.send(JSON.stringify({ type: "auth_result", success: true, username: verifiedUser }));
25215
- return;
25216
- }
25217
- if (action === "login") {
25218
- const loginToken = authManager.login(username, password);
25219
- if (!loginToken) {
25220
- ws.send(JSON.stringify({ type: "auth_result", success: false, error: "Invalid username or password" }));
25271
+ if (action === "login") {
25272
+ const loginToken = await authManager.loginAsync(username, password);
25273
+ if (!loginToken) {
25274
+ ws.send(JSON.stringify({ type: "auth_result", success: false, error: "Invalid username or password" }));
25275
+ return;
25276
+ }
25277
+ if (ws.readyState !== ws.OPEN || authManager.verifyToken(loginToken) !== username) return;
25278
+ activeToken = loginToken;
25279
+ authenticatedUser = username;
25280
+ const userShared = getUserShared(username);
25281
+ handler = attachHandler(userShared);
25282
+ clearPreAuthTimer();
25283
+ console.log(` \u2713 User logged in: ${username} (tab: ${tabId.slice(0, 12)})`);
25284
+ ws.send(JSON.stringify({ type: "auth_result", success: true, username }));
25221
25285
  return;
25222
25286
  }
25223
- authenticatedUser = username;
25224
- const userShared = getUserShared(username);
25225
- handler = attachHandler(userShared);
25226
- clearPreAuthTimer();
25227
- console.log(` \u2713 User logged in: ${username} (tab: ${tabId.slice(0, 12)})`);
25228
- ws.send(JSON.stringify({ type: "auth_result", success: true, username }));
25287
+ ws.send(JSON.stringify({ type: "auth_result", success: false, error: "Unknown auth action" }));
25229
25288
  return;
25289
+ } finally {
25290
+ authBusy = false;
25230
25291
  }
25231
- ws.send(JSON.stringify({ type: "auth_result", success: false, error: "Unknown auth action" }));
25232
- return;
25233
25292
  }
25234
25293
  if (!handler) {
25235
25294
  if (ws.readyState === ws.OPEN) {
@@ -25275,13 +25334,15 @@ async function startWebServer(options = {}) {
25275
25334
  }
25276
25335
  });
25277
25336
  server.listen(actualPort, host, () => {
25278
- const url = `http://${host === "0.0.0.0" ? "localhost" : host}:${actualPort}`;
25337
+ actualPort = server.address().port;
25338
+ const address = host.includes(":") ? `[${host}]` : host === "0.0.0.0" ? "localhost" : host;
25339
+ const url = `http://${address}:${actualPort}` + (!authManager.isEnabled() ? `#token=${access.localToken}` : "");
25279
25340
  console.log(`
25280
25341
  \u{1F310} Web UI ready: ${url}`);
25281
25342
  if (actualPort !== port) console.log(` (port ${port} was in use, using ${actualPort})`);
25282
25343
  if (host === "0.0.0.0" || host === "::") {
25283
25344
  try {
25284
- const nets = networkInterfaces();
25345
+ const nets = networkInterfaces2();
25285
25346
  for (const name of Object.keys(nets)) {
25286
25347
  for (const net2 of nets[name] ?? []) {
25287
25348
  if (net2.family === "IPv4" && !net2.internal) {
@@ -25314,27 +25375,24 @@ function resolveProjectMcpPath() {
25314
25375
  const cwd = process.cwd();
25315
25376
  const gitRoot = getGitRoot(cwd);
25316
25377
  const projectRoot = gitRoot ?? cwd;
25317
- const configPath = join28(projectRoot, MCP_PROJECT_CONFIG_NAME);
25318
- return existsSync32(configPath) ? configPath : null;
25378
+ const configPath = join25(projectRoot, MCP_PROJECT_CONFIG_NAME);
25379
+ return existsSync31(configPath) ? configPath : null;
25319
25380
  }
25320
25381
  function loadProjectMcpConfig() {
25321
25382
  const cwd = process.cwd();
25322
25383
  const gitRoot = getGitRoot(cwd);
25323
25384
  const projectRoot = gitRoot ?? cwd;
25324
- const configPath = join28(projectRoot, MCP_PROJECT_CONFIG_NAME);
25325
- if (!existsSync32(configPath)) return null;
25385
+ const configPath = join25(projectRoot, MCP_PROJECT_CONFIG_NAME);
25386
+ if (!existsSync31(configPath)) return null;
25326
25387
  try {
25327
- const raw = JSON.parse(readFileSync25(configPath, "utf-8"));
25388
+ const raw = JSON.parse(readFileSync22(configPath, "utf-8"));
25328
25389
  return raw.mcpServers ?? raw;
25329
25390
  } catch {
25330
25391
  return null;
25331
25392
  }
25332
25393
  }
25333
25394
  function isHttpsRequest(req) {
25334
- if (req.secure) return true;
25335
- const forwardedProto = req.headers["x-forwarded-proto"];
25336
- const proto = Array.isArray(forwardedProto) ? forwardedProto[0] : forwardedProto;
25337
- return proto?.split(",")[0]?.trim().toLowerCase() === "https";
25395
+ return req.secure || req.res?.locals.externalSecure === true;
25338
25396
  }
25339
25397
  function authCookieOptions(req) {
25340
25398
  return {
@@ -25361,13 +25419,13 @@ function parseCookie(cookie) {
25361
25419
  }
25362
25420
  async function openBrowser(url) {
25363
25421
  try {
25364
- const { spawn: spawn7 } = await import("child_process");
25422
+ const { spawn: spawn6 } = await import("child_process");
25365
25423
  if (process.platform === "win32") {
25366
- spawn7("cmd", ["/c", "start", '""', url], { detached: true, stdio: "ignore" }).unref();
25424
+ spawn6("cmd", ["/c", "start", '""', url], { detached: true, stdio: "ignore" }).unref();
25367
25425
  } else if (process.platform === "darwin") {
25368
- spawn7("open", [url], { detached: true, stdio: "ignore" }).unref();
25426
+ spawn6("open", [url], { detached: true, stdio: "ignore" }).unref();
25369
25427
  } else {
25370
- spawn7("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
25428
+ spawn6("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
25371
25429
  }
25372
25430
  } catch {
25373
25431
  }