@wrongstack/tools 0.297.0 → 0.298.0

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 (253) hide show
  1. package/dist/_spawn-stream.d.ts +2 -0
  2. package/dist/_util.d.ts +29 -1
  3. package/dist/audit.js +113 -65
  4. package/dist/bash.js +5 -4
  5. package/dist/batch-tool-use.js +70 -7
  6. package/dist/browser/index.js +31 -1
  7. package/dist/builtin.js +841 -351
  8. package/dist/codebase-index/index.js +68 -16
  9. package/dist/codebase-index/project-server-protocol.d.ts +26 -0
  10. package/dist/codebase-index/project-server.js +60 -16
  11. package/dist/codebase-index/worker.js +36 -13
  12. package/dist/codebase-index/writer-search-helpers.d.ts +16 -0
  13. package/dist/codebase-index/writer.d.ts +13 -1
  14. package/dist/design.d.ts +1 -1
  15. package/dist/design.js +4 -3
  16. package/dist/diff.js +11 -1
  17. package/dist/e2e.js +9 -6
  18. package/dist/edit.js +9 -6
  19. package/dist/exec.js +560 -529
  20. package/dist/format.js +116 -65
  21. package/dist/git.js +22 -2
  22. package/dist/glob.js +11 -5
  23. package/dist/grep.js +160 -104
  24. package/dist/index.js +792 -290
  25. package/dist/install.js +120 -68
  26. package/dist/json.js +64 -18
  27. package/dist/kanban-board-inputs.d.ts +21 -4
  28. package/dist/kanban.js +20 -5
  29. package/dist/languages/index.js +128 -71
  30. package/dist/lint.js +113 -65
  31. package/dist/outdated.js +86 -26
  32. package/dist/pack.js +841 -351
  33. package/dist/patch.js +11 -1
  34. package/dist/plan.js +32 -10
  35. package/dist/read.js +77 -23
  36. package/dist/replace.js +11 -3
  37. package/dist/scaffold.js +9 -6
  38. package/dist/session-kanban.js +21 -5
  39. package/dist/task.js +3 -1
  40. package/dist/test.js +113 -65
  41. package/dist/todo.js +3 -1
  42. package/dist/tool-tier.js +841 -351
  43. package/dist/tool-use.d.ts +1 -1
  44. package/dist/tool-use.js +28 -4
  45. package/dist/tree.d.ts +1 -0
  46. package/dist/tree.js +44 -8
  47. package/dist/typecheck.js +113 -65
  48. package/dist/write.js +9 -6
  49. package/package.json +5 -4
  50. package/dist/_concurrency.d.ts.map +0 -1
  51. package/dist/_danger-detect.d.ts.map +0 -1
  52. package/dist/_edit-match.d.ts.map +0 -1
  53. package/dist/_env.d.ts.map +0 -1
  54. package/dist/_fetch-guard.d.ts.map +0 -1
  55. package/dist/_output-spool.d.ts.map +0 -1
  56. package/dist/_redact-command.d.ts.map +0 -1
  57. package/dist/_regex.d.ts.map +0 -1
  58. package/dist/_session-shell.d.ts.map +0 -1
  59. package/dist/_shell-pick.d.ts.map +0 -1
  60. package/dist/_spawn-stream.d.ts.map +0 -1
  61. package/dist/_syntax-check.d.ts.map +0 -1
  62. package/dist/_util.d.ts.map +0 -1
  63. package/dist/_win32-resolve.d.ts.map +0 -1
  64. package/dist/audit.d.ts.map +0 -1
  65. package/dist/audit.js.map +0 -7
  66. package/dist/auto-proceed-loop-guard.d.ts.map +0 -1
  67. package/dist/auto-proceed-loop-guard.js.map +0 -7
  68. package/dist/bash-kill-guard.d.ts.map +0 -1
  69. package/dist/bash.d.ts.map +0 -1
  70. package/dist/bash.js.map +0 -7
  71. package/dist/batch-tool-use.d.ts.map +0 -1
  72. package/dist/batch-tool-use.js.map +0 -7
  73. package/dist/browser/artifacts.d.ts.map +0 -1
  74. package/dist/browser/index.d.ts.map +0 -1
  75. package/dist/browser/index.js.map +0 -7
  76. package/dist/browser/manager.d.ts.map +0 -1
  77. package/dist/browser/network-guard-proxy.d.ts.map +0 -1
  78. package/dist/browser/security.d.ts.map +0 -1
  79. package/dist/browser/tools.d.ts.map +0 -1
  80. package/dist/browser/types.d.ts.map +0 -1
  81. package/dist/builtin.d.ts.map +0 -1
  82. package/dist/builtin.js.map +0 -7
  83. package/dist/circuit-breaker.d.ts.map +0 -1
  84. package/dist/circuit-breaker.js.map +0 -7
  85. package/dist/codebase-index/background-indexer.d.ts.map +0 -1
  86. package/dist/codebase-index/bm25.d.ts.map +0 -1
  87. package/dist/codebase-index/circuit-breaker.d.ts.map +0 -1
  88. package/dist/codebase-index/codebase-index-tool.d.ts.map +0 -1
  89. package/dist/codebase-index/codebase-search-tool.d.ts.map +0 -1
  90. package/dist/codebase-index/codebase-stats-tool.d.ts.map +0 -1
  91. package/dist/codebase-index/dead-code-scan.d.ts.map +0 -1
  92. package/dist/codebase-index/generic-parser.d.ts.map +0 -1
  93. package/dist/codebase-index/gitignore.d.ts.map +0 -1
  94. package/dist/codebase-index/go-parser.d.ts.map +0 -1
  95. package/dist/codebase-index/index-service.d.ts.map +0 -1
  96. package/dist/codebase-index/index.d.ts.map +0 -1
  97. package/dist/codebase-index/index.js.map +0 -7
  98. package/dist/codebase-index/indexer.d.ts.map +0 -1
  99. package/dist/codebase-index/json-parser.d.ts.map +0 -1
  100. package/dist/codebase-index/languages.d.ts.map +0 -1
  101. package/dist/codebase-index/lsp-kind.d.ts.map +0 -1
  102. package/dist/codebase-index/parser-dispatch.d.ts.map +0 -1
  103. package/dist/codebase-index/project-server-cache.d.ts.map +0 -1
  104. package/dist/codebase-index/project-server-client.d.ts.map +0 -1
  105. package/dist/codebase-index/project-server-endpoint.d.ts.map +0 -1
  106. package/dist/codebase-index/project-server-protocol.d.ts.map +0 -1
  107. package/dist/codebase-index/project-server.d.ts.map +0 -1
  108. package/dist/codebase-index/project-server.js.map +0 -7
  109. package/dist/codebase-index/py-parser.d.ts.map +0 -1
  110. package/dist/codebase-index/refs-extractor.d.ts.map +0 -1
  111. package/dist/codebase-index/rs-parser.d.ts.map +0 -1
  112. package/dist/codebase-index/schema.d.ts.map +0 -1
  113. package/dist/codebase-index/spawn-gate.d.ts.map +0 -1
  114. package/dist/codebase-index/sqlite-runtime.d.ts.map +0 -1
  115. package/dist/codebase-index/ts-parser.d.ts.map +0 -1
  116. package/dist/codebase-index/worker-protocol.d.ts.map +0 -1
  117. package/dist/codebase-index/worker.d.ts.map +0 -1
  118. package/dist/codebase-index/worker.js.map +0 -7
  119. package/dist/codebase-index/writer-admin.d.ts.map +0 -1
  120. package/dist/codebase-index/writer-bulk-insert.d.ts.map +0 -1
  121. package/dist/codebase-index/writer-graph-helpers.d.ts.map +0 -1
  122. package/dist/codebase-index/writer-graph-reader.d.ts.map +0 -1
  123. package/dist/codebase-index/writer-helpers.d.ts.map +0 -1
  124. package/dist/codebase-index/writer-pragmas.d.ts.map +0 -1
  125. package/dist/codebase-index/writer-ref-mapper.d.ts.map +0 -1
  126. package/dist/codebase-index/writer-schema.d.ts.map +0 -1
  127. package/dist/codebase-index/writer-search-helpers.d.ts.map +0 -1
  128. package/dist/codebase-index/writer-store-pool.d.ts.map +0 -1
  129. package/dist/codebase-index/writer.d.ts.map +0 -1
  130. package/dist/codebase-index/yaml-parser.d.ts.map +0 -1
  131. package/dist/design.d.ts.map +0 -1
  132. package/dist/design.js.map +0 -7
  133. package/dist/diff.d.ts.map +0 -1
  134. package/dist/diff.js.map +0 -7
  135. package/dist/document.d.ts.map +0 -1
  136. package/dist/document.js.map +0 -7
  137. package/dist/e2e.d.ts.map +0 -1
  138. package/dist/e2e.js.map +0 -7
  139. package/dist/edit.d.ts.map +0 -1
  140. package/dist/edit.js.map +0 -7
  141. package/dist/exec-kill-guard.d.ts.map +0 -1
  142. package/dist/exec.d.ts.map +0 -1
  143. package/dist/exec.js.map +0 -7
  144. package/dist/fetch.d.ts.map +0 -1
  145. package/dist/fetch.js.map +0 -7
  146. package/dist/format.d.ts.map +0 -1
  147. package/dist/format.js.map +0 -7
  148. package/dist/git.d.ts.map +0 -1
  149. package/dist/git.js.map +0 -7
  150. package/dist/glob.d.ts.map +0 -1
  151. package/dist/glob.js.map +0 -7
  152. package/dist/grep.d.ts.map +0 -1
  153. package/dist/grep.js.map +0 -7
  154. package/dist/index.d.ts.map +0 -1
  155. package/dist/index.js.map +0 -7
  156. package/dist/install.d.ts.map +0 -1
  157. package/dist/install.js.map +0 -7
  158. package/dist/json.d.ts.map +0 -1
  159. package/dist/json.js.map +0 -7
  160. package/dist/kanban-board-inputs.d.ts.map +0 -1
  161. package/dist/kanban-decomposition-actions.d.ts.map +0 -1
  162. package/dist/kanban-detail-actions.d.ts.map +0 -1
  163. package/dist/kanban-evidence-bridge.d.ts.map +0 -1
  164. package/dist/kanban-presence.d.ts.map +0 -1
  165. package/dist/kanban-split-task-handler.d.ts.map +0 -1
  166. package/dist/kanban-task-inputs.d.ts.map +0 -1
  167. package/dist/kanban-tool-results.d.ts.map +0 -1
  168. package/dist/kanban-tool-schema.d.ts.map +0 -1
  169. package/dist/kanban-tool-types.d.ts.map +0 -1
  170. package/dist/kanban.d.ts.map +0 -1
  171. package/dist/kanban.js.map +0 -7
  172. package/dist/languages/detect.d.ts.map +0 -1
  173. package/dist/languages/diagnostics.d.ts.map +0 -1
  174. package/dist/languages/execute-tool.d.ts.map +0 -1
  175. package/dist/languages/execute.d.ts.map +0 -1
  176. package/dist/languages/index.d.ts.map +0 -1
  177. package/dist/languages/index.js.map +0 -7
  178. package/dist/languages/legacy-bridge.d.ts.map +0 -1
  179. package/dist/languages/package-tool.d.ts.map +0 -1
  180. package/dist/languages/plan.d.ts.map +0 -1
  181. package/dist/languages/profile-helpers.d.ts.map +0 -1
  182. package/dist/languages/profiles/additional.d.ts.map +0 -1
  183. package/dist/languages/profiles/primary.d.ts.map +0 -1
  184. package/dist/languages/registry.d.ts.map +0 -1
  185. package/dist/languages/tool.d.ts.map +0 -1
  186. package/dist/languages/types.d.ts.map +0 -1
  187. package/dist/lint.d.ts.map +0 -1
  188. package/dist/lint.js.map +0 -7
  189. package/dist/logs.d.ts.map +0 -1
  190. package/dist/logs.js.map +0 -7
  191. package/dist/memory.d.ts.map +0 -1
  192. package/dist/memory.js.map +0 -7
  193. package/dist/mode.d.ts.map +0 -1
  194. package/dist/mode.js.map +0 -7
  195. package/dist/next-steps.d.ts.map +0 -1
  196. package/dist/next-steps.js.map +0 -7
  197. package/dist/outdated.d.ts.map +0 -1
  198. package/dist/outdated.js.map +0 -7
  199. package/dist/pack.d.ts.map +0 -1
  200. package/dist/pack.js.map +0 -7
  201. package/dist/patch.d.ts.map +0 -1
  202. package/dist/patch.js.map +0 -7
  203. package/dist/plan.d.ts.map +0 -1
  204. package/dist/plan.js.map +0 -7
  205. package/dist/process-guardian.d.ts.map +0 -1
  206. package/dist/process-registry-persistent.d.ts.map +0 -1
  207. package/dist/process-registry.d.ts.map +0 -1
  208. package/dist/process-registry.js.map +0 -7
  209. package/dist/ps-slash.d.ts.map +0 -1
  210. package/dist/ps-slash.js.map +0 -7
  211. package/dist/read.d.ts.map +0 -1
  212. package/dist/read.js.map +0 -7
  213. package/dist/replace.d.ts.map +0 -1
  214. package/dist/replace.js.map +0 -7
  215. package/dist/scaffold.d.ts.map +0 -1
  216. package/dist/scaffold.js.map +0 -7
  217. package/dist/search.d.ts.map +0 -1
  218. package/dist/search.js.map +0 -7
  219. package/dist/session-kanban.d.ts.map +0 -1
  220. package/dist/session-kanban.js.map +0 -7
  221. package/dist/set-working-dir.d.ts.map +0 -1
  222. package/dist/skill.d.ts.map +0 -1
  223. package/dist/skill.js.map +0 -7
  224. package/dist/spawn-background.d.ts.map +0 -1
  225. package/dist/task.d.ts.map +0 -1
  226. package/dist/task.js.map +0 -7
  227. package/dist/test.d.ts.map +0 -1
  228. package/dist/test.js.map +0 -7
  229. package/dist/todo.d.ts.map +0 -1
  230. package/dist/todo.js.map +0 -7
  231. package/dist/tool-diff.d.ts.map +0 -1
  232. package/dist/tool-diff.js.map +0 -7
  233. package/dist/tool-help.d.ts.map +0 -1
  234. package/dist/tool-help.js.map +0 -7
  235. package/dist/tool-icon-map.d.ts.map +0 -1
  236. package/dist/tool-icons.d.ts.map +0 -1
  237. package/dist/tool-icons.js.map +0 -7
  238. package/dist/tool-search.d.ts.map +0 -1
  239. package/dist/tool-search.js.map +0 -7
  240. package/dist/tool-summary.d.ts.map +0 -1
  241. package/dist/tool-summary.js.map +0 -7
  242. package/dist/tool-tier.d.ts.map +0 -1
  243. package/dist/tool-tier.js.map +0 -7
  244. package/dist/tool-use.d.ts.map +0 -1
  245. package/dist/tool-use.js.map +0 -7
  246. package/dist/tree.d.ts.map +0 -1
  247. package/dist/tree.js.map +0 -7
  248. package/dist/typecheck.d.ts.map +0 -1
  249. package/dist/typecheck.js.map +0 -7
  250. package/dist/win32.d.ts.map +0 -1
  251. package/dist/win32.js.map +0 -7
  252. package/dist/write.d.ts.map +0 -1
  253. package/dist/write.js.map +0 -7
@@ -2875,6 +2875,7 @@ var REFS_INDEX_SQL = [
2875
2875
  var SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')";
2876
2876
 
2877
2877
  // src/codebase-index/writer-search-helpers.ts
2878
+ var SEARCH_CANDIDATE_SCAN_CAP = 5e3;
2878
2879
  function normalizeSearchLimit(limit) {
2879
2880
  return typeof limit === "number" && Number.isFinite(limit) ? Math.max(0, Math.trunc(limit)) : void 0;
2880
2881
  }
@@ -2904,8 +2905,8 @@ function buildWriterSearchWhere(query, filter) {
2904
2905
  }
2905
2906
  if (query.trim()) {
2906
2907
  const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
2907
- conditions.push(`(${tokens.map(() => "text LIKE ?").join(" OR ")})`);
2908
- for (const token of tokens) values.push(`%${token}%`);
2908
+ conditions.push(`(${tokens.map(() => "text LIKE ? ESCAPE '\\'").join(" OR ")})`);
2909
+ for (const token of tokens) values.push(`%${escapeLike(token)}%`);
2909
2910
  }
2910
2911
  return { where: conditions.length ? `WHERE ${conditions.join(" AND ")}` : "", values };
2911
2912
  }
@@ -3014,6 +3015,7 @@ var StorePool = class {
3014
3015
 
3015
3016
  // src/codebase-index/writer.ts
3016
3017
  var DB_FILE2 = "index.db";
3018
+ var MAX_STATEMENT_CACHE = 128;
3017
3019
  var IndexStore = class _IndexStore {
3018
3020
  db;
3019
3021
  /** Absolute path to this project's index directory. */
@@ -3051,12 +3053,31 @@ var IndexStore = class _IndexStore {
3051
3053
  // Dirty on open so the first getOrBuildBm25() rebuilds against current rows;
3052
3054
  // an empty or pre-existing corpus makes a stale IDF table meaningless.
3053
3055
  bm25Dirty = true;
3054
- /** Prepare-once helper: compile `sql` on first use, reuse thereafter. */
3056
+ /**
3057
+ * Prepare-once helper: compile `sql` on first use, reuse thereafter.
3058
+ *
3059
+ * Bounded LRU rather than an open Map. The cache is keyed by SQL TEXT, and
3060
+ * the fallback search builder emits one `text LIKE ?` clause per query token
3061
+ * — so the SQL varies with the token count and a stream of differently-sized
3062
+ * queries grew the cache without limit. Sage's store already bounds its
3063
+ * equivalent at 128 (WS-096).
3064
+ *
3065
+ * Re-inserting on a hit keeps the hot fixed-SQL statements (upsertFile,
3066
+ * insertRefs, …) at the young end, so a burst of one-off search SQL evicts
3067
+ * itself rather than the reindex hot path.
3068
+ */
3055
3069
  stmt(sql) {
3056
- let s = this.stmtCache.get(sql);
3057
- if (s === void 0) {
3058
- s = this.db.prepare(sql);
3059
- this.stmtCache.set(sql, s);
3070
+ const cached = this.stmtCache.get(sql);
3071
+ if (cached !== void 0) {
3072
+ this.stmtCache.delete(sql);
3073
+ this.stmtCache.set(sql, cached);
3074
+ return cached;
3075
+ }
3076
+ const s = this.db.prepare(sql);
3077
+ this.stmtCache.set(sql, s);
3078
+ if (this.stmtCache.size > MAX_STATEMENT_CACHE) {
3079
+ const oldest = this.stmtCache.keys().next();
3080
+ if (!oldest.done) this.stmtCache.delete(oldest.value);
3060
3081
  }
3061
3082
  return s;
3062
3083
  }
@@ -3329,7 +3350,7 @@ var IndexStore = class _IndexStore {
3329
3350
  const { where, values } = built;
3330
3351
  const limit = normalizeSearchLimit(opts?.limit);
3331
3352
  const limitSql = limit !== void 0 ? " LIMIT ?" : "";
3332
- const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment, text FROM symbols ${where}${limitSql}`;
3353
+ const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment FROM symbols ${where}${limitSql}`;
3333
3354
  const binds = limit !== void 0 ? [...values, limit] : values;
3334
3355
  const rows = this.stmt(sql).all(
3335
3356
  ...binds
@@ -3458,11 +3479,13 @@ var IndexStore = class _IndexStore {
3458
3479
  /** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
3459
3480
  searchRankedFallback(query, filter, limit) {
3460
3481
  if (!query.trim()) {
3461
- const total = this.countSearch(query, filter);
3462
- if (total === 0) return { results: [], total: 0 };
3463
- return { results: this.search(query, filter, { limit }), total };
3482
+ const total2 = this.countSearch(query, filter);
3483
+ if (total2 === 0) return { results: [], total: 0 };
3484
+ return { results: this.search(query, filter, { limit }), total: total2 };
3464
3485
  }
3465
- const candidates = this.search(query, filter);
3486
+ const total = this.countSearch(query, filter);
3487
+ if (total === 0) return { results: [], total: 0 };
3488
+ const candidates = this.search(query, filter, { limit: SEARCH_CANDIDATE_SCAN_CAP });
3466
3489
  if (candidates.length === 0) return { results: [], total: 0 };
3467
3490
  const candidateById = new Map(candidates.map((c) => [c.id, c]));
3468
3491
  const bm25 = this.getOrBuildBm25();
@@ -3488,7 +3511,7 @@ var IndexStore = class _IndexStore {
3488
3511
  const c = expectDefined(candidateById.get(id));
3489
3512
  return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
3490
3513
  });
3491
- return { results, total: candidates.length };
3514
+ return { results, total };
3492
3515
  }
3493
3516
  getAllIndexable() {
3494
3517
  return getAllIndexableWithStatement((sql) => this.stmt(sql));
@@ -4082,6 +4105,8 @@ var ProjectServerConnection = class {
4082
4105
  connectResolve = null;
4083
4106
  connectReject = null;
4084
4107
  nextId = 1;
4108
+ /** WS-027: read from the owner-only metadata file — see currentAuthToken. */
4109
+ authToken;
4085
4110
  pending = /* @__PURE__ */ new Map();
4086
4111
  transition(status, options = {}) {
4087
4112
  const previous = connectionStates.get(this.endpoint);
@@ -4218,6 +4243,31 @@ var ProjectServerConnection = class {
4218
4243
  this.transition("offline");
4219
4244
  maybeStopHeartbeatLoop();
4220
4245
  }
4246
+ /**
4247
+ * WS-027: the per-process token, read from the daemon's owner-only metadata
4248
+ * file — never from the `hello` frame, which the daemon sends to every
4249
+ * socket that connects (the mistake WS-028 found in the SAGE daemon).
4250
+ *
4251
+ * Read lazily and re-read while unknown: the daemon starts listening before
4252
+ * it writes metadata (endpoint ownership has to be won first), and a
4253
+ * respawned daemon mints a new token, so a cached wrong one would be sticky.
4254
+ */
4255
+ currentAuthToken() {
4256
+ if (this.authToken === void 0) {
4257
+ try {
4258
+ const raw = fs4.readFileSync(
4259
+ projectIndexServerMetadataPath(this.projectRoot, this.indexDir),
4260
+ "utf8"
4261
+ );
4262
+ const parsed = JSON.parse(raw);
4263
+ if (typeof parsed.authToken === "string" && parsed.authToken.length > 0) {
4264
+ this.authToken = parsed.authToken;
4265
+ }
4266
+ } catch {
4267
+ }
4268
+ }
4269
+ return this.authToken;
4270
+ }
4221
4271
  request(message, options) {
4222
4272
  const socket = this.socket;
4223
4273
  if (!socket || socket.destroyed) {
@@ -4261,7 +4311,7 @@ var ProjectServerConnection = class {
4261
4311
  return;
4262
4312
  }
4263
4313
  }
4264
- this.write({ ...message, id });
4314
+ this.write({ ...message, id, authToken: this.currentAuthToken() });
4265
4315
  });
4266
4316
  }
4267
4317
  async ensureConnected(spawnIfMissing) {
@@ -4308,6 +4358,7 @@ var ProjectServerConnection = class {
4308
4358
  connectOnce() {
4309
4359
  this.socket?.destroy();
4310
4360
  this.socket = null;
4361
+ this.authToken = void 0;
4311
4362
  this.info = null;
4312
4363
  this.activity = null;
4313
4364
  this.health = null;
@@ -6270,8 +6321,9 @@ function runDeadCodeScan(projectRoot, opts = {}) {
6270
6321
  const content = fs12.readFileSync(epFile, "utf8");
6271
6322
  const strippedContent = content.replace(/\/\*[\s\S]*?\*\//g, (m) => " ".repeat(m.length)).replace(/\/\/[^\n]*/g, (m) => " ".repeat(m.length));
6272
6323
  const reExportRe = /export\s+(?:(?:type\s+)?\{[\s\S]*?\}\s+from|\*\s+as\s+\w+\s+from|\*\s+from)\s+['"]([^'"]+)['"]/g;
6273
- let match;
6274
- while ((match = reExportRe.exec(strippedContent)) !== null) {
6324
+ for (; ; ) {
6325
+ const match = reExportRe.exec(strippedContent);
6326
+ if (match === null) break;
6275
6327
  const moduleSpec = match[1];
6276
6328
  const resolvedFiles = resolveModulePath(epFile, moduleSpec, indexedFiles);
6277
6329
  for (const rf of resolvedFiles) {
@@ -1,6 +1,10 @@
1
1
  import type { OpName, OpShapes } from './worker-protocol.js';
2
2
  /** Hard ceiling for one newline-delimited IPC message (measured as JS characters). */
3
3
  export declare const PROJECT_INDEX_SERVER_MAX_FRAME_CHARS: number;
4
+ /**
5
+ * What the daemon tells a connecting client about itself. Carries NO secret —
6
+ * see {@link ProjectIndexServerMetadata}.
7
+ */
4
8
  export interface ProjectIndexServerInfo {
5
9
  protocolVersion: number;
6
10
  buildId: string;
@@ -41,11 +45,30 @@ export interface ProjectIndexServerHealth {
41
45
  oldestClientIdleMs?: number | undefined;
42
46
  activity: ProjectIndexServerActivity;
43
47
  }
48
+ /**
49
+ * The daemon's on-disk metadata file, written 0600. The auth token exists here
50
+ * and nowhere else — never in the `hello` frame, which the daemon sends to
51
+ * every socket that connects (the mistake WS-028 found in the SAGE daemon).
52
+ */
53
+ export interface ProjectIndexServerMetadata extends ProjectIndexServerInfo {
54
+ authToken: string;
55
+ }
56
+ /**
57
+ * WS-027: this daemon owns the project's code index — it reads every file in
58
+ * the repository and answers content queries over it — and it had no handshake
59
+ * credential at all. Any process that could open the socket could search the
60
+ * whole codebase through it or drive a reindex. `authToken` comes from the
61
+ * daemon's owner-only metadata file, so a caller must prove same-user access.
62
+ *
63
+ * `cancel` is ungated: it reaches only the sending connection's own in-flight
64
+ * request, so there is nothing to escalate.
65
+ */
44
66
  export type ProjectServerClientMessage = {
45
67
  type: 'request';
46
68
  id: number;
47
69
  op: OpName;
48
70
  args: OpShapes[OpName]['args'];
71
+ authToken?: string | undefined;
49
72
  } | {
50
73
  type: 'cancel';
51
74
  id: number;
@@ -54,13 +77,16 @@ export type ProjectServerClientMessage = {
54
77
  id: number;
55
78
  watchExternal: boolean;
56
79
  debounceMs: number;
80
+ authToken?: string | undefined;
57
81
  } | {
58
82
  type: 'ping';
59
83
  id: number;
84
+ authToken?: string | undefined;
60
85
  } | {
61
86
  type: 'shutdown';
62
87
  id: number;
63
88
  reason?: string | undefined;
89
+ authToken?: string | undefined;
64
90
  };
65
91
  export type ProjectServerMessage = ({
66
92
  type: 'hello';
@@ -1972,6 +1972,7 @@ var init_yaml_parser = __esm({
1972
1972
  });
1973
1973
 
1974
1974
  // src/codebase-index/project-server.ts
1975
+ import { randomBytes } from "node:crypto";
1975
1976
  import * as fs10 from "node:fs";
1976
1977
  import * as net from "node:net";
1977
1978
  import * as path13 from "node:path";
@@ -2939,6 +2940,7 @@ var REFS_INDEX_SQL = [
2939
2940
  var SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')";
2940
2941
 
2941
2942
  // src/codebase-index/writer-search-helpers.ts
2943
+ var SEARCH_CANDIDATE_SCAN_CAP = 5e3;
2942
2944
  function normalizeSearchLimit(limit) {
2943
2945
  return typeof limit === "number" && Number.isFinite(limit) ? Math.max(0, Math.trunc(limit)) : void 0;
2944
2946
  }
@@ -2968,8 +2970,8 @@ function buildWriterSearchWhere(query, filter) {
2968
2970
  }
2969
2971
  if (query.trim()) {
2970
2972
  const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
2971
- conditions.push(`(${tokens.map(() => "text LIKE ?").join(" OR ")})`);
2972
- for (const token of tokens) values.push(`%${token}%`);
2973
+ conditions.push(`(${tokens.map(() => "text LIKE ? ESCAPE '\\'").join(" OR ")})`);
2974
+ for (const token of tokens) values.push(`%${escapeLike(token)}%`);
2973
2975
  }
2974
2976
  return { where: conditions.length ? `WHERE ${conditions.join(" AND ")}` : "", values };
2975
2977
  }
@@ -3078,6 +3080,7 @@ var StorePool = class {
3078
3080
 
3079
3081
  // src/codebase-index/writer.ts
3080
3082
  var DB_FILE2 = "index.db";
3083
+ var MAX_STATEMENT_CACHE = 128;
3081
3084
  var IndexStore = class _IndexStore {
3082
3085
  db;
3083
3086
  /** Absolute path to this project's index directory. */
@@ -3115,12 +3118,31 @@ var IndexStore = class _IndexStore {
3115
3118
  // Dirty on open so the first getOrBuildBm25() rebuilds against current rows;
3116
3119
  // an empty or pre-existing corpus makes a stale IDF table meaningless.
3117
3120
  bm25Dirty = true;
3118
- /** Prepare-once helper: compile `sql` on first use, reuse thereafter. */
3121
+ /**
3122
+ * Prepare-once helper: compile `sql` on first use, reuse thereafter.
3123
+ *
3124
+ * Bounded LRU rather than an open Map. The cache is keyed by SQL TEXT, and
3125
+ * the fallback search builder emits one `text LIKE ?` clause per query token
3126
+ * — so the SQL varies with the token count and a stream of differently-sized
3127
+ * queries grew the cache without limit. Sage's store already bounds its
3128
+ * equivalent at 128 (WS-096).
3129
+ *
3130
+ * Re-inserting on a hit keeps the hot fixed-SQL statements (upsertFile,
3131
+ * insertRefs, …) at the young end, so a burst of one-off search SQL evicts
3132
+ * itself rather than the reindex hot path.
3133
+ */
3119
3134
  stmt(sql) {
3120
- let s = this.stmtCache.get(sql);
3121
- if (s === void 0) {
3122
- s = this.db.prepare(sql);
3123
- this.stmtCache.set(sql, s);
3135
+ const cached = this.stmtCache.get(sql);
3136
+ if (cached !== void 0) {
3137
+ this.stmtCache.delete(sql);
3138
+ this.stmtCache.set(sql, cached);
3139
+ return cached;
3140
+ }
3141
+ const s = this.db.prepare(sql);
3142
+ this.stmtCache.set(sql, s);
3143
+ if (this.stmtCache.size > MAX_STATEMENT_CACHE) {
3144
+ const oldest = this.stmtCache.keys().next();
3145
+ if (!oldest.done) this.stmtCache.delete(oldest.value);
3124
3146
  }
3125
3147
  return s;
3126
3148
  }
@@ -3393,7 +3415,7 @@ var IndexStore = class _IndexStore {
3393
3415
  const { where, values } = built;
3394
3416
  const limit = normalizeSearchLimit(opts?.limit);
3395
3417
  const limitSql = limit !== void 0 ? " LIMIT ?" : "";
3396
- const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment, text FROM symbols ${where}${limitSql}`;
3418
+ const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment FROM symbols ${where}${limitSql}`;
3397
3419
  const binds = limit !== void 0 ? [...values, limit] : values;
3398
3420
  const rows = this.stmt(sql).all(
3399
3421
  ...binds
@@ -3522,11 +3544,13 @@ var IndexStore = class _IndexStore {
3522
3544
  /** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
3523
3545
  searchRankedFallback(query, filter, limit) {
3524
3546
  if (!query.trim()) {
3525
- const total = this.countSearch(query, filter);
3526
- if (total === 0) return { results: [], total: 0 };
3527
- return { results: this.search(query, filter, { limit }), total };
3547
+ const total2 = this.countSearch(query, filter);
3548
+ if (total2 === 0) return { results: [], total: 0 };
3549
+ return { results: this.search(query, filter, { limit }), total: total2 };
3528
3550
  }
3529
- const candidates = this.search(query, filter);
3551
+ const total = this.countSearch(query, filter);
3552
+ if (total === 0) return { results: [], total: 0 };
3553
+ const candidates = this.search(query, filter, { limit: SEARCH_CANDIDATE_SCAN_CAP });
3530
3554
  if (candidates.length === 0) return { results: [], total: 0 };
3531
3555
  const candidateById = new Map(candidates.map((c) => [c.id, c]));
3532
3556
  const bm25 = this.getOrBuildBm25();
@@ -3552,7 +3576,7 @@ var IndexStore = class _IndexStore {
3552
3576
  const c = expectDefined4(candidateById.get(id));
3553
3577
  return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
3554
3578
  });
3555
- return { results, total: candidates.length };
3579
+ return { results, total };
3556
3580
  }
3557
3581
  getAllIndexable() {
3558
3582
  return getAllIndexableWithStatement((sql) => this.stmt(sql));
@@ -4587,6 +4611,11 @@ var clientLeaseMsRaw = Number(process.env["WRONGSTACK_INDEX_SERVER_CLIENT_LEASE_
4587
4611
  var clientLeaseMs = Number.isFinite(clientLeaseMsRaw) && clientLeaseMsRaw >= 100 ? clientLeaseMsRaw : DEFAULT_CLIENT_LEASE_MS;
4588
4612
  var clientLeaseSweepMs = Math.min(1e4, Math.max(100, Math.floor(clientLeaseMs / 3)));
4589
4613
  var startedAt = (/* @__PURE__ */ new Date()).toISOString();
4614
+ var authToken = randomBytes(16).toString("hex");
4615
+ var markMetadataWritten;
4616
+ var metadataWritten = new Promise((resolve4) => {
4617
+ markMetadataWritten = resolve4;
4618
+ });
4590
4619
  var serverInfo = {
4591
4620
  protocolVersion: PROJECT_INDEX_SERVER_PROTOCOL_VERSION,
4592
4621
  buildId: projectIndexServerBuildId(import.meta.url),
@@ -4861,6 +4890,16 @@ async function handleMessage(state, message) {
4861
4890
  state.cancel.get(message.id)?.();
4862
4891
  return;
4863
4892
  }
4893
+ if (message.authToken !== authToken) {
4894
+ send(state, {
4895
+ type: "response",
4896
+ id: message.id,
4897
+ ok: false,
4898
+ error: "Codebase-index IPC request rejected: missing or invalid authToken. Reconnect to refresh metadata (server.json#authToken).",
4899
+ errorName: "UnauthorizedIndexRequest"
4900
+ });
4901
+ return;
4902
+ }
4864
4903
  if (message.type === "ping") {
4865
4904
  send(state, { type: "response", id: message.id, ok: true, result: serverHealth() });
4866
4905
  return;
@@ -5047,7 +5086,8 @@ function removeMetadataIfOwned() {
5047
5086
  function writeMetadata() {
5048
5087
  fs10.mkdirSync(path13.dirname(metadataPath), { recursive: true });
5049
5088
  const temporary = `${metadataPath}.${process.pid}.tmp`;
5050
- fs10.writeFileSync(temporary, `${JSON.stringify(serverInfo, null, 2)}
5089
+ const metadata = { ...serverInfo, authToken };
5090
+ fs10.writeFileSync(temporary, `${JSON.stringify(metadata, null, 2)}
5051
5091
  `, { mode: 384 });
5052
5092
  try {
5053
5093
  fs10.renameSync(temporary, metadataPath);
@@ -5071,8 +5111,11 @@ var server = net.createServer((socket) => {
5071
5111
  lastSeenAt: Date.now()
5072
5112
  };
5073
5113
  clients.add(state);
5074
- send(state, { type: "hello", ...serverInfo });
5075
- send(state, { type: "index-state", state: indexActivity });
5114
+ void metadataWritten.then(() => {
5115
+ if (socket.destroyed) return;
5116
+ send(state, { type: "hello", ...serverInfo });
5117
+ send(state, { type: "index-state", state: indexActivity });
5118
+ });
5076
5119
  socket.on("data", (chunk) => consume(state, chunk));
5077
5120
  socket.on("close", () => {
5078
5121
  for (const cancel of state.cancel.values()) cancel();
@@ -5144,6 +5187,7 @@ server.listen(endpoint, () => {
5144
5187
  }
5145
5188
  }
5146
5189
  writeMetadata();
5190
+ markMetadataWritten?.();
5147
5191
  scheduleIdleStop();
5148
5192
  });
5149
5193
  process.once("SIGINT", () => void stop("SIGINT"));
@@ -2927,6 +2927,7 @@ var REFS_INDEX_SQL = [
2927
2927
  var SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')";
2928
2928
 
2929
2929
  // src/codebase-index/writer-search-helpers.ts
2930
+ var SEARCH_CANDIDATE_SCAN_CAP = 5e3;
2930
2931
  function normalizeSearchLimit(limit) {
2931
2932
  return typeof limit === "number" && Number.isFinite(limit) ? Math.max(0, Math.trunc(limit)) : void 0;
2932
2933
  }
@@ -2956,8 +2957,8 @@ function buildWriterSearchWhere(query, filter) {
2956
2957
  }
2957
2958
  if (query.trim()) {
2958
2959
  const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
2959
- conditions.push(`(${tokens.map(() => "text LIKE ?").join(" OR ")})`);
2960
- for (const token of tokens) values.push(`%${token}%`);
2960
+ conditions.push(`(${tokens.map(() => "text LIKE ? ESCAPE '\\'").join(" OR ")})`);
2961
+ for (const token of tokens) values.push(`%${escapeLike(token)}%`);
2961
2962
  }
2962
2963
  return { where: conditions.length ? `WHERE ${conditions.join(" AND ")}` : "", values };
2963
2964
  }
@@ -3066,6 +3067,7 @@ var StorePool = class {
3066
3067
 
3067
3068
  // src/codebase-index/writer.ts
3068
3069
  var DB_FILE2 = "index.db";
3070
+ var MAX_STATEMENT_CACHE = 128;
3069
3071
  var IndexStore = class _IndexStore {
3070
3072
  db;
3071
3073
  /** Absolute path to this project's index directory. */
@@ -3103,12 +3105,31 @@ var IndexStore = class _IndexStore {
3103
3105
  // Dirty on open so the first getOrBuildBm25() rebuilds against current rows;
3104
3106
  // an empty or pre-existing corpus makes a stale IDF table meaningless.
3105
3107
  bm25Dirty = true;
3106
- /** Prepare-once helper: compile `sql` on first use, reuse thereafter. */
3108
+ /**
3109
+ * Prepare-once helper: compile `sql` on first use, reuse thereafter.
3110
+ *
3111
+ * Bounded LRU rather than an open Map. The cache is keyed by SQL TEXT, and
3112
+ * the fallback search builder emits one `text LIKE ?` clause per query token
3113
+ * — so the SQL varies with the token count and a stream of differently-sized
3114
+ * queries grew the cache without limit. Sage's store already bounds its
3115
+ * equivalent at 128 (WS-096).
3116
+ *
3117
+ * Re-inserting on a hit keeps the hot fixed-SQL statements (upsertFile,
3118
+ * insertRefs, …) at the young end, so a burst of one-off search SQL evicts
3119
+ * itself rather than the reindex hot path.
3120
+ */
3107
3121
  stmt(sql) {
3108
- let s = this.stmtCache.get(sql);
3109
- if (s === void 0) {
3110
- s = this.db.prepare(sql);
3111
- this.stmtCache.set(sql, s);
3122
+ const cached = this.stmtCache.get(sql);
3123
+ if (cached !== void 0) {
3124
+ this.stmtCache.delete(sql);
3125
+ this.stmtCache.set(sql, cached);
3126
+ return cached;
3127
+ }
3128
+ const s = this.db.prepare(sql);
3129
+ this.stmtCache.set(sql, s);
3130
+ if (this.stmtCache.size > MAX_STATEMENT_CACHE) {
3131
+ const oldest = this.stmtCache.keys().next();
3132
+ if (!oldest.done) this.stmtCache.delete(oldest.value);
3112
3133
  }
3113
3134
  return s;
3114
3135
  }
@@ -3381,7 +3402,7 @@ var IndexStore = class _IndexStore {
3381
3402
  const { where, values } = built;
3382
3403
  const limit = normalizeSearchLimit(opts?.limit);
3383
3404
  const limitSql = limit !== void 0 ? " LIMIT ?" : "";
3384
- const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment, text FROM symbols ${where}${limitSql}`;
3405
+ const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment FROM symbols ${where}${limitSql}`;
3385
3406
  const binds = limit !== void 0 ? [...values, limit] : values;
3386
3407
  const rows = this.stmt(sql).all(
3387
3408
  ...binds
@@ -3510,11 +3531,13 @@ var IndexStore = class _IndexStore {
3510
3531
  /** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
3511
3532
  searchRankedFallback(query, filter, limit) {
3512
3533
  if (!query.trim()) {
3513
- const total = this.countSearch(query, filter);
3514
- if (total === 0) return { results: [], total: 0 };
3515
- return { results: this.search(query, filter, { limit }), total };
3534
+ const total2 = this.countSearch(query, filter);
3535
+ if (total2 === 0) return { results: [], total: 0 };
3536
+ return { results: this.search(query, filter, { limit }), total: total2 };
3516
3537
  }
3517
- const candidates = this.search(query, filter);
3538
+ const total = this.countSearch(query, filter);
3539
+ if (total === 0) return { results: [], total: 0 };
3540
+ const candidates = this.search(query, filter, { limit: SEARCH_CANDIDATE_SCAN_CAP });
3518
3541
  if (candidates.length === 0) return { results: [], total: 0 };
3519
3542
  const candidateById = new Map(candidates.map((c) => [c.id, c]));
3520
3543
  const bm25 = this.getOrBuildBm25();
@@ -3540,7 +3563,7 @@ var IndexStore = class _IndexStore {
3540
3563
  const c = expectDefined4(candidateById.get(id));
3541
3564
  return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
3542
3565
  });
3543
- return { results, total: candidates.length };
3566
+ return { results, total };
3544
3567
  }
3545
3568
  getAllIndexable() {
3546
3569
  return getAllIndexableWithStatement((sql) => this.stmt(sql));
@@ -19,6 +19,22 @@ export interface WriterSearchRow {
19
19
  score?: number;
20
20
  snippet?: string;
21
21
  }
22
+ /**
23
+ * Ceiling on how many LIKE candidates the FTS5-less ranked fallback pulls into
24
+ * memory before scoring (WS-031).
25
+ *
26
+ * The fallback ranks with an in-process BM25 pass, so it needs more than the
27
+ * caller's `limit` (≤100) to rank well — but it does not need the whole corpus.
28
+ * Without a bound, a broad query on a large index materializes every matching
29
+ * row as a JS object; this subsystem has a documented OOM history, and search
30
+ * runs on the request path.
31
+ *
32
+ * Set far above any query a human would type, so ranking quality is unchanged
33
+ * in practice: it bounds the pathological case, it does not shape normal ones.
34
+ * `total` is reported from a SQL `COUNT(*)` rather than the truncated array, so
35
+ * capping never makes the reported match count wrong.
36
+ */
37
+ export declare const SEARCH_CANDIDATE_SCAN_CAP = 5000;
22
38
  export declare function normalizeSearchLimit(limit: number | undefined): number | undefined;
23
39
  export declare function buildWriterSearchWhere(query: string, filter?: WriterSearchFilter | undefined): {
24
40
  where: string;
@@ -38,7 +38,19 @@ export declare class IndexStore {
38
38
  */
39
39
  private bm25Cache;
40
40
  private bm25Dirty;
41
- /** Prepare-once helper: compile `sql` on first use, reuse thereafter. */
41
+ /**
42
+ * Prepare-once helper: compile `sql` on first use, reuse thereafter.
43
+ *
44
+ * Bounded LRU rather than an open Map. The cache is keyed by SQL TEXT, and
45
+ * the fallback search builder emits one `text LIKE ?` clause per query token
46
+ * — so the SQL varies with the token count and a stream of differently-sized
47
+ * queries grew the cache without limit. Sage's store already bounds its
48
+ * equivalent at 128 (WS-096).
49
+ *
50
+ * Re-inserting on a hit keeps the hot fixed-SQL statements (upsertFile,
51
+ * insertRefs, …) at the young end, so a burst of one-off search SQL evicts
52
+ * itself rather than the reindex hot path.
53
+ */
42
54
  private stmt;
43
55
  constructor(projectRoot: string, opts?: {
44
56
  indexDir?: string | undefined;
package/dist/design.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { Tool } from '@wrongstack/core/types';
2
1
  import { type SemanticTune } from '@wrongstack/core/design';
2
+ import type { Tool } from '@wrongstack/core/types';
3
3
  type Overrides = Record<string, string>;
4
4
  interface DesignInput {
5
5
  action?: 'list' | 'use' | 'foundations' | 'set' | 'tune' | 'materialize' | 'verify' | undefined;
package/dist/design.js CHANGED
@@ -48,6 +48,9 @@ var designTool = {
48
48
  description: 'Browse, load, customize, and enforce curated frontend/mobile UI design kits. Use BEFORE writing UI code to commit to one coherent, modern, responsive, dark/light, accessible design. Actions: "list" (menu), "use" (load+pin a kit for a stack), "foundations" (baseline), "set" (override kit colors/tokens), "materialize" (write the tokens to a real theme file \u2014 CSS @theme/OKLCH or native), "verify" (scan UI files for off-palette colors).',
49
49
  usageHint: 'Flow: `design {action:"use", kit:"minimal-clarity", stack:"web"}` \u2192 optionally `design {action:"set", set:{primary:"oklch(62% 0.2 25)"}}` \u2192 `design {action:"materialize"}` to write tokens to disk \u2192 implement against them \u2192 `design {action:"verify"}`.',
50
50
  permission: "confirm",
51
+ // WS-046: gives permission decisions something to key on.
52
+ // The action performed; `out` is optional so it cannot be the subject.
53
+ subjectKey: "action",
51
54
  mutating: true,
52
55
  capabilities: ["fs.write"],
53
56
  timeoutMs: 15e3,
@@ -228,9 +231,7 @@ Run \`design {action:"materialize"}\` to write the tuned tokens to your theme fi
228
231
  const abs = path.join(absParent, path.basename(absResolved));
229
232
  const rel = path.relative(root, abs);
230
233
  if (rel.startsWith("..") || path.isAbsolute(rel)) {
231
- throw new Error(
232
- `design: materialize path "${result.path}" would escape the project root`
233
- );
234
+ throw new Error(`design: materialize path "${result.path}" would escape the project root`);
234
235
  }
235
236
  let exists = false;
236
237
  try {
package/dist/diff.js CHANGED
@@ -31,6 +31,7 @@ function safeResolve(input, ctx) {
31
31
  }
32
32
 
33
33
  // src/diff.ts
34
+ var MAX_FILE_DUMP_BYTES = 5 * 1024 * 1024;
34
35
  var diffTool = {
35
36
  name: "diff",
36
37
  category: "Filesystem",
@@ -38,6 +39,7 @@ var diffTool = {
38
39
  usageHint: 'USE FOR CODE REVIEW AND CHANGE INSPECTION:\n\n- `files` + no `a`/`b` \u2192 show file content with line numbers (NOT a unified diff; no +/- prefixes).\n- `a` and/or `b` \u2192 git-style commit/branch diff (unified format, real +/- prefixes).\n- `staged: true` \u2192 only show staged changes.\n- `mode` can be "unified", "stat", or "side-by-side" (only affects the git-diff path).\n\nNOTE: For a true file-vs-file unified diff, supply `a` and `b` so the tool delegates to `git diff`. The `files`-only path is a line-numbered dump, not a diff.\n\nThis tool has important safety guards against flag injection (see previous security findings).',
39
40
  permission: "auto",
40
41
  mutating: false,
42
+ maxOutputBytes: 262144,
41
43
  capabilities: ["fs.read"],
42
44
  icon: "diff",
43
45
  timeoutMs: 1e4,
@@ -156,10 +158,18 @@ async function fileDiff(input, ctx, _signal) {
156
158
  };
157
159
  }
158
160
  const results = [];
161
+ let truncated = false;
159
162
  for (const file of files) {
160
163
  const absPath = safeResolve(file, ctx);
161
164
  const stat2 = await fs.stat(absPath).catch(() => null);
162
165
  if (!stat2?.isFile()) continue;
166
+ if (stat2.size > MAX_FILE_DUMP_BYTES) {
167
+ truncated = true;
168
+ results.push(
169
+ `--- ${file} (skipped: ${stat2.size} bytes exceeds the ${MAX_FILE_DUMP_BYTES} limit; use the read tool with offset/limit) ---`
170
+ );
171
+ continue;
172
+ }
163
173
  const content = await fs.readFile(absPath, "utf8");
164
174
  const lines = content.split(/\r?\n/);
165
175
  results.push(formatWithLineNumbers(file, lines));
@@ -167,7 +177,7 @@ async function fileDiff(input, ctx, _signal) {
167
177
  return {
168
178
  diff: results.join("\n\n"),
169
179
  files,
170
- truncated: false,
180
+ truncated,
171
181
  mode: input.mode ?? "unified"
172
182
  };
173
183
  }
package/dist/e2e.js CHANGED
@@ -27,12 +27,13 @@ function ensureInsideRoot(absPath, ctx) {
27
27
  function safeResolve(input, ctx) {
28
28
  return ensureInsideRoot(resolvePath(input, ctx), ctx);
29
29
  }
30
- async function assertRealInsideRoot(absPath, ctx) {
31
- if (ctx.allowOutsideProjectRoot) return;
30
+ async function resolveRealInsideRoot(absPath, ctx) {
31
+ if (ctx.allowOutsideProjectRoot) return absPath;
32
32
  const realRoots = await Promise.all(
33
33
  allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r)))
34
34
  );
35
35
  let probe = absPath;
36
+ const pendingTail = [];
36
37
  for (; ; ) {
37
38
  let real;
38
39
  try {
@@ -40,13 +41,16 @@ async function assertRealInsideRoot(absPath, ctx) {
40
41
  } catch (err) {
41
42
  if (err.code === "ENOENT") {
42
43
  const parent = path.dirname(probe);
43
- if (parent === probe) return;
44
+ if (parent === probe) return absPath;
45
+ pendingTail.unshift(path.basename(probe));
44
46
  probe = parent;
45
47
  continue;
46
48
  }
47
49
  throw err;
48
50
  }
49
- if (isInsideAny(real, realRoots)) return;
51
+ if (isInsideAny(real, realRoots)) {
52
+ return pendingTail.length > 0 ? path.join(real, ...pendingTail) : real;
53
+ }
50
54
  throw new Error(
51
55
  `Path "${absPath}" resolves through a symlink outside project root "${realRoots[0]}"`
52
56
  );
@@ -54,8 +58,7 @@ async function assertRealInsideRoot(absPath, ctx) {
54
58
  }
55
59
  async function safeResolveReal(input, ctx) {
56
60
  const abs = safeResolve(input, ctx);
57
- await assertRealInsideRoot(abs, ctx);
58
- return abs;
61
+ return await resolveRealInsideRoot(abs, ctx);
59
62
  }
60
63
 
61
64
  // src/e2e.ts