token-goat 2.6.36 → 2.8.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 (34) hide show
  1. package/README.md +31 -5
  2. package/SECURITY.md +46 -17
  3. package/dist/{token-goat-chunk-UFOVM7ZN.mjs → token-goat-chunk-2F6TFBZE.mjs} +1274 -367
  4. package/dist/token-goat-chunk-324QOJYZ.mjs +91 -0
  5. package/dist/{token-goat-chunk-V465YKOR.mjs → token-goat-chunk-44Y77VHR.mjs} +365 -23
  6. package/dist/{token-goat-chunk-CNDOJ3ZP.mjs → token-goat-chunk-4KZILRZN.mjs} +2 -2
  7. package/dist/token-goat-chunk-5CVKO3DA.mjs +185 -0
  8. package/dist/token-goat-chunk-A62K4XW2.mjs +23 -0
  9. package/dist/{token-goat-chunk-VYMGEVZS.mjs → token-goat-chunk-AO6MFFTW.mjs} +317 -11
  10. package/dist/{token-goat-chunk-65BKISIS.mjs → token-goat-chunk-FBGBTICM.mjs} +4 -4
  11. package/dist/{token-goat-chunk-LN6OUHTV.mjs → token-goat-chunk-J35AKWEQ.mjs} +5 -5
  12. package/dist/{token-goat-hook-chunk-BDR6C6IE.mjs → token-goat-chunk-LVCBDJVE.mjs} +318 -51
  13. package/dist/token-goat-chunk-R4SR7MQY.mjs +486 -0
  14. package/dist/{token-goat-chunk-IYTVE6KN.mjs → token-goat-chunk-SRAR6DOK.mjs} +133 -141
  15. package/dist/{token-goat-chunk-MGOUYAA2.mjs → token-goat-chunk-TUPJRK7R.mjs} +1 -1
  16. package/dist/{token-goat-chunk-KYFJC37X.mjs → token-goat-chunk-VXSYZGBA.mjs} +582 -135
  17. package/dist/{token-goat-chunk-DG53MVNJ.mjs → token-goat-chunk-WN5T5EW5.mjs} +212 -212
  18. package/dist/token-goat-hook.mjs +7 -7
  19. package/dist/token-goat.core.mjs +5 -5
  20. package/package.json +9 -7
  21. package/dist/token-goat-chunk-FRTBMRP7.mjs +0 -10048
  22. package/dist/token-goat-hook-chunk-3QYSN4QV.mjs +0 -14764
  23. package/dist/token-goat-hook-chunk-3ZDBWJDF.mjs +0 -13659
  24. package/dist/token-goat-hook-chunk-5UH54CW6.mjs +0 -912
  25. package/dist/token-goat-hook-chunk-6ODM3MP7.mjs +0 -706
  26. package/dist/token-goat-hook-chunk-A77A26A7.mjs +0 -18184
  27. package/dist/token-goat-hook-chunk-BUOCULAM.mjs +0 -29
  28. package/dist/token-goat-hook-chunk-C6GIABOX.mjs +0 -15971
  29. package/dist/token-goat-hook-chunk-E257IGSN.mjs +0 -153
  30. package/dist/token-goat-hook-chunk-MW5HPEGD.mjs +0 -10411
  31. package/dist/token-goat-hook-chunk-QSCYNJ2B.mjs +0 -23
  32. package/dist/token-goat-hook-chunk-RFRLWOQH.mjs +0 -11
  33. package/dist/token-goat-hook-chunk-XUMIVYEN.mjs +0 -109
  34. package/dist/token-goat-hook-chunk-Y2WHX2P3.mjs +0 -6463
@@ -19,6 +19,7 @@ import {
19
19
  detectLanguage,
20
20
  displaySafePath,
21
21
  displaySafeText,
22
+ ensureDataDirPrivate,
22
23
  ensureDirSync,
23
24
  escapeRegExp,
24
25
  extractEnv,
@@ -89,7 +90,7 @@ import {
89
90
  withFileLock,
90
91
  writeIfDifferent,
91
92
  writeJsonSettings
92
- } from "./token-goat-chunk-V465YKOR.mjs";
93
+ } from "./token-goat-chunk-44Y77VHR.mjs";
93
94
  import {
94
95
  registerReset
95
96
  } from "./token-goat-chunk-AO2QD2AG.mjs";
@@ -113,15 +114,221 @@ function createLazyModuleLoader(load3, errorLabel) {
113
114
  };
114
115
  }
115
116
 
117
+ // src/xml_parser.ts
118
+ var MAX_XML_DEPTH = 512;
119
+ var NAMED_ENTITIES = {
120
+ lt: "<",
121
+ gt: ">",
122
+ amp: "&",
123
+ quot: '"',
124
+ apos: "'"
125
+ };
126
+ function decodeXmlEntities(text) {
127
+ if (!text.includes("&")) return text;
128
+ return text.replace(/&(#[xX][0-9a-fA-F]+|#[0-9]+|[a-zA-Z_][\w.:-]*);/g, (whole, body) => {
129
+ if (body.charCodeAt(0) === 35) {
130
+ const isHex = body.charCodeAt(1) === 120 || body.charCodeAt(1) === 88;
131
+ const digits = isHex ? body.slice(2) : body.slice(1);
132
+ const code = parseInt(digits, isHex ? 16 : 10);
133
+ if (!Number.isFinite(code) || code < 0 || code > 1114111) return whole;
134
+ if (code >= 55296 && code <= 57343) return whole;
135
+ return String.fromCodePoint(code);
136
+ }
137
+ const named = NAMED_ENTITIES[body];
138
+ return named ?? whole;
139
+ });
140
+ }
141
+ function newFrame(name) {
142
+ return { name, children: /* @__PURE__ */ new Map(), text: [], attrs: [] };
143
+ }
144
+ function finishFrame(frame) {
145
+ const text = frame.text.join("");
146
+ if (frame.children.size === 0 && frame.attrs.length === 0) return text;
147
+ const obj = {};
148
+ for (const [key, value] of frame.children) obj[key] = value;
149
+ if (text.length > 0) obj["#text"] = text;
150
+ for (const [key, value] of frame.attrs) obj[`@_${key}`] = value;
151
+ return obj;
152
+ }
153
+ function addChild(parent, name, value) {
154
+ const existing = parent.children.get(name);
155
+ if (existing === void 0 && !parent.children.has(name)) {
156
+ parent.children.set(name, value);
157
+ return;
158
+ }
159
+ if (Array.isArray(existing)) {
160
+ existing.push(value);
161
+ return;
162
+ }
163
+ parent.children.set(name, [existing, value]);
164
+ }
165
+ var WHITESPACE = /* @__PURE__ */ new Set([" ", " ", "\n", "\r"]);
166
+ function findTagEnd(src, from) {
167
+ let quote = "";
168
+ for (let i = from; i < src.length; i++) {
169
+ const ch = src[i];
170
+ if (quote !== "") {
171
+ if (ch === quote) quote = "";
172
+ continue;
173
+ }
174
+ if (ch === '"' || ch === "'") {
175
+ quote = ch;
176
+ continue;
177
+ }
178
+ if (ch === ">") return i;
179
+ }
180
+ return -1;
181
+ }
182
+ function skipDeclaration(src, from) {
183
+ let quote = "";
184
+ let inSubset = false;
185
+ for (let i = from + 2; i < src.length; i++) {
186
+ const ch = src[i];
187
+ if (quote !== "") {
188
+ if (ch === quote) quote = "";
189
+ continue;
190
+ }
191
+ if (ch === '"' || ch === "'") {
192
+ quote = ch;
193
+ continue;
194
+ }
195
+ if (ch === "[") inSubset = true;
196
+ else if (ch === "]") inSubset = false;
197
+ else if (ch === ">" && !inSubset) return i + 1;
198
+ }
199
+ return src.length;
200
+ }
201
+ function parseTagBody(body) {
202
+ let i = 0;
203
+ while (i < body.length && !WHITESPACE.has(body[i])) i++;
204
+ const name = body.slice(0, i);
205
+ const attrs = [];
206
+ while (i < body.length) {
207
+ while (i < body.length && WHITESPACE.has(body[i])) i++;
208
+ if (i >= body.length) break;
209
+ const nameStart = i;
210
+ while (i < body.length && !WHITESPACE.has(body[i]) && body[i] !== "=") i++;
211
+ const attrName = body.slice(nameStart, i);
212
+ if (attrName.length === 0) {
213
+ i++;
214
+ continue;
215
+ }
216
+ while (i < body.length && WHITESPACE.has(body[i])) i++;
217
+ if (body[i] !== "=") {
218
+ attrs.push([attrName, ""]);
219
+ continue;
220
+ }
221
+ i++;
222
+ while (i < body.length && WHITESPACE.has(body[i])) i++;
223
+ const quote = body[i];
224
+ if (quote === '"' || quote === "'") {
225
+ const valueStart = i + 1;
226
+ const valueEnd = body.indexOf(quote, valueStart);
227
+ const end = valueEnd === -1 ? body.length : valueEnd;
228
+ attrs.push([attrName, decodeXmlEntities(body.slice(valueStart, end))]);
229
+ i = end + 1;
230
+ } else {
231
+ const valueStart = i;
232
+ while (i < body.length && !WHITESPACE.has(body[i])) i++;
233
+ attrs.push([attrName, decodeXmlEntities(body.slice(valueStart, i))]);
234
+ }
235
+ }
236
+ return { name, attrs };
237
+ }
238
+ function parseXml(xml) {
239
+ const src = xml.charCodeAt(0) === 65279 ? xml.slice(1) : xml;
240
+ const root = newFrame("");
241
+ const stack = [root];
242
+ const len = src.length;
243
+ let i = 0;
244
+ const top = () => stack[stack.length - 1];
245
+ const pushText = (from, to) => {
246
+ if (to > from) top().text.push(decodeXmlEntities(src.slice(from, to)));
247
+ };
248
+ const closeElement = (name) => {
249
+ let target = -1;
250
+ for (let k = stack.length - 1; k >= 1; k--) {
251
+ if (stack[k].name === name) {
252
+ target = k;
253
+ break;
254
+ }
255
+ }
256
+ if (target === -1) return;
257
+ while (stack.length > target) {
258
+ const frame = stack.pop();
259
+ addChild(top(), frame.name, finishFrame(frame));
260
+ }
261
+ };
262
+ while (i < len) {
263
+ const lt = src.indexOf("<", i);
264
+ if (lt === -1) {
265
+ pushText(i, len);
266
+ break;
267
+ }
268
+ pushText(i, lt);
269
+ if (src.startsWith("<!--", lt)) {
270
+ const end2 = src.indexOf("-->", lt + 4);
271
+ i = end2 === -1 ? len : end2 + 3;
272
+ continue;
273
+ }
274
+ if (src.startsWith("<![CDATA[", lt)) {
275
+ const end2 = src.indexOf("]]>", lt + 9);
276
+ top().text.push(src.slice(lt + 9, end2 === -1 ? len : end2));
277
+ i = end2 === -1 ? len : end2 + 3;
278
+ continue;
279
+ }
280
+ if (src.startsWith("<!", lt)) {
281
+ i = skipDeclaration(src, lt);
282
+ continue;
283
+ }
284
+ if (src.startsWith("<?", lt)) {
285
+ const end2 = src.indexOf("?>", lt + 2);
286
+ const { name: name2, attrs: attrs2 } = parseTagBody(src.slice(lt + 2, end2 === -1 ? len : end2));
287
+ if (name2.length > 0) {
288
+ const frame2 = newFrame(`?${name2}`);
289
+ frame2.attrs = attrs2;
290
+ addChild(top(), frame2.name, finishFrame(frame2));
291
+ }
292
+ i = end2 === -1 ? len : end2 + 2;
293
+ continue;
294
+ }
295
+ if (src.startsWith("</", lt)) {
296
+ const end2 = src.indexOf(">", lt + 2);
297
+ closeElement(src.slice(lt + 2, end2 === -1 ? len : end2).trim());
298
+ i = end2 === -1 ? len : end2 + 1;
299
+ continue;
300
+ }
301
+ const end = findTagEnd(src, lt + 1);
302
+ const tagEnd = end === -1 ? len : end;
303
+ let body = src.slice(lt + 1, tagEnd);
304
+ const selfClosing = body.endsWith("/");
305
+ if (selfClosing) body = body.slice(0, -1);
306
+ const { name, attrs } = parseTagBody(body);
307
+ i = end === -1 ? len : end + 1;
308
+ if (name.length === 0) continue;
309
+ const frame = newFrame(name);
310
+ frame.attrs = attrs;
311
+ if (selfClosing) {
312
+ addChild(top(), name, finishFrame(frame));
313
+ continue;
314
+ }
315
+ if (stack.length > MAX_XML_DEPTH) {
316
+ throw new Error(`XML nesting deeper than ${MAX_XML_DEPTH} elements; refusing to parse (this file is not something any office application produces)`);
317
+ }
318
+ stack.push(frame);
319
+ }
320
+ while (stack.length > 1) {
321
+ const frame = stack.pop();
322
+ addChild(top(), frame.name, finishFrame(frame));
323
+ }
324
+ return Object.fromEntries(root.children);
325
+ }
326
+
116
327
  // src/ooxml_extract.ts
117
328
  var loadFflate = createLazyModuleLoader(
118
329
  async () => await import("fflate"),
119
330
  "office-file reading disabled (fflate unavailable)"
120
331
  );
121
- var loadXmlParser = createLazyModuleLoader(
122
- async () => await import("fast-xml-parser"),
123
- "office-file reading disabled (fast-xml-parser unavailable)"
124
- );
125
332
  var MAX_OOXML_INPUT_BYTES = 50 * 1024 * 1024;
126
333
  function accessFailureMessage(err, filePath) {
127
334
  const code = err?.code;
@@ -159,15 +366,7 @@ function decodeZipEntry(entries, entryPath) {
159
366
  return new TextDecoder("utf-8").decode(bytes);
160
367
  }
161
368
  async function parseOoxmlPart(xmlText2) {
162
- const fxp = await loadXmlParser();
163
- if (!fxp) throw new Error("fast-xml-parser is not installed; run `npm install fast-xml-parser` to enable this command");
164
- const parser = new fxp.XMLParser({
165
- ignoreAttributes: false,
166
- preserveOrder: false,
167
- trimValues: false,
168
- parseTagValue: false
169
- });
170
- return parser.parse(xmlText2);
369
+ return parseXml(xmlText2);
171
370
  }
172
371
  function pushTextValue(runs, val) {
173
372
  if (Array.isArray(val)) {
@@ -345,9 +544,9 @@ async function notesPathFor(entries, slidePath) {
345
544
  }
346
545
  return null;
347
546
  }
348
- async function parseSlide(entries, path30) {
349
- const xml = decodeZipEntry(entries, path30);
350
- if (xml === null) throw new Error(`missing part: ${path30}`);
547
+ async function parseSlide(entries, path31) {
548
+ const xml = decodeZipEntry(entries, path31);
549
+ if (xml === null) throw new Error(`missing part: ${path31}`);
351
550
  return parseOoxmlPart(xml);
352
551
  }
353
552
  async function notesTextFor(entries, notesPath) {
@@ -363,8 +562,8 @@ async function pptxOutline(filePath) {
363
562
  const { entries, slidePaths } = await listSlideParts(filePath);
364
563
  const out = [];
365
564
  for (let i = 0; i < slidePaths.length; i++) {
366
- const path30 = slidePaths[i];
367
- const parsed = await parseSlide(entries, path30);
565
+ const path31 = slidePaths[i];
566
+ const parsed = await parseSlide(entries, path31);
368
567
  const shapes = slideShapes(parsed);
369
568
  const titleShape = shapes.find((s) => {
370
569
  const t = shapePlaceholderType(s);
@@ -373,7 +572,7 @@ async function pptxOutline(filePath) {
373
572
  const title = titleShape !== void 0 ? shapeText(titleShape) : "";
374
573
  const allText = collectTextRuns(parsed, "a:t").join(" ");
375
574
  const bodyChars = Math.max(0, allText.length - title.length);
376
- const hasNotes = (await notesTextFor(entries, await notesPathFor(entries, path30))).length > 0;
575
+ const hasNotes = (await notesTextFor(entries, await notesPathFor(entries, path31))).length > 0;
377
576
  out.push({ slide: i + 1, title, bodyChars, hasNotes });
378
577
  }
379
578
  return out;
@@ -383,13 +582,13 @@ async function pptxSlideText(filePath, slideNumber, includeNotes) {
383
582
  if (slideNumber < 1 || slideNumber > slidePaths.length) {
384
583
  throw new Error(`slide ${slideNumber} out of range (this deck has ${slidePaths.length} slides)`);
385
584
  }
386
- const path30 = slidePaths[slideNumber - 1];
387
- const parsed = await parseSlide(entries, path30);
585
+ const path31 = slidePaths[slideNumber - 1];
586
+ const parsed = await parseSlide(entries, path31);
388
587
  const shapes = slideShapes(parsed);
389
588
  const blocks = [...shapes.map(shapeText).filter((t) => t.length > 0), ...tableRowBlocks(parsed)];
390
589
  const lines2 = [`# Slide ${slideNumber}`, ...blocks];
391
590
  if (includeNotes) {
392
- const notes = await notesTextFor(entries, await notesPathFor(entries, path30));
591
+ const notes = await notesTextFor(entries, await notesPathFor(entries, path31));
393
592
  if (notes.length > 0) lines2.push("", "## Speaker notes", notes);
394
593
  }
395
594
  return lines2.join("\n\n");
@@ -3023,19 +3222,30 @@ var CLAUDE_CODE_EVENT_NAMES = {
3023
3222
  notification: "Notification",
3024
3223
  stop: "Stop",
3025
3224
  pre_compact: "PreCompact",
3225
+ post_compact: "PostCompact",
3026
3226
  user_prompt_submit: "UserPromptSubmit",
3027
3227
  subagent_stop: "SubagentStop",
3028
- session_start: "SessionStart"
3228
+ session_start: "SessionStart",
3229
+ // Claude Code has no separate failure event -- a failed tool arrives on PostToolUse there,
3230
+ // and only Copilot splits it out. This entry exists because the map is exhaustive over
3231
+ // HookEventName, and it is a spelling for the response envelope rather than a claim that
3232
+ // Claude Code will ever send this event. Nothing iterates this map to build install config,
3233
+ // so naming an event Claude Code does not have cannot register one.
3234
+ post_tool_use_failure: "PostToolUseFailure"
3029
3235
  };
3030
3236
  var EVENTS_WITHOUT_ADDITIONAL_CONTEXT = /* @__PURE__ */ new Set([
3031
3237
  "notification",
3032
3238
  "pre_compact"
3033
3239
  ]);
3034
- function serializeOutput(output, eventName) {
3240
+ var EVENTS_WITH_RAW_STDOUT_CONTEXT = /* @__PURE__ */ new Set(["pre_compact"]);
3241
+ function serializeOutput(output, eventName, harness) {
3035
3242
  switch (output.hookType) {
3036
3243
  case "deny":
3037
3244
  return JSON.stringify({ decision: "block", reason: output.message });
3038
3245
  case "context":
3246
+ if (EVENTS_WITH_RAW_STDOUT_CONTEXT.has(eventName) && harness === "claudecode") {
3247
+ return output.context;
3248
+ }
3039
3249
  if (EVENTS_WITHOUT_ADDITIONAL_CONTEXT.has(eventName)) {
3040
3250
  return JSON.stringify({ systemMessage: output.context });
3041
3251
  }
@@ -3499,6 +3709,7 @@ var _bashOutputs = /* @__PURE__ */ new Map();
3499
3709
  var _grepQueries = /* @__PURE__ */ new Map();
3500
3710
  var _globQueries = /* @__PURE__ */ new Map();
3501
3711
  var _lastTabContext = null;
3712
+ var _seenImageHashes = [];
3502
3713
  var _outstandingAgentSpawns = [];
3503
3714
  var _outstandingAgentSpawnsAtLoad = [];
3504
3715
  var _bashReruns = /* @__PURE__ */ new Set();
@@ -3698,6 +3909,18 @@ function setLastTabContext(text) {
3698
3909
  function getLastTabContext() {
3699
3910
  return _lastTabContext;
3700
3911
  }
3912
+ var MAX_SEEN_IMAGE_HASHES = 16;
3913
+ function hasSeenImage(hash2) {
3914
+ return _seenImageHashes.includes(hash2);
3915
+ }
3916
+ function recordSeenImage(hash2) {
3917
+ const at = _seenImageHashes.indexOf(hash2);
3918
+ if (at !== -1) _seenImageHashes.splice(at, 1);
3919
+ _seenImageHashes.push(hash2);
3920
+ if (_seenImageHashes.length > MAX_SEEN_IMAGE_HASHES) {
3921
+ _seenImageHashes.splice(0, _seenImageHashes.length - MAX_SEEN_IMAGE_HASHES);
3922
+ }
3923
+ }
3701
3924
  var MAX_OUTSTANDING_AGENT_SPAWNS = 30;
3702
3925
  function recordOutstandingAgentSpawn(prompt) {
3703
3926
  _outstandingAgentSpawns.push({ prompt, ts: Date.now() });
@@ -3811,6 +4034,7 @@ function exportSessionState() {
3811
4034
  globQueries: Array.from(_globQueries.entries()),
3812
4035
  outstandingAgentSpawns: _outstandingAgentSpawns.map((e) => [e.prompt, e.ts]),
3813
4036
  ..._lastTabContext !== null ? { lastTabContext: _lastTabContext } : {},
4037
+ ..._seenImageHashes.length > 0 ? { seenImageHashes: [..._seenImageHashes] } : {},
3814
4038
  ..._compactedAt > 0 ? { compactedAt: _compactedAt } : {}
3815
4039
  };
3816
4040
  }
@@ -3836,6 +4060,7 @@ function importSessionState(s) {
3836
4060
  _outstandingAgentSpawns = (s.outstandingAgentSpawns ?? []).map(([prompt, ts]) => ({ prompt, ts }));
3837
4061
  _outstandingAgentSpawnsAtLoad = [..._outstandingAgentSpawns];
3838
4062
  _lastTabContext = s.lastTabContext ?? null;
4063
+ _seenImageHashes = [...s.seenImageHashes ?? []];
3839
4064
  _compactedAt = s.compactedAt ?? 0;
3840
4065
  }
3841
4066
  registerReset(() => {
@@ -3857,6 +4082,7 @@ registerReset(() => {
3857
4082
  _outstandingAgentSpawns = [];
3858
4083
  _outstandingAgentSpawnsAtLoad = [];
3859
4084
  _lastTabContext = null;
4085
+ _seenImageHashes = [];
3860
4086
  _compactedAt = 0;
3861
4087
  _sessionId = null;
3862
4088
  });
@@ -3879,9 +4105,11 @@ const VALID_HOOK_EVENTS = new Set([
3879
4105
  'notification',
3880
4106
  'stop',
3881
4107
  'pre_compact',
4108
+ 'post_compact',
3882
4109
  'user_prompt_submit',
3883
4110
  'subagent_stop',
3884
4111
  'session_start',
4112
+ 'post_tool_use_failure',
3885
4113
  ])`;
3886
4114
  var SHIM_TRY_IN_PROCESS = `// Attempts the in-process hook call: import()s dist/token-goat-hook.mjs (a sibling of
3887
4115
  // the baked token-goat entry path, built with zero load-time side effects -- unlike
@@ -4030,6 +4258,7 @@ var HOOK_EVENT_MAP = [
4030
4258
  ["PreToolUse", "pre_tool_use"],
4031
4259
  ["PostToolUse", "post_tool_use"],
4032
4260
  ["PreCompact", "pre_compact"],
4261
+ ["PostCompact", "post_compact"],
4033
4262
  ["UserPromptSubmit", "user_prompt_submit"],
4034
4263
  ["SubagentStop", "subagent_stop"],
4035
4264
  ["SessionStart", "session_start"]
@@ -4346,9 +4575,11 @@ const HOOK_EVENT_NAME_MAP = {
4346
4575
  notification: 'Notification',
4347
4576
  stop: 'Stop',
4348
4577
  pre_compact: 'PreCompact',
4578
+ post_compact: 'PostCompact',
4349
4579
  user_prompt_submit: 'UserPromptSubmit',
4350
4580
  subagent_stop: 'SubagentStop',
4351
4581
  session_start: 'SessionStart',
4582
+ post_tool_use_failure: 'PostToolUseFailure',
4352
4583
  }
4353
4584
 
4354
4585
  function stripTg(value) {
@@ -4729,10 +4960,19 @@ const path = require('node:path')
4729
4960
  const { pathToFileURL } = require('node:url')
4730
4961
 
4731
4962
  // Copilot event name -> token-goat internal HookEventName (src/types.ts's
4732
- // HOOK_EVENTS). Only these seven have a token-goat handler; every other real
4733
- // Copilot event (sessionEnd, postToolUseFailure, subagentStart,
4734
- // errorOccurred, notification, permissionRequest) is left unimplemented
4963
+ // HOOK_EVENTS). Only these eight have a token-goat handler; every other real
4964
+ // Copilot event (sessionEnd, subagentStart, errorOccurred, notification,
4965
+ // permissionRequest) is left unimplemented
4735
4966
  // rather than guessed at, and falls through to the default no-op below.
4967
+ // postToolUseFailure is the newest of the eight and the only one whose channel
4968
+ // costs tokens instead of saving them: it fires instead of postToolUse when a
4969
+ // tool result is a failure, and accepts only additionalContext back --
4970
+ // modifiedResult and suppressOutput are not honored there, so a failed result
4971
+ // still cannot be fenced, compressed or shrunk. It is wired anyway because
4972
+ // additionalContext demonstrably reaches the model (see the postToolUseFailure
4973
+ // branch in translate() for the bundle offset), and hooks_tool_failure.ts
4974
+ // spends that channel only on an exact repeat failure, where staying silent
4975
+ // costs a whole wasted retry.
4736
4976
  // 'sessionStart' was previously a permanent no-op on the stated grounds that
4737
4977
  // token-goat has no internal session_start handler. That was simply wrong --
4738
4978
  // hooks_session_start.ts has long emitted the command-routing reminder that
@@ -4752,6 +4992,7 @@ const COPILOT_TO_TG_EVENT = {
4752
4992
  agentStop: 'stop',
4753
4993
  subagentStop: 'subagent_stop',
4754
4994
  userPromptSubmitted: 'user_prompt_submit',
4995
+ postToolUseFailure: 'post_tool_use_failure',
4755
4996
  }
4756
4997
 
4757
4998
  // Copilot built-in tool name -> token-goat internal tool name. Confirmed via
@@ -4766,9 +5007,47 @@ const COPILOT_TO_TG_EVENT = {
4766
5007
  // MCP-server tool invocations (<server-name>-<tool-name>) have no
4767
5008
  // token-goat equivalent and are passed through unmapped (safe no-op for
4768
5009
  // handlers that don't recognize the name).
5010
+ // 'read_bash'/'read_powershell' are the background-shell output pollers, and
5011
+ // they are the exact shape of Claude Code's BashOutput: Copilot's shell tool
5012
+ // is async, so a long-running command is started once and then re-read over
5013
+ // and over within one turn, each read returning the accumulated output again.
5014
+ // hooks_bashoutput.ts already collapses that into a delta (or a short
5015
+ // "unchanged" marker) for Claude Code, and now does the same here. Both the
5016
+ // names and the argument key were read out of the shipping 1.0.80 bundle
5017
+ // rather than guessed: the builtin tool-name table in
5018
+ // prebuilds/win32-x64/runtime.node lists read_bash/stop_bash/list_bash and
5019
+ // read_powershell/stop_powershell/list_powershell, and the poller's own input
5020
+ // schema in app.js is {shellId, delay} -- so the shell id needs the
5021
+ // POLL_ID_ARG_KEY remap below to reach postBashOutputHandler, which reads
5022
+ // 'bash_id'. read_shell/stop_shell/list_shells, which an earlier static sweep
5023
+ // of the same binary suggested were the real names, are internal Rust
5024
+ // identifiers (tool_read_shell_prepare_input, tool_list_shells_descriptor,
5025
+ // PreparedStopShellInput, and the serde field names of the shell config
5026
+ // struct), never wire tool names, so they are deliberately absent here.
5027
+ // stop_bash/list_bash and their powershell twins stay unmapped because
5028
+ // token-goat has no KillShell-equivalent handler for them to reach; a mapping
5029
+ // would be pure decoration.
5030
+ // 'bash'/'powershell' stay exactly as they were. The same sweep suggested the
5031
+ // executor had been renamed to write_bash/write_powershell and that this
5032
+ // mapping was dead, and that is not what the bundle says: app.js resolves the
5033
+ // executor as shellConfig?.shellToolName ?? "bash" (with "powershell" as the
5034
+ // Windows default of the same config), and its command lives under 'command'
5035
+ // exactly as hooks_bash.ts expects. write_bash/write_powershell are real tool
5036
+ // names, but they are a different tool -- their schema is {shellId, input,
5037
+ // delay} and the bundle files them under the subtype "write_shell", i.e. send
5038
+ // stdin to an already-running shell, not run a command. Mapping them to Bash
5039
+ // would label a stdin write as a shell execution, so they are left unmapped.
5040
+ // 'task', 'read_agent' and 'memory'-family tools are likewise left alone:
5041
+ // task's result is assembled in the native addon and its shape is unknown,
5042
+ // read_agent is an incremental poll that postAgentHandler is not written for,
5043
+ // and the real tool names behind memory were never confirmed. Each would put
5044
+ // a handler that rewrites model-visible output in front of a payload shape
5045
+ // nobody has seen, which is worse than leaving the compression on the table.
4769
5046
  const TOOL_TO_TG = {
4770
5047
  bash: 'Bash',
4771
5048
  powershell: 'Bash',
5049
+ read_bash: 'BashOutput',
5050
+ read_powershell: 'BashOutput',
4772
5051
  view: 'Read',
4773
5052
  create: 'Write',
4774
5053
  edit: 'Edit',
@@ -4809,6 +5088,17 @@ const FILE_PATH_ARG_KEY = {
4809
5088
  create: 'path',
4810
5089
  }
4811
5090
 
5091
+ // read_bash/read_powershell send the background shell's id under 'shellId' (confirmed
5092
+ // against the poller's input schema in the shipping 1.0.80 app.js: {shellId, delay});
5093
+ // postBashOutputHandler reads 'bash_id'. Without this the tool-name mapping above would
5094
+ // be inert -- the handler bails on a missing bash_id and every poll would keep costing
5095
+ // the full accumulated output. Same keying convention as FILE_PATH_ARG_KEY: the ORIGINAL
5096
+ // Copilot tool name, since that is what the argument shape belongs to.
5097
+ const POLL_ID_ARG_KEY = {
5098
+ read_bash: 'shellId',
5099
+ read_powershell: 'shellId',
5100
+ }
5101
+
4812
5102
  // Copilot spawns a brand-new process for every single hook invocation (no long-lived plugin
4813
5103
  // process the way OpenClaw's is -- OPENCLAW_HOOK_SCRIPT's own \`copilot-\${process.pid}-\${Date.now()}\`
4814
5104
  // fallback is safe there specifically because that process lives for the whole session, so the
@@ -4825,13 +5115,19 @@ function stableFallbackSessionId(cwd) {
4825
5115
  }
4826
5116
 
4827
5117
  function remapToolInput(copilotToolName, input) {
5118
+ if (!input || typeof input !== 'object') return input
5119
+ let out = input
5120
+ // Add the canonical key alongside the original rather than renaming it, so nothing that
5121
+ // might read the original 'path'/'shellId' key elsewhere (e.g. a future handler) loses it.
4828
5122
  const pathKey = FILE_PATH_ARG_KEY[copilotToolName]
4829
- if (pathKey === undefined || !input || typeof input !== 'object' || !(pathKey in input)) {
4830
- return input
5123
+ if (pathKey !== undefined && pathKey in out) {
5124
+ out = Object.assign({}, out, { file_path: out[pathKey] })
5125
+ }
5126
+ const idKey = POLL_ID_ARG_KEY[copilotToolName]
5127
+ if (idKey !== undefined && idKey in out) {
5128
+ out = Object.assign({}, out, { bash_id: out[idKey] })
4831
5129
  }
4832
- // Add file_path alongside the original key rather than renaming it, so nothing that
4833
- // might read the original 'path' key elsewhere (e.g. a future handler) loses it.
4834
- return Object.assign({}, input, { file_path: input[pathKey] })
5130
+ return out
4835
5131
  }
4836
5132
 
4837
5133
  // Attempts the in-process hook call: import()s dist/token-goat-hook.mjs (a sibling of
@@ -4983,7 +5279,46 @@ function translate(copilotEvent, resp) {
4983
5279
  }
4984
5280
 
4985
5281
  if (copilotEvent === 'postToolUse') {
4986
- // Confirmed against https://docs.github.com/en/copilot/reference/hooks-reference: postToolUse accepts modifiedResult ({resultType: 'success', textResultForLlm: string}) to replace the tool output, and additionalContext can coexist with it in the same response -- token-goat's rewriteOutput producers (compression, injection fencing, image shrink) map to modifiedResult here; resultType is hardcoded 'success' since token-goat never intends to fail the tool call. Emitted camelCase-only, not the snake_case "VS Code compatible" variant the inbound side handles around line 247: the hooks reference doc selects that format by registering the event name in PascalCase, and COPILOT_CLI_HOOK_EVENTS (copilot_cli_install.ts) registers every event this shim handles in camelCase, so a token-goat-installed hook can never be in that mode and a snake_case response would be wrong here, not merely redundant.
5282
+ // Verified against the shipping @github/copilot 1.0.80 bundle, not against the docs page.
5283
+ // NativeHookPipelineProcessor.postToolExecution (app.js offset 2043150) gates on
5284
+ // toolResult.resultType === "success", then does n.toolResultJson && CSr(e.toolResult,
5285
+ // n.toolResultJson), where CSr (offset 2032350) is an in-place Object.assign; the mutated
5286
+ // object is re-serialized back to native in the postTool callback at offset 1793926. So
5287
+ // modifiedResult IS honored on this event, and token-goat's rewriteOutput producers
5288
+ // (compression, injection fencing, image shrink) really do reach the model. resultType is
5289
+ // hardcoded 'success' because success is the only branch this event ever runs on.
5290
+ //
5291
+ // additionalContext on THIS event is dropped on the JS path. grep -abo "onAdditionalContext:"
5292
+ // app.js returns nothing, so the callback is never supplied, and the two "onAdditionalContext?"
5293
+ // call sites (offsets 2041896 and 2043300) are both no-ops. preToolsExecution (offset 2041832)
5294
+ // additionally pushes each context into an array that IS drained into the native return
5295
+ // payload (additional_contexts, offset 1791950); postToolExecution has no such push and its
5296
+ // native return payload (offset 1793926) has no additional_contexts key. Residual, stated
5297
+ // honestly: the native hookProcessorPostToolUse might fold additionalContext into the
5298
+ // toolResultJson it returns, and that was NOT verified. The evidence leans against it, since
5299
+ // the failure sibling path appends its context explicitly in JS via
5300
+ // hookAppendPostToolUseFailureContext and there is no success-path counterpart. The
5301
+ // out.additionalContext below is kept as cheap best-effort, not as a channel anything should
5302
+ // depend on -- see src/pending_context.ts, whose whole design depended on it.
5303
+ //
5304
+ // Failed tool calls never reach this event. postToolUse and postToolUseFailure are two
5305
+ // distinct hook events (both listed in the runtime.node hook-event enum at offset 101618150),
5306
+ // and the shipped copilot-sdk/types.d.ts says onPostToolUse "does not fire for non-success
5307
+ // results". The failure event cannot carry a fence either: PostToolUseFailureHookInput carries
5308
+ // only a stringified error message, not the tool result, and PostToolUseFailureHookOutput
5309
+ // consumes only additionalContext -- "modifiedResult or suppressOutput are not honored for
5310
+ // failure hooks". rejected/denied/timeout results trigger no post hook at all. So on Copilot,
5311
+ // the output of a failed tool call reaches the model unfenced, uncompressed and unshrunk, and
5312
+ // no response shape this shim could emit changes that. That gap is real and still open. What
5313
+ // is now wired is the narrower thing that IS possible there: the postToolUseFailure branch
5314
+ // below carries advisory text alongside the failure, and never rewrites it.
5315
+ //
5316
+ // Emitted camelCase-only. The inbound side around line 247 also tolerates a snake_case "VS
5317
+ // Code compatible" shape, and this comment used to justify the camelCase choice by claiming
5318
+ // PascalCase event registration selects the snake_case response format. That reasoning is
5319
+ // UNVERIFIED in 1.0.80: every hook-response field in the native string tables is camelCase,
5320
+ // and the snake_case hits in the bundle are unrelated internal Rust identifiers. camelCase
5321
+ // stays because it is what the bundle reads; the old justification is no longer asserted.
4987
5322
  const hso = resp && resp.hookSpecificOutput
4988
5323
  const updatedToolOutput = hso && hso.updatedToolOutput
4989
5324
  const context = extractContext(resp)
@@ -4995,6 +5330,25 @@ function translate(copilotEvent, resp) {
4995
5330
  return out
4996
5331
  }
4997
5332
 
5333
+ if (copilotEvent === 'postToolUseFailure') {
5334
+ // The failed-tool twin of postToolUse, and the only response field it accepts is
5335
+ // additionalContext: the shipped copilot-sdk/types.d.ts says "modifiedResult or suppressOutput
5336
+ // are not honored for failure hooks", so nothing here can fence, compress or shrink the failed
5337
+ // output -- that gap is real and stays open. What is NOT open, and was the reason this event
5338
+ // went unwired for so long, is whether additionalContext reaches the model at all. It does:
5339
+ // app.js 1.0.80 at offset 2043380 either folds it into textResultForLlm (when
5340
+ // appendFailureContextToToolResult is set) or has the native
5341
+ // hookAppendPostToolUseFailureContext push {content, source:'system'} onto
5342
+ // toolResult.newMessages. That is the exact opposite of postToolUse, whose additionalContext
5343
+ // the JS side drops on the floor, so neither event's behaviour generalises to the other.
5344
+ //
5345
+ // Because this channel spends tokens rather than saving them, the handler behind it
5346
+ // (hooks_tool_failure.ts) is silent on a first failure and speaks only on an exact repeat.
5347
+ const context = extractContext(resp)
5348
+ if (context) return { additionalContext: context }
5349
+ return {}
5350
+ }
5351
+
4998
5352
  if (copilotEvent === 'sessionStart') {
4999
5353
  // sessionStart has no tool result to modify -- only additionalContext applies, and it's the
5000
5354
  // one channel that reaches the model before it picks its first read tool, so this is where
@@ -5018,12 +5372,47 @@ function translate(copilotEvent, resp) {
5018
5372
  return { decision: 'allow' }
5019
5373
  }
5020
5374
 
5021
- // preCompact / userPromptSubmitted: confirmed against the hooks reference
5022
- // doc that both are notification-only -- Copilot never reads a response
5023
- // body for either, so any additionalContext/systemMessage token-goat
5024
- // produces has no surfacing channel here. This still routes through the
5025
- // token-goat hook call above so the internal handler's own side effects keep
5026
- // running; only the response is discarded.
5375
+ if (copilotEvent === 'userPromptSubmitted') {
5376
+ // Copilot's own hooks reference says command-hook output here "is dropped, including
5377
+ // modifiedPrompt", and this branch used to believe it and return nothing. That is wrong for
5378
+ // additionalContext on 1.0.80, established by experiment rather than by reading: a config-file
5379
+ // command hook (at <cwd>/.github/hooks/, the project scope -- the user scope ~/.copilot/hooks/
5380
+ // was never exercised) returned {"additionalContext":"<marker>"} and the marker turned up
5381
+ // verbatim inside the session's user.message.transformedContent, wrapped in a
5382
+ // <system_reminder> block. What settles that it reached the model rather than only the on-disk
5383
+ // record is the billing: the provider's returned usage charged ~140 input tokens for a turn
5384
+ // whose raw content field is 35 bytes, so the marker's bytes were paid for whichever field
5385
+ // carried them. (The weaker argument first offered for this -- that transformedContent carries
5386
+ // an envelope the content field lacks, 29 B vs 195 B -- was measured on the control turn that
5387
+ // had no marker in it, and proves nothing about the marker.) That the model sees it is the
5388
+ // whole point: hook.start/hook.end records also persist and reach nothing.
5389
+ //
5390
+ // Scope of the finding, stated honestly: demonstrated ONCE on 1.0.80, not shown to be
5391
+ // reliable. Of two turns in that experiment, one delivered the marker and one fired a
5392
+ // userPromptSubmitted hook that produced no output and never ran the script; no explanation
5393
+ // was established and the rate is unknown. A hint that silently fails to arrive costs nothing
5394
+ // and breaks nothing here, which is why the direct return is still the right default.
5395
+ //
5396
+ // modifiedPrompt is NOT claimed to work and is not wanted: rewriting a user's prompt is far
5397
+ // more invasive than anything token-goat does, so only additionalContext is forwarded.
5398
+ //
5399
+ // This is also the write end of any post-compaction channel. Copilot has no postCompact hook.
5400
+ // Whether the summary is recoverable from events.jsonl is NOT settled -- the emit()/
5401
+ // emitEphemeral() distinction does not gate the writer, and the real decision is in native
5402
+ // code; see COPILOT_NO_POST_COMPACT_REASON in ../bridges_status.ts for what was and was not
5403
+ // established. The read end that IS confirmed is preCompact, which fires as a notification, so
5404
+ // a manifest can be built there and drained here without reading the event log at all.
5405
+ const context = extractContext(resp)
5406
+ if (context) return { additionalContext: context }
5407
+ return {}
5408
+ }
5409
+
5410
+ // preCompact is the genuine notification-only case, and the contrast with userPromptSubmitted
5411
+ // above is why this fallthrough is worth a comment at all. The hooks reference marks it
5412
+ // "No -- notification only" for output processing, and unlike the additionalContext claim that
5413
+ // turned out to be false, this one is confirmed in the shipping bundle: both preCompact call
5414
+ // sites in app.js (1.0.79 and re-checked in 1.0.80) await the hook and never assign its result.
5415
+ // There is no field to aim at here, so nothing to reconsider on the next version bump.
5027
5416
  return {}
5028
5417
  }
5029
5418
 
@@ -5065,7 +5454,8 @@ var COPILOT_CLI_HOOK_EVENTS = [
5065
5454
  "preCompact",
5066
5455
  "agentStop",
5067
5456
  "subagentStop",
5068
- "userPromptSubmitted"
5457
+ "userPromptSubmitted",
5458
+ "postToolUseFailure"
5069
5459
  ];
5070
5460
  function copilotCliUserRoot() {
5071
5461
  const override = process.env["COPILOT_HOME"];
@@ -5078,6 +5468,23 @@ function copilotCliUserHooksDir() {
5078
5468
  function copilotCliProjectHooksDir() {
5079
5469
  return path5.join(process.cwd(), ".github", "hooks");
5080
5470
  }
5471
+ function copilotCliCacheRoot() {
5472
+ const override = process.env["COPILOT_CACHE_HOME"];
5473
+ if (override !== void 0 && override.trim() !== "") return path5.resolve(override);
5474
+ const home = os3.homedir();
5475
+ if (process.platform === "darwin") return path5.join(home, "Library", "Caches", "copilot");
5476
+ if (process.platform === "win32") {
5477
+ const local = process.env["LOCALAPPDATA"];
5478
+ const base2 = local !== void 0 && local.trim() !== "" ? local : path5.join(home, ".cache");
5479
+ return path5.join(base2, "copilot");
5480
+ }
5481
+ const xdg = process.env["XDG_CACHE_HOME"];
5482
+ const base = xdg !== void 0 && xdg.trim() !== "" ? xdg : path5.join(home, ".cache");
5483
+ return path5.join(base, "copilot");
5484
+ }
5485
+ function copilotCliMcpToolsDir() {
5486
+ return path5.join(copilotCliCacheRoot(), "mcp-tools");
5487
+ }
5081
5488
  function copilotCliHooksDir(opts = {}) {
5082
5489
  return opts.local === true ? copilotCliProjectHooksDir() : copilotCliUserHooksDir();
5083
5490
  }
@@ -6006,11 +6413,15 @@ function sessionFileStem(sessionId) {
6006
6413
  return `${saltedStemPrefix(sessionId.slice(0, sep))}${digest}`;
6007
6414
  }
6008
6415
  function sessionPath(sessionId) {
6416
+ return sessionSidecarPath(sessionId, ".json");
6417
+ }
6418
+ function sessionSidecarPath(sessionId, suffix) {
6009
6419
  if (!sessionId) return null;
6010
6420
  const safe = sessionFileStem(sessionId);
6011
6421
  if (!safe) return null;
6422
+ if (suffix.includes("/") || suffix.includes("\\") || suffix.includes("..")) return null;
6012
6423
  const dir = path10.join(tokenGoatHome(), SESSIONS_SUBDIR);
6013
- const candidate = path10.join(dir, `${safe}.json`);
6424
+ const candidate = path10.join(dir, `${safe}${suffix}`);
6014
6425
  try {
6015
6426
  const rel = path10.relative(dir, candidate);
6016
6427
  if (rel.startsWith("..")) return null;
@@ -6099,6 +6510,7 @@ function coerce(raw) {
6099
6510
  ) : [];
6100
6511
  const cliReads = Array.isArray(o["cliReads"]) ? o["cliReads"].filter((h) => typeof h === "string") : [];
6101
6512
  const bashReruns = Array.isArray(o["bashReruns"]) ? o["bashReruns"].filter((h) => typeof h === "string") : [];
6513
+ const seenImageHashes = Array.isArray(o["seenImageHashes"]) ? o["seenImageHashes"].filter((h) => typeof h === "string") : [];
6102
6514
  const pendingLargeFileHints = Array.isArray(o["pendingLargeFileHints"]) ? o["pendingLargeFileHints"].filter(
6103
6515
  (p) => Array.isArray(p) && p.length === 2 && typeof p[0] === "string" && typeof p[1] === "number"
6104
6516
  ) : [];
@@ -6126,6 +6538,7 @@ function coerce(raw) {
6126
6538
  globQueries,
6127
6539
  outstandingAgentSpawns,
6128
6540
  ...typeof o["lastTabContext"] === "string" ? { lastTabContext: o["lastTabContext"] } : {},
6541
+ ...seenImageHashes.length > 0 ? { seenImageHashes } : {},
6129
6542
  ...typeof o["compactedAt"] === "number" ? { compactedAt: o["compactedAt"] } : {},
6130
6543
  ...typeof o["created_ts"] === "number" ? { created_ts: o["created_ts"] } : {}
6131
6544
  };
@@ -6233,12 +6646,21 @@ function mergeSessionState(disk, mem) {
6233
6646
  grepQueries: mergePairs(disk.grepQueries ?? [], mem.grepQueries ?? []),
6234
6647
  globQueries: mergePairs(disk.globQueries ?? [], mem.globQueries ?? []),
6235
6648
  outstandingAgentSpawns: mergeOutstandingAgentSpawns(disk.outstandingAgentSpawns ?? [], mem.outstandingAgentSpawns ?? []),
6649
+ // An accumulating collection, so union rather than pick a winner: two hook processes can
6650
+ // each see a screenshot the other never did. Disk first, then mem, so the order stays
6651
+ // oldest-to-newest and the cap evicts the oldest -- the same policy recordSeenImage applies
6652
+ // in memory, applied again here because a union of two capped lists can exceed the cap.
6653
+ seenImageHashes: mergeSeenImageHashes(disk.seenImageHashes ?? [], mem.seenImageHashes ?? []),
6236
6654
  // Last-seen scalar, not an accumulating collection: prefer mem's value (this process's freshest observation) over disk's, since a newer write always supersedes an older one.
6237
6655
  ...mem.lastTabContext !== void 0 ? { lastTabContext: mem.lastTabContext } : disk.lastTabContext !== void 0 ? { lastTabContext: disk.lastTabContext } : {},
6238
6656
  // Prefer the value already on disk: it marks the original creation time, and must never be bumped forward to the merge's "now". `mem` never carries one (it is not tracked in memory), so this is really "keep whatever disk has".
6239
6657
  ...disk.created_ts !== void 0 ? { created_ts: disk.created_ts } : mem.created_ts !== void 0 ? { created_ts: mem.created_ts } : {}
6240
6658
  };
6241
6659
  }
6660
+ function mergeSeenImageHashes(disk, mem) {
6661
+ const merged = Array.from(/* @__PURE__ */ new Set([...disk, ...mem]));
6662
+ return merged.length > MAX_SEEN_IMAGE_HASHES ? merged.slice(merged.length - MAX_SEEN_IMAGE_HASHES) : merged;
6663
+ }
6242
6664
  function capFiles(s, max) {
6243
6665
  if (s.files.length <= max) return s;
6244
6666
  const kept = [...s.files].sort((a, b) => b.lastReadAt - a.lastReadAt).slice(0, max);
@@ -7075,6 +7497,19 @@ async function probeImageMeta(input) {
7075
7497
  return null;
7076
7498
  }
7077
7499
  }
7500
+ async function imageQualifiesForShrink(input) {
7501
+ if (input.length >= DEFAULT_SIZE_THRESHOLD_BYTES) return true;
7502
+ const sharp = await loadSharp();
7503
+ if (sharp === null) return false;
7504
+ try {
7505
+ const cfg = loadConfig().image_shrink;
7506
+ const limitInputPixels = cfg.max_image_pixels > 0 ? cfg.max_image_pixels : false;
7507
+ const meta = await sharp(input, { limitInputPixels }).metadata();
7508
+ return Math.max(meta.width ?? 0, meta.height ?? 0) > DEFAULT_MAX_DIMENSION;
7509
+ } catch {
7510
+ return false;
7511
+ }
7512
+ }
7078
7513
  async function shrinkImage(input, opts) {
7079
7514
  const cfg = loadConfig().image_shrink;
7080
7515
  const maxDimension = opts?.maxDimension ?? DEFAULT_MAX_DIMENSION;
@@ -7196,7 +7631,6 @@ async function preReadImageHandler(event) {
7196
7631
  pruneShrinkCache();
7197
7632
  const stat2 = statInfo(filePath);
7198
7633
  if (stat2 === null) return passOutput();
7199
- const size = stat2.size;
7200
7634
  const quality = loadConfig().image_shrink.jpeg_quality;
7201
7635
  const cached = findCachedShrink(filePath, stat2.size, stat2.mtimeMs, quality);
7202
7636
  if (cached !== null) {
@@ -7223,34 +7657,13 @@ async function preReadImageHandler(event) {
7223
7657
  } catch {
7224
7658
  }
7225
7659
  }
7226
- let input = null;
7227
- let qualifies = size >= DEFAULT_SIZE_THRESHOLD_BYTES;
7228
- if (!qualifies) {
7229
- try {
7230
- input = fs16.readFileSync(filePath);
7231
- } catch {
7232
- return passOutput();
7233
- }
7234
- const sharp = await loadSharp();
7235
- if (sharp === null) return passOutput();
7236
- try {
7237
- const cfg = loadConfig().image_shrink;
7238
- const limitInputPixels = cfg.max_image_pixels > 0 ? cfg.max_image_pixels : false;
7239
- const meta = await sharp(input, { limitInputPixels }).metadata();
7240
- const longestEdge = Math.max(meta.width ?? 0, meta.height ?? 0);
7241
- qualifies = longestEdge > DEFAULT_MAX_DIMENSION;
7242
- } catch {
7243
- return passOutput();
7244
- }
7245
- }
7246
- if (!qualifies) return passOutput();
7247
- if (input === null) {
7248
- try {
7249
- input = fs16.readFileSync(filePath);
7250
- } catch {
7251
- return passOutput();
7252
- }
7660
+ let input;
7661
+ try {
7662
+ input = fs16.readFileSync(filePath);
7663
+ } catch {
7664
+ return passOutput();
7253
7665
  }
7666
+ if (!await imageQualifiesForShrink(input)) return passOutput();
7254
7667
  const result = await shrinkImage(input, { quality, sizeThresholdBytes: 0 });
7255
7668
  if (result === null) {
7256
7669
  recordStat("image_shrink_skipped");
@@ -7261,30 +7674,432 @@ async function preReadImageHandler(event) {
7261
7674
  }
7262
7675
  registerHook("pre_tool_use", preReadImageHandler, { toolName: "Read" });
7263
7676
 
7264
- // src/embeddings.ts
7677
+ // src/embed_model.ts
7678
+ import { createHash as createHash4 } from "node:crypto";
7679
+ import * as fs17 from "node:fs";
7265
7680
  import { createRequire as createRequire3 } from "node:module";
7266
- var _require2 = createRequire3(import.meta.url);
7267
- var _transformer = null;
7268
- var _transformerError = null;
7269
- var _transformerLoadAttempted = false;
7270
- function ensureTransformerLoaded() {
7271
- if (_transformerLoadAttempted) return;
7272
- _transformerLoadAttempted = true;
7273
- try {
7274
- _transformer = _require2("@xenova/transformers");
7275
- if (loadConfig().network.offline) {
7276
- const env = _transformer.env;
7277
- if (env) env["allowRemoteModels"] = false;
7681
+ import * as path13 from "node:path";
7682
+ import { pipeline } from "node:stream/promises";
7683
+
7684
+ // src/embed_tokenizer.ts
7685
+ var MAX_SEQUENCE_TOKENS = 512;
7686
+ var UnsupportedTokenizerError = class extends Error {
7687
+ constructor(message) {
7688
+ super(message);
7689
+ this.name = "UnsupportedTokenizerError";
7690
+ }
7691
+ };
7692
+ function fail(message) {
7693
+ throw new UnsupportedTokenizerError(message);
7694
+ }
7695
+ function asRecord(value, path31) {
7696
+ if (typeof value !== "object" || value === null || Array.isArray(value)) fail(`${path31} is not an object`);
7697
+ return value;
7698
+ }
7699
+ function asString(value, path31) {
7700
+ if (typeof value !== "string") fail(`${path31} is not a string`);
7701
+ return value;
7702
+ }
7703
+ function asArray2(value, path31) {
7704
+ if (!Array.isArray(value)) fail(`${path31} is not an array`);
7705
+ return value;
7706
+ }
7707
+ function expectValue(actual, wanted, path31) {
7708
+ if (actual !== wanted) fail(`${path31} is ${JSON.stringify(actual)}, expected ${JSON.stringify(wanted)}`);
7709
+ }
7710
+ function isChinese(cp) {
7711
+ return cp >= 19968 && cp <= 40959 || cp >= 13312 && cp <= 19903 || cp >= 131072 && cp <= 173791 || cp >= 173824 && cp <= 177983 || cp >= 177984 && cp <= 178207 || cp >= 178208 && cp <= 183983 || cp >= 63744 && cp <= 64255 || cp >= 194560 && cp <= 195103;
7712
+ }
7713
+ var PUNCTUATION = new RegExp("\\p{P}", "u");
7714
+ function isPunctuation(ch) {
7715
+ const cp = ch.codePointAt(0) ?? 0;
7716
+ if (cp >= 33 && cp <= 47 || cp >= 58 && cp <= 64 || cp >= 91 && cp <= 96 || cp >= 123 && cp <= 126) {
7717
+ return true;
7718
+ }
7719
+ return PUNCTUATION.test(ch);
7720
+ }
7721
+ var CONTROL = new RegExp("\\p{Cc}|\\p{Cf}|\\p{Co}|\\p{Cs}", "u");
7722
+ function isControl(ch) {
7723
+ if (ch === " " || ch === "\n" || ch === "\r") return false;
7724
+ return CONTROL.test(ch);
7725
+ }
7726
+ var SPACE_SEPARATOR = new RegExp("\\p{Zs}", "u");
7727
+ function isWhitespace(ch) {
7728
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r" || SPACE_SEPARATOR.test(ch);
7729
+ }
7730
+ var COMBINING_MARK = /[\u0300-\u036f]/gu;
7731
+ function normalize(text) {
7732
+ let out = "";
7733
+ for (const ch of text) {
7734
+ const cp = ch.codePointAt(0) ?? 0;
7735
+ if (cp === 0 || cp === 65533 || isControl(ch)) continue;
7736
+ if (isWhitespace(ch)) {
7737
+ out += " ";
7738
+ continue;
7278
7739
  }
7279
- } catch (e) {
7280
- _transformerError = e instanceof Error ? e : new Error(String(e));
7740
+ if (isChinese(cp)) {
7741
+ out += ` ${ch} `;
7742
+ continue;
7743
+ }
7744
+ out += ch;
7281
7745
  }
7746
+ return out.normalize("NFD").replace(COMBINING_MARK, "").toLowerCase();
7747
+ }
7748
+ function preTokenize(text) {
7749
+ const words = [];
7750
+ for (const piece of text.split(/\s+/)) {
7751
+ if (!piece) continue;
7752
+ let buf = "";
7753
+ for (const ch of piece) {
7754
+ if (isPunctuation(ch)) {
7755
+ if (buf) {
7756
+ words.push(buf);
7757
+ buf = "";
7758
+ }
7759
+ words.push(ch);
7760
+ } else {
7761
+ buf += ch;
7762
+ }
7763
+ }
7764
+ if (buf) words.push(buf);
7765
+ }
7766
+ return words;
7767
+ }
7768
+ function readSpec(raw) {
7769
+ const spec = asRecord(raw, "tokenizer.json");
7770
+ const normalizer = asRecord(spec["normalizer"], "normalizer");
7771
+ expectValue(normalizer["type"], "BertNormalizer", "normalizer.type");
7772
+ expectValue(normalizer["clean_text"], true, "normalizer.clean_text");
7773
+ expectValue(normalizer["handle_chinese_chars"], true, "normalizer.handle_chinese_chars");
7774
+ expectValue(normalizer["lowercase"], true, "normalizer.lowercase");
7775
+ const stripAccents = normalizer["strip_accents"];
7776
+ if (stripAccents !== null && stripAccents !== true) {
7777
+ fail(`normalizer.strip_accents is ${JSON.stringify(stripAccents)}, expected null or true`);
7778
+ }
7779
+ expectValue(asRecord(spec["pre_tokenizer"], "pre_tokenizer")["type"], "BertPreTokenizer", "pre_tokenizer.type");
7780
+ const model = asRecord(spec["model"], "model");
7781
+ expectValue(model["type"], "WordPiece", "model.type");
7782
+ expectValue(model["continuing_subword_prefix"], "##", "model.continuing_subword_prefix");
7783
+ const post = asRecord(spec["post_processor"], "post_processor");
7784
+ expectValue(post["type"], "TemplateProcessing", "post_processor.type");
7785
+ const template = asArray2(post["single"], "post_processor.single").map((part) => {
7786
+ const special = asRecord(part, "post_processor.single[]")["SpecialToken"];
7787
+ return special === void 0 ? "A" : asString(asRecord(special, "SpecialToken")["id"], "SpecialToken.id");
7788
+ }).join(" ");
7789
+ if (template !== "[CLS] A [SEP]") fail(`post_processor.single is "${template}", expected "[CLS] A [SEP]"`);
7790
+ const vocab = /* @__PURE__ */ new Map();
7791
+ for (const [token, id] of Object.entries(asRecord(model["vocab"], "model.vocab"))) {
7792
+ if (typeof id !== "number" || !Number.isInteger(id)) fail(`model.vocab["${token}"] is not an integer id`);
7793
+ vocab.set(token, id);
7794
+ }
7795
+ const maxChars = model["max_input_chars_per_word"];
7796
+ if (maxChars !== void 0 && (typeof maxChars !== "number" || !Number.isInteger(maxChars) || maxChars < 1)) {
7797
+ fail("model.max_input_chars_per_word is not a positive integer");
7798
+ }
7799
+ return {
7800
+ vocab,
7801
+ unkToken: asString(model["unk_token"], "model.unk_token"),
7802
+ maxInputCharsPerWord: typeof maxChars === "number" ? maxChars : 100
7803
+ };
7282
7804
  }
7805
+ var BertWordPiece = class _BertWordPiece {
7806
+ vocab;
7807
+ maxInputCharsPerWord;
7808
+ clsId;
7809
+ sepId;
7810
+ padId;
7811
+ unkId;
7812
+ /** @param raw the parsed contents of a tokenizer.json. */
7813
+ constructor(raw) {
7814
+ const spec = readSpec(raw);
7815
+ this.vocab = spec.vocab;
7816
+ this.maxInputCharsPerWord = spec.maxInputCharsPerWord;
7817
+ this.clsId = this.requireToken("[CLS]");
7818
+ this.sepId = this.requireToken("[SEP]");
7819
+ this.padId = this.requireToken("[PAD]");
7820
+ this.unkId = this.requireToken(spec.unkToken);
7821
+ }
7822
+ /** Parse and validate in one step, for the common case of reading the file off disk. */
7823
+ static fromJson(json) {
7824
+ let parsed;
7825
+ try {
7826
+ parsed = JSON.parse(json);
7827
+ } catch (err) {
7828
+ throw new UnsupportedTokenizerError(`tokenizer.json is not valid JSON: ${err.message}`);
7829
+ }
7830
+ return new _BertWordPiece(parsed);
7831
+ }
7832
+ requireToken(token) {
7833
+ const id = this.vocab.get(token);
7834
+ if (id === void 0) fail(`model.vocab is missing the ${token} token`);
7835
+ return id;
7836
+ }
7837
+ /** Greedy longest-match-first over one whitespace- and punctuation-free word. */
7838
+ wordToIds(word, into) {
7839
+ if (word.length > this.maxInputCharsPerWord) {
7840
+ into.push(this.unkId);
7841
+ return;
7842
+ }
7843
+ const pieces = [];
7844
+ let start = 0;
7845
+ while (start < word.length) {
7846
+ let end = word.length;
7847
+ let found = -1;
7848
+ while (start < end) {
7849
+ const sub = start === 0 ? word.slice(start, end) : `##${word.slice(start, end)}`;
7850
+ const id = this.vocab.get(sub);
7851
+ if (id !== void 0) {
7852
+ found = id;
7853
+ break;
7854
+ }
7855
+ end--;
7856
+ }
7857
+ if (found === -1) {
7858
+ into.push(this.unkId);
7859
+ return;
7860
+ }
7861
+ pieces.push(found);
7862
+ start = end;
7863
+ }
7864
+ for (const id of pieces) into.push(id);
7865
+ }
7866
+ /**
7867
+ * `[CLS] ... [SEP]`, cut to `maxLength` tokens.
7868
+ *
7869
+ * The cut is taken after the markers are added, not before, which means a sequence long enough to
7870
+ * be truncated ends on an ordinary token and has no [SEP] at all. That is what the reference does
7871
+ * and therefore what the model has been fed all along, so it is deliberate rather than an
7872
+ * oversight: keeping the [SEP] would be the more defensible sequence and a different one, and a
7873
+ * tokenizer whose whole justification is producing identical ids does not get to improve on them.
7874
+ * The oracle carries two cases that reach the limit; both end mid-text.
7875
+ */
7876
+ encode(text, maxLength = MAX_SEQUENCE_TOKENS) {
7877
+ if (!Number.isInteger(maxLength) || maxLength < 2) {
7878
+ throw new RangeError(`maxLength must be an integer >= 2 (both markers), got ${maxLength}`);
7879
+ }
7880
+ const ids = [this.clsId];
7881
+ for (const word of preTokenize(normalize(text))) {
7882
+ this.wordToIds(word, ids);
7883
+ if (ids.length >= maxLength) return ids.slice(0, maxLength);
7884
+ }
7885
+ ids.push(this.sepId);
7886
+ return ids.length > maxLength ? ids.slice(0, maxLength) : ids;
7887
+ }
7888
+ };
7889
+
7890
+ // src/embed_model.ts
7891
+ var _require2 = createRequire3(import.meta.url);
7283
7892
  var DEFAULT_MODEL = "Xenova/bge-small-en-v1.5";
7284
7893
  var DEFAULT_DIM = 384;
7285
7894
  var PINNED_MODEL_REVISION = "ea104dacec62c0de699686887e3f920caeb4f3e3";
7895
+ var MODEL_FILES = [
7896
+ {
7897
+ name: "tokenizer.json",
7898
+ sha256: "d241a60d5e8f04cc1b2b3e9ef7a4921b27bf526d9f6050ab90f9267a1f9e5c66",
7899
+ bytes: 711396
7900
+ },
7901
+ {
7902
+ name: "onnx/model_quantized.onnx",
7903
+ sha256: "6c9c6101a956d62dfb5e7190c538226c0c5bb9cb27b651234b6df063ee7dbfe4",
7904
+ bytes: 34014426
7905
+ }
7906
+ ];
7907
+ function modelDir() {
7908
+ return path13.join(dataDir(), "models", ...DEFAULT_MODEL.split("/"), PINNED_MODEL_REVISION);
7909
+ }
7910
+ function downloadUrl(file) {
7911
+ return `https://huggingface.co/${DEFAULT_MODEL}/resolve/${PINNED_MODEL_REVISION}/${file.name}`;
7912
+ }
7913
+ var _ort = null;
7914
+ var _ortError = null;
7915
+ var _ortLoadAttempted = false;
7916
+ function ensureRuntimeLoaded() {
7917
+ if (_ortLoadAttempted) return;
7918
+ _ortLoadAttempted = true;
7919
+ try {
7920
+ _ort = _require2("onnxruntime-node");
7921
+ } catch (e) {
7922
+ _ortError = e instanceof Error ? e : new Error(String(e));
7923
+ }
7924
+ }
7925
+ function isRuntimeAvailable() {
7926
+ ensureRuntimeLoaded();
7927
+ return _ort !== null && _ortError === null;
7928
+ }
7929
+ function runtimeLoadError() {
7930
+ ensureRuntimeLoaded();
7931
+ return _ortError;
7932
+ }
7933
+ function runtimeVersion() {
7934
+ ensureRuntimeLoaded();
7935
+ if (_ort === null) return "unknown";
7936
+ try {
7937
+ let dir = path13.dirname(_require2.resolve("onnxruntime-node"));
7938
+ for (let depth = 0; depth < 6; depth++) {
7939
+ const manifest = path13.join(dir, "package.json");
7940
+ if (fs17.existsSync(manifest)) {
7941
+ const parsed = JSON.parse(fs17.readFileSync(manifest, "utf8"));
7942
+ if (parsed.name === "onnxruntime-node" && typeof parsed.version === "string") return parsed.version;
7943
+ }
7944
+ const up = path13.dirname(dir);
7945
+ if (up === dir) break;
7946
+ dir = up;
7947
+ }
7948
+ } catch {
7949
+ }
7950
+ return "unknown";
7951
+ }
7952
+ function sha256Of(filePath) {
7953
+ return new Promise((resolve10, reject) => {
7954
+ const hash2 = createHash4("sha256");
7955
+ const stream = fs17.createReadStream(filePath);
7956
+ stream.on("error", reject);
7957
+ stream.on("data", (chunk) => hash2.update(chunk));
7958
+ stream.on("end", () => resolve10(hash2.digest("hex")));
7959
+ });
7960
+ }
7961
+ async function download(file, target) {
7962
+ const url = downloadUrl(file);
7963
+ const response = await fetch(url, { redirect: "follow" });
7964
+ if (!response.ok) throw new Error(`GET ${url} returned ${response.status} ${response.statusText}`);
7965
+ if (!response.body) throw new Error(`GET ${url} returned no body`);
7966
+ const temp = `${target}.${process.pid}.partial`;
7967
+ const hash2 = createHash4("sha256");
7968
+ let written = 0;
7969
+ const out = fs17.createWriteStream(temp);
7970
+ try {
7971
+ await pipeline(async function* () {
7972
+ for await (const chunk of response.body) {
7973
+ written += chunk.byteLength;
7974
+ if (written > file.bytes) throw new Error(`${file.name} is longer than the pinned ${file.bytes} bytes`);
7975
+ hash2.update(chunk);
7976
+ yield chunk;
7977
+ }
7978
+ }, out);
7979
+ if (written !== file.bytes) {
7980
+ throw new Error(`${file.name} is ${written} bytes, expected the pinned ${file.bytes}`);
7981
+ }
7982
+ const digest = hash2.digest("hex");
7983
+ if (digest !== file.sha256) {
7984
+ throw new Error(`${file.name} has sha256 ${digest}, expected the pinned ${file.sha256}`);
7985
+ }
7986
+ fs17.renameSync(temp, target);
7987
+ } catch (e) {
7988
+ await new Promise((resolve10) => {
7989
+ if (out.closed) resolve10();
7990
+ else out.once("close", () => resolve10());
7991
+ });
7992
+ try {
7993
+ fs17.rmSync(temp, { force: true, maxRetries: 20, retryDelay: 25 });
7994
+ } catch {
7995
+ }
7996
+ throw e;
7997
+ }
7998
+ }
7999
+ async function ensureModelFiles(modelName = DEFAULT_MODEL) {
8000
+ if (modelName !== DEFAULT_MODEL) {
8001
+ throw new Error(
8002
+ `Only ${DEFAULT_MODEL} is supported: its files are pinned to a revision and to a sha256 each, and "${modelName}" has neither, so there would be nothing to check the download against.`
8003
+ );
8004
+ }
8005
+ ensureDataDirPrivate();
8006
+ const dir = modelDir();
8007
+ const offline = loadConfig().network.offline;
8008
+ for (const file of MODEL_FILES) {
8009
+ const target = path13.join(dir, file.name);
8010
+ if (fs17.existsSync(target)) {
8011
+ const digest = await sha256Of(target);
8012
+ if (digest === file.sha256) continue;
8013
+ fs17.rmSync(target, { force: true });
8014
+ }
8015
+ if (offline) {
8016
+ throw new Error(
8017
+ `Offline mode is on (network.offline): refusing to download ${file.name} for the embedding model. Copy the pinned files into ${dir} on a connected machine to use semantic search here.`
8018
+ );
8019
+ }
8020
+ fs17.mkdirSync(path13.dirname(target), { recursive: true });
8021
+ console.warn(
8022
+ `Downloading the embedding model, once (${file.name}, ${Math.round(file.bytes / 1024 / 1024)} MB) into ${dir}`
8023
+ );
8024
+ await download(file, target);
8025
+ }
8026
+ return dir;
8027
+ }
8028
+ function poolAndNormalize(hidden, seq, dim) {
8029
+ const pooled = new Float64Array(dim);
8030
+ for (let t = 0; t < seq; t++) {
8031
+ const base = t * dim;
8032
+ for (let d = 0; d < dim; d++) pooled[d] = (pooled[d] ?? 0) + (hidden[base + d] ?? 0);
8033
+ }
8034
+ let sumOfSquares = 0;
8035
+ for (let d = 0; d < dim; d++) {
8036
+ const mean = (pooled[d] ?? 0) / seq;
8037
+ pooled[d] = mean;
8038
+ sumOfSquares += mean * mean;
8039
+ }
8040
+ const norm = Math.sqrt(sumOfSquares);
8041
+ const out = new Float32Array(dim);
8042
+ if (norm === 0 || !Number.isFinite(norm)) return out;
8043
+ for (let d = 0; d < dim; d++) out[d] = (pooled[d] ?? 0) / norm;
8044
+ return out;
8045
+ }
8046
+ var EmbeddingModel = class _EmbeddingModel {
8047
+ constructor(tokenizer, session, tensorFactory) {
8048
+ this.tokenizer = tokenizer;
8049
+ this.session = session;
8050
+ this.tensorFactory = tensorFactory;
8051
+ }
8052
+ tokenizer;
8053
+ session;
8054
+ tensorFactory;
8055
+ static async load(modelName = DEFAULT_MODEL) {
8056
+ if (!isRuntimeAvailable()) {
8057
+ throw new Error(`onnxruntime-node is not available: ${_ortError?.message ?? "unknown error"}`);
8058
+ }
8059
+ const dir = await ensureModelFiles(modelName);
8060
+ const tokenizer = BertWordPiece.fromJson(fs17.readFileSync(path13.join(dir, "tokenizer.json"), "utf8"));
8061
+ const ort = _ort;
8062
+ const session = await ort.InferenceSession.create(path13.join(dir, "onnx", "model_quantized.onnx"));
8063
+ return new _EmbeddingModel(tokenizer, session, ort.Tensor);
8064
+ }
8065
+ /** Embed one text. Sequences are run singly, so there is no padding and no mask to get wrong. */
8066
+ async embed(text) {
8067
+ const ids = this.tokenizer.encode(text);
8068
+ const length = ids.length;
8069
+ const feeds = {
8070
+ input_ids: new this.tensorFactory("int64", BigInt64Array.from(ids, BigInt), [1, length]),
8071
+ attention_mask: new this.tensorFactory("int64", new BigInt64Array(length).fill(1n), [1, length])
8072
+ };
8073
+ if (this.session.inputNames.includes("token_type_ids")) {
8074
+ feeds["token_type_ids"] = new this.tensorFactory("int64", new BigInt64Array(length), [1, length]);
8075
+ }
8076
+ const outputName = this.session.outputNames[0];
8077
+ if (outputName === void 0) throw new Error("the model declares no outputs");
8078
+ const output = (await this.session.run(feeds))[outputName];
8079
+ if (!output) throw new Error(`the model produced no ${outputName}`);
8080
+ const [, seq, dim] = output.dims;
8081
+ if (seq === void 0 || dim === void 0) {
8082
+ throw new Error(`expected a [batch, sequence, dimension] output, got [${output.dims.join(", ")}]`);
8083
+ }
8084
+ if (dim !== DEFAULT_DIM) {
8085
+ throw new Error(`the model produced ${dim}-dimension vectors, expected ${DEFAULT_DIM}`);
8086
+ }
8087
+ return poolAndNormalize(output.data, seq, dim);
8088
+ }
8089
+ };
8090
+ registerReset(() => {
8091
+ _ort = null;
8092
+ _ortError = null;
8093
+ _ortLoadAttempted = false;
8094
+ });
8095
+
8096
+ // src/embeddings.ts
7286
8097
  var QUERY_INSTRUCTION_PREFIX = "Represent this sentence for searching relevant passages: ";
7287
8098
  var _extractorCache = /* @__PURE__ */ new Map();
8099
+ var inHousePipelineFn = async (_task, modelName) => {
8100
+ const model = await EmbeddingModel.load(modelName);
8101
+ return async (text) => ({ data: await model.embed(text) });
8102
+ };
7288
8103
  var _pipelineFnOverride = null;
7289
8104
  registerReset(() => {
7290
8105
  _extractorCache.clear();
@@ -7303,8 +8118,7 @@ async function buildExtractorWithRetry(pipelineFn, modelName) {
7303
8118
  let lastError;
7304
8119
  for (let attempt = 1; attempt <= PIPELINE_RETRY_ATTEMPTS; attempt++) {
7305
8120
  try {
7306
- const pipelineOptions = modelName === DEFAULT_MODEL ? { revision: PINNED_MODEL_REVISION } : void 0;
7307
- return await pipelineFn("feature-extraction", modelName, pipelineOptions);
8121
+ return await pipelineFn("feature-extraction", modelName);
7308
8122
  } catch (e) {
7309
8123
  lastError = e;
7310
8124
  if (attempt < PIPELINE_RETRY_ATTEMPTS) await sleep(PIPELINE_RETRY_DELAY_MS * attempt);
@@ -7356,25 +8170,23 @@ var _MIN_TOKEN_LEN = 3;
7356
8170
  var OVER_FETCH_FACTOR = 4;
7357
8171
  var MAX_OVER_FETCH = 100;
7358
8172
  function isAvailable() {
7359
- ensureTransformerLoaded();
7360
- return _transformer !== null && _transformerError === null;
8173
+ return isRuntimeAvailable();
8174
+ }
8175
+ function embeddingBackendLoadError() {
8176
+ return runtimeLoadError();
7361
8177
  }
7362
8178
  async function embedTexts(texts, modelName = DEFAULT_MODEL) {
7363
8179
  if (!isAvailable()) {
7364
8180
  throw new Error(
7365
- `Transformer not available: ${_transformerError?.message ?? "unknown error"}`
8181
+ `Embedding backend not available: ${runtimeLoadError()?.message ?? "unknown error"}`
7366
8182
  );
7367
8183
  }
7368
8184
  if (texts.length === 0) {
7369
8185
  return [];
7370
8186
  }
7371
- if (!_transformer || typeof _transformer !== "object") {
7372
- throw new Error("Transformer module is unavailable");
7373
- }
7374
8187
  let extractorPromise = _extractorCache.get(modelName);
7375
8188
  if (!extractorPromise) {
7376
- const transformerObj = _transformer;
7377
- const pipelineFn = _pipelineFnOverride ?? transformerObj["pipeline"];
8189
+ const pipelineFn = _pipelineFnOverride ?? inHousePipelineFn;
7378
8190
  extractorPromise = buildExtractorWithRetry(pipelineFn, modelName);
7379
8191
  _extractorCache.set(modelName, extractorPromise);
7380
8192
  extractorPromise.catch(() => {
@@ -7586,6 +8398,7 @@ async function upsertChunks(db, chunks) {
7586
8398
  deleteFileEmbeddings(db, filePath);
7587
8399
  return "unavailable";
7588
8400
  }
8401
+ ensureEmbeddingProvenance(db);
7589
8402
  const texts = chunks.map((c) => c.text);
7590
8403
  const embeddings = await embedTexts(texts);
7591
8404
  const chunkInsertStmt = db.prepare(`
@@ -7656,7 +8469,6 @@ function fetchScopedHits(db, queryVec, k, maxDistance, rootDir) {
7656
8469
  }
7657
8470
  async function searchSemantic(db, query, topK = 8, modelName = DEFAULT_MODEL, maxDistance = DEFAULT_DISTANCE_THRESHOLD, rootDir) {
7658
8471
  if (!isAvailable()) {
7659
- console.warn("Embeddings not available; semantic search disabled");
7660
8472
  return [];
7661
8473
  }
7662
8474
  if (query.trim().length === 0) {
@@ -7665,6 +8477,7 @@ async function searchSemantic(db, query, topK = 8, modelName = DEFAULT_MODEL, ma
7665
8477
  if (!chunkVectorsTableExists(db)) {
7666
8478
  return [];
7667
8479
  }
8480
+ ensureEmbeddingProvenance(db, modelName);
7668
8481
  const queryEmbeddings = await embedTexts([`${QUERY_INSTRUCTION_PREFIX}${query}`], modelName);
7669
8482
  if (queryEmbeddings.length === 0) {
7670
8483
  return [];
@@ -7813,6 +8626,44 @@ function deleteFileEmbeddings(db, filePath) {
7813
8626
  }
7814
8627
  db.prepare(`DELETE FROM chunks WHERE ${pathEqClause("file_path")}`).run(folded);
7815
8628
  }
8629
+ function resetAllEmbeddings(db) {
8630
+ const paths = db.prepare("SELECT DISTINCT file_path FROM chunks").pluck().all();
8631
+ const clearEmbedSha = db.prepare(`UPDATE files SET embed_sha = NULL WHERE ${pathEqClause("path")}`);
8632
+ const tx = db.transaction(() => {
8633
+ for (const p of paths) deleteFileEmbeddings(db, p);
8634
+ for (const p of paths) clearEmbedSha.run(foldPath(p));
8635
+ });
8636
+ tx.immediate();
8637
+ return paths.length;
8638
+ }
8639
+ function embeddingProvenance(modelName = DEFAULT_MODEL) {
8640
+ const revision = modelName === DEFAULT_MODEL ? PINNED_MODEL_REVISION.slice(0, 12) : "unpinned";
8641
+ return `${modelName}@${revision}/${backendId()}`;
8642
+ }
8643
+ function backendId() {
8644
+ return `onnxruntime-node@${majorMinor(runtimeVersion())}`;
8645
+ }
8646
+ function majorMinor(version) {
8647
+ const parts = version.split(".");
8648
+ return parts.length >= 2 ? `${parts[0]}.${parts[1]}` : version;
8649
+ }
8650
+ var _provenanceChecked = /* @__PURE__ */ new WeakSet();
8651
+ function ensureEmbeddingProvenance(db, modelName = DEFAULT_MODEL) {
8652
+ if (_provenanceChecked.has(db)) return;
8653
+ _provenanceChecked.add(db);
8654
+ const current = embeddingProvenance(modelName);
8655
+ const stored = db.prepare("SELECT provenance FROM embedding_provenance WHERE id = 1").pluck().get();
8656
+ if (stored === current) return;
8657
+ const cleared = resetAllEmbeddings(db);
8658
+ db.prepare(
8659
+ "INSERT INTO embedding_provenance (id, provenance) VALUES (1, ?) ON CONFLICT(id) DO UPDATE SET provenance = excluded.provenance"
8660
+ ).run(current);
8661
+ if (cleared > 0) {
8662
+ console.warn(
8663
+ `Embedding stack changed (${stored ?? "unrecorded"} -> ${current}); discarded ${cleared} file${cleared === 1 ? "" : "s"} worth of vectors because old and new ones are not comparable. Run \`token-goat index\` to rebuild them.`
8664
+ );
8665
+ }
8666
+ }
7816
8667
  function _extractQueryTokens(query) {
7817
8668
  const tokens = /* @__PURE__ */ new Set();
7818
8669
  const matches = query.matchAll(_TOKEN_RE);
@@ -7849,17 +8700,19 @@ function _pathPriorityPenalty(filePath) {
7849
8700
  }
7850
8701
 
7851
8702
  // src/hooks_read.ts
7852
- import * as fs22 from "node:fs";
7853
- import * as path18 from "node:path";
8703
+ import * as fs23 from "node:fs";
8704
+ import * as path19 from "node:path";
7854
8705
 
7855
8706
  // src/compact.ts
7856
- import * as fs17 from "node:fs";
7857
- import * as path13 from "node:path";
8707
+ import * as fs18 from "node:fs";
8708
+ import * as path14 from "node:path";
7858
8709
 
7859
8710
  // src/overflow_guard.ts
8711
+ function estimateTokensFromLength(length) {
8712
+ return Math.max(1, Math.floor(Math.max(0, length) / 3) + 1);
8713
+ }
7860
8714
  function estimateTokens(text) {
7861
- const stripped = stripAnsiCodes(text);
7862
- return Math.max(1, Math.floor(stripped.length / 3) + 1);
8715
+ return estimateTokensFromLength(stripAnsiCodes(text).length);
7863
8716
  }
7864
8717
  function trimToBudget(text, budgetTokens, command) {
7865
8718
  const markerMarginTokens = 64;
@@ -8116,7 +8969,7 @@ function inferSessionGoal(cache, maxTokens = 80) {
8116
8969
  const dirCounts = new Counter();
8117
8970
  for (const fpath of editedPaths) {
8118
8971
  try {
8119
- let parent = path13.dirname(fpath);
8972
+ let parent = path14.dirname(fpath);
8120
8973
  if (parent === ".") {
8121
8974
  parent = "root";
8122
8975
  } else if (parent.startsWith("./")) {
@@ -8192,11 +9045,11 @@ function isNoisePath(inputPath) {
8192
9045
  }
8193
9046
  function findLatestSessionId() {
8194
9047
  try {
8195
- const sessionsDir = path13.join(tokenGoatHome(), "sessions");
8196
- if (!fs17.existsSync(sessionsDir)) {
9048
+ const sessionsDir = path14.join(tokenGoatHome(), "sessions");
9049
+ if (!fs18.existsSync(sessionsDir)) {
8197
9050
  return null;
8198
9051
  }
8199
- const files = fs17.readdirSync(sessionsDir);
9052
+ const files = fs18.readdirSync(sessionsDir);
8200
9053
  const jsonFiles = files.filter((f) => f.endsWith(".json") && !f.includes(AGENT_SALT_MARKER));
8201
9054
  if (jsonFiles.length === 0) {
8202
9055
  return null;
@@ -8206,9 +9059,9 @@ function findLatestSessionId() {
8206
9059
  return null;
8207
9060
  }
8208
9061
  let latestFile = firstFile;
8209
- let latestMtime = fs17.statSync(path13.join(sessionsDir, firstFile)).mtimeMs;
9062
+ let latestMtime = fs18.statSync(path14.join(sessionsDir, firstFile)).mtimeMs;
8210
9063
  for (const file of jsonFiles) {
8211
- const mtime = fs17.statSync(path13.join(sessionsDir, file)).mtimeMs;
9064
+ const mtime = fs18.statSync(path14.join(sessionsDir, file)).mtimeMs;
8212
9065
  if (mtime > latestMtime) {
8213
9066
  latestFile = file;
8214
9067
  latestMtime = mtime;
@@ -8230,37 +9083,37 @@ function eventCount(cache) {
8230
9083
  function writeSessionManifest(projectHash, sessionId, manifestJson) {
8231
9084
  const safeSessionId = sanitizeIdForFilename(sessionId, 64);
8232
9085
  if (!safeSessionId) return;
8233
- const sessionsDir = path13.join(dataDir(), "projects", projectHash, "sessions");
8234
- if (!fs17.existsSync(sessionsDir)) {
9086
+ const sessionsDir = path14.join(dataDir(), "projects", projectHash, "sessions");
9087
+ if (!fs18.existsSync(sessionsDir)) {
8235
9088
  ensureDirSync(sessionsDir);
8236
9089
  }
8237
- const dest = path13.join(sessionsDir, `${safeSessionId}.json`);
9090
+ const dest = path14.join(sessionsDir, `${safeSessionId}.json`);
8238
9091
  atomicWriteText(dest, JSON.stringify(manifestJson));
8239
9092
  }
8240
9093
  function readAllSessionManifests(projectHash, maxAgeSecs = 3600) {
8241
- const sessionsDir = path13.join(dataDir(), "projects", projectHash, "sessions");
8242
- if (!fs17.existsSync(sessionsDir)) {
9094
+ const sessionsDir = path14.join(dataDir(), "projects", projectHash, "sessions");
9095
+ if (!fs18.existsSync(sessionsDir)) {
8243
9096
  return [];
8244
9097
  }
8245
9098
  const now = Date.now() / 1e3;
8246
9099
  const results = [];
8247
9100
  try {
8248
- const files = fs17.readdirSync(sessionsDir);
9101
+ const files = fs18.readdirSync(sessionsDir);
8249
9102
  for (const file of files) {
8250
9103
  if (!file.endsWith(".json")) {
8251
9104
  continue;
8252
9105
  }
8253
9106
  try {
8254
- const fullPath = path13.join(sessionsDir, file);
8255
- const stat2 = fs17.statSync(fullPath);
9107
+ const fullPath = path14.join(sessionsDir, file);
9108
+ const stat2 = fs18.statSync(fullPath);
8256
9109
  if (now - stat2.mtimeMs / 1e3 > maxAgeSecs) {
8257
9110
  try {
8258
- fs17.unlinkSync(fullPath);
9111
+ fs18.unlinkSync(fullPath);
8259
9112
  } catch {
8260
9113
  }
8261
9114
  continue;
8262
9115
  }
8263
- const text = fs17.readFileSync(fullPath, "utf8");
9116
+ const text = fs18.readFileSync(fullPath, "utf8");
8264
9117
  const data = JSON.parse(text);
8265
9118
  if (typeof data === "object" && data !== null && "files" in data) {
8266
9119
  results.push(data);
@@ -8431,8 +9284,8 @@ function buildManifestWithCount(sessionId, opts) {
8431
9284
  }
8432
9285
 
8433
9286
  // src/snapshots.ts
8434
- import * as fs18 from "node:fs";
8435
- import * as path14 from "node:path";
9287
+ import * as fs19 from "node:fs";
9288
+ import * as path15 from "node:path";
8436
9289
  var MAX_SNAPSHOTS_PER_SESSION = 150;
8437
9290
  var MAX_SNAPSHOT_BYTES = 256 * 1024;
8438
9291
  var SNAPSHOT_TRUNCATE_BYTES = 50 * 1024;
@@ -8442,10 +9295,10 @@ var VALID_KINDS = /* @__PURE__ */ new Set([KIND_READ, KIND_PREDICTIVE]);
8442
9295
  function sessionDir(sessionId) {
8443
9296
  if (!sessionId) return null;
8444
9297
  const safe = sanitizeIdForFilename(sessionId, 64, "anon");
8445
- const base = path14.join(tokenGoatHome(), "session_snapshots");
8446
- const candidate = path14.join(base, safe);
9298
+ const base = path15.join(tokenGoatHome(), "session_snapshots");
9299
+ const candidate = path15.join(base, safe);
8447
9300
  try {
8448
- const rel = path14.relative(base, candidate);
9301
+ const rel = path15.relative(base, candidate);
8449
9302
  if (rel.startsWith("..")) return null;
8450
9303
  } catch {
8451
9304
  return null;
@@ -8458,7 +9311,7 @@ function pathKey(filePath) {
8458
9311
  function snapshot_path(sessionId, filePath) {
8459
9312
  const d = sessionDir(sessionId);
8460
9313
  if (!d) return null;
8461
- return path14.join(d, `${pathKey(filePath)}.bin`);
9314
+ return path15.join(d, `${pathKey(filePath)}.bin`);
8462
9315
  }
8463
9316
  function kindSidecarPath(snapshotPath) {
8464
9317
  return snapshotPath + ".kind";
@@ -8466,11 +9319,11 @@ function kindSidecarPath(snapshotPath) {
8466
9319
  function writeSnapshotKind(sidecarPath, kind) {
8467
9320
  try {
8468
9321
  const safeKind = VALID_KINDS.has(kind) ? kind : KIND_READ;
8469
- const dir = path14.dirname(sidecarPath);
8470
- if (!fs18.existsSync(dir)) {
9322
+ const dir = path15.dirname(sidecarPath);
9323
+ if (!fs19.existsSync(dir)) {
8471
9324
  ensureDirSync(dir);
8472
9325
  }
8473
- fs18.writeFileSync(sidecarPath, safeKind, "utf8");
9326
+ fs19.writeFileSync(sidecarPath, safeKind, "utf8");
8474
9327
  return true;
8475
9328
  } catch {
8476
9329
  return false;
@@ -8479,12 +9332,12 @@ function writeSnapshotKind(sidecarPath, kind) {
8479
9332
  function evictOldest(d, maxCount) {
8480
9333
  try {
8481
9334
  const entries = [];
8482
- const files = fs18.readdirSync(d);
9335
+ const files = fs19.readdirSync(d);
8483
9336
  for (const file of files) {
8484
- const fullPath = path14.join(d, file);
9337
+ const fullPath = path15.join(d, file);
8485
9338
  if (!file.endsWith(".bin")) continue;
8486
9339
  try {
8487
- const stat2 = fs18.statSync(fullPath);
9340
+ const stat2 = fs19.statSync(fullPath);
8488
9341
  entries.push([fullPath, stat2.mtimeMs]);
8489
9342
  } catch {
8490
9343
  continue;
@@ -8496,10 +9349,10 @@ function evictOldest(d, maxCount) {
8496
9349
  const over = entries.length - maxCount;
8497
9350
  for (const [p] of entries.slice(0, over)) {
8498
9351
  try {
8499
- fs18.unlinkSync(p);
9352
+ fs19.unlinkSync(p);
8500
9353
  removed++;
8501
9354
  try {
8502
- fs18.unlinkSync(kindSidecarPath(p));
9355
+ fs19.unlinkSync(kindSidecarPath(p));
8503
9356
  } catch {
8504
9357
  }
8505
9358
  } catch {
@@ -8528,10 +9381,10 @@ function store(sessionId, filePath, content, opts = {}) {
8528
9381
  if (!p) return null;
8529
9382
  const sha = fingerprintContent(stored);
8530
9383
  try {
8531
- const isNewEntry = !fs18.existsSync(p);
9384
+ const isNewEntry = !fs19.existsSync(p);
8532
9385
  if (!isNewEntry) {
8533
9386
  try {
8534
- const existing = fs18.readFileSync(p);
9387
+ const existing = fs19.readFileSync(p);
8535
9388
  if (Buffer.from(existing).equals(stored)) {
8536
9389
  return {
8537
9390
  path: p,
@@ -8542,8 +9395,8 @@ function store(sessionId, filePath, content, opts = {}) {
8542
9395
  } catch {
8543
9396
  }
8544
9397
  }
8545
- const dir = path14.dirname(p);
8546
- if (!fs18.existsSync(dir)) {
9398
+ const dir = path15.dirname(p);
9399
+ if (!fs19.existsSync(dir)) {
8547
9400
  ensureDirSync(dir);
8548
9401
  }
8549
9402
  if (isNewEntry) {
@@ -8563,9 +9416,9 @@ function store(sessionId, filePath, content, opts = {}) {
8563
9416
  }
8564
9417
  function load(sessionId, filePath, opts = {}) {
8565
9418
  const p = snapshot_path(sessionId, filePath);
8566
- if (!p || !fs18.existsSync(p)) return null;
9419
+ if (!p || !fs19.existsSync(p)) return null;
8567
9420
  try {
8568
- const stat2 = fs18.statSync(p);
9421
+ const stat2 = fs19.statSync(p);
8569
9422
  if (stat2.size > MAX_SNAPSHOT_BYTES) {
8570
9423
  return null;
8571
9424
  }
@@ -8573,7 +9426,7 @@ function load(sessionId, filePath, opts = {}) {
8573
9426
  return null;
8574
9427
  }
8575
9428
  try {
8576
- const data = fs18.readFileSync(p);
9429
+ const data = fs19.readFileSync(p);
8577
9430
  if (opts.expected_sha) {
8578
9431
  const actualSha = fingerprintContent(data);
8579
9432
  if (actualSha.toLowerCase() !== opts.expected_sha.toLowerCase()) {
@@ -8586,31 +9439,31 @@ function load(sessionId, filePath, opts = {}) {
8586
9439
  }
8587
9440
  }
8588
9441
  function removeEligibleSnapshotFile(fullPath, file, cutoff) {
8589
- const stat2 = fs18.lstatSync(fullPath);
9442
+ const stat2 = fs19.lstatSync(fullPath);
8590
9443
  if ((stat2.mode & 61440) === 40960) return false;
8591
9444
  if (cutoff !== void 0 && stat2.mtimeMs >= cutoff) return false;
8592
- fs18.unlinkSync(fullPath);
9445
+ fs19.unlinkSync(fullPath);
8593
9446
  return file.endsWith(".bin");
8594
9447
  }
8595
9448
  function cleanup_stale(maxAgeHours = 24) {
8596
- const base = path14.join(tokenGoatHome(), "session_snapshots");
8597
- if (!fs18.existsSync(base)) return 0;
9449
+ const base = path15.join(tokenGoatHome(), "session_snapshots");
9450
+ if (!fs19.existsSync(base)) return 0;
8598
9451
  const cutoff = Date.now() - maxAgeHours * 3600 * 1e3;
8599
9452
  let removed = 0;
8600
9453
  try {
8601
- const sessionDirs = fs18.readdirSync(base);
9454
+ const sessionDirs = fs19.readdirSync(base);
8602
9455
  for (const sessionDir2 of sessionDirs) {
8603
- const sessionPath2 = path14.join(base, sessionDir2);
9456
+ const sessionPath2 = path15.join(base, sessionDir2);
8604
9457
  try {
8605
- const stat2 = fs18.statSync(sessionPath2);
9458
+ const stat2 = fs19.statSync(sessionPath2);
8606
9459
  if (!stat2.isDirectory()) continue;
8607
9460
  } catch {
8608
9461
  continue;
8609
9462
  }
8610
9463
  try {
8611
- const files = fs18.readdirSync(sessionPath2);
9464
+ const files = fs19.readdirSync(sessionPath2);
8612
9465
  for (const file of files) {
8613
- const fullPath = path14.join(sessionPath2, file);
9466
+ const fullPath = path15.join(sessionPath2, file);
8614
9467
  try {
8615
9468
  if (removeEligibleSnapshotFile(fullPath, file, cutoff)) removed++;
8616
9469
  } catch {
@@ -8618,7 +9471,7 @@ function cleanup_stale(maxAgeHours = 24) {
8618
9471
  }
8619
9472
  }
8620
9473
  try {
8621
- fs18.rmdirSync(sessionPath2);
9474
+ fs19.rmdirSync(sessionPath2);
8622
9475
  } catch {
8623
9476
  }
8624
9477
  } catch {
@@ -8654,11 +9507,11 @@ function buildPackageManifestHint(options) {
8654
9507
  return null;
8655
9508
  }
8656
9509
  }
8657
- function _sanitizeHintPath(path30) {
8658
- if (typeof path30 !== "string") {
9510
+ function _sanitizeHintPath(path31) {
9511
+ if (typeof path31 !== "string") {
8659
9512
  return "???";
8660
9513
  }
8661
- return path30.replace(/[\x00]/g, "").slice(0, 200);
9514
+ return path31.replace(/[\x00]/g, "").slice(0, 200);
8662
9515
  }
8663
9516
 
8664
9517
  // src/hints/lang_patterns.ts
@@ -9286,33 +10139,33 @@ function dispatchFileTypeHandler(filePath, content, contentLengthHint) {
9286
10139
  }
9287
10140
 
9288
10141
  // src/doc_compact.ts
9289
- import * as fs19 from "fs";
9290
- import * as path15 from "path";
10142
+ import * as fs20 from "fs";
10143
+ import * as path16 from "path";
9291
10144
  var defaultSentencesPerSection = 2;
9292
10145
  var headerPrefix = "<!-- token-goat doc-compact source-hash:";
9293
10146
  var headerRegex = /^<!-- token-goat doc-compact source-hash:(\S+) source:(.+?) -->\r?$/;
9294
10147
  var compactSubdir = "doc_compacts";
9295
10148
  function sourceHash(filePath) {
9296
10149
  try {
9297
- return fingerprintContent(fs19.readFileSync(filePath));
10150
+ return fingerprintContent(fs20.readFileSync(filePath));
9298
10151
  } catch {
9299
10152
  return "";
9300
10153
  }
9301
10154
  }
9302
10155
  function _compactSlug(absPathStr) {
9303
10156
  const h = fingerprintContent(foldPath(absPathStr)).slice(0, 12);
9304
- const ext = path15.extname(absPathStr);
9305
- const stem = path15.basename(absPathStr, ext);
10157
+ const ext = path16.extname(absPathStr);
10158
+ const stem = path16.basename(absPathStr, ext);
9306
10159
  const safeStem = sanitizeIdForFilename(stem, 32);
9307
10160
  return `${h}_${safeStem}`;
9308
10161
  }
9309
10162
  function compactPathFor(sourcePath) {
9310
10163
  const abs = resolveIndexPath(sourcePath);
9311
- return path15.join(dataDir(), compactSubdir, `${_compactSlug(abs)}.md`);
10164
+ return path16.join(dataDir(), compactSubdir, `${_compactSlug(abs)}.md`);
9312
10165
  }
9313
10166
  function readCompactHeader(compactPath) {
9314
10167
  try {
9315
- const text = fs19.readFileSync(compactPath, "utf-8");
10168
+ const text = fs20.readFileSync(compactPath, "utf-8");
9316
10169
  const firstLine = text.split("\n")[0] || "";
9317
10170
  const m = firstLine.match(headerRegex);
9318
10171
  if (!m || !m[1] || !m[2]) return null;
@@ -9331,8 +10184,8 @@ function isCompactFresh(compactPath, sourcePath) {
9331
10184
  }
9332
10185
  function markCompactStale(compactPath) {
9333
10186
  try {
9334
- if (!fs19.existsSync(compactPath)) return false;
9335
- const text = fs19.readFileSync(compactPath, "utf-8");
10187
+ if (!fs20.existsSync(compactPath)) return false;
10188
+ const text = fs20.readFileSync(compactPath, "utf-8");
9336
10189
  const lines2 = text.split("\n");
9337
10190
  if (!lines2[0]) return false;
9338
10191
  const m = lines2[0].match(headerRegex);
@@ -9347,7 +10200,7 @@ function markCompactStale(compactPath) {
9347
10200
  }
9348
10201
  function readCompactBody(compactPath) {
9349
10202
  try {
9350
- const text = fs19.readFileSync(compactPath, "utf-8");
10203
+ const text = fs20.readFileSync(compactPath, "utf-8");
9351
10204
  const lines2 = text.split("\n");
9352
10205
  if (lines2.length < 2) return null;
9353
10206
  const body = lines2.slice(1).join("\n").trimStart();
@@ -9357,14 +10210,14 @@ function readCompactBody(compactPath) {
9357
10210
  }
9358
10211
  }
9359
10212
  function writeCompact(compactPath, sourcePath, compactBody, sourceRel) {
9360
- const srcPath = path15.resolve(sourcePath);
10213
+ const srcPath = path16.resolve(sourcePath);
9361
10214
  const sha = sourceHash(srcPath);
9362
- const displayRel = sourceRel || path15.basename(srcPath);
10215
+ const displayRel = sourceRel || path16.basename(srcPath);
9363
10216
  const header = `${headerPrefix}${sha} source:${displayRel} -->
9364
10217
  `;
9365
10218
  const fullText = header + compactBody.trimStart();
9366
- const dir = path15.dirname(compactPath);
9367
- if (!fs19.existsSync(dir)) {
10219
+ const dir = path16.dirname(compactPath);
10220
+ if (!fs20.existsSync(dir)) {
9368
10221
  ensureDirSync(dir);
9369
10222
  }
9370
10223
  atomicWriteText(compactPath, fullText);
@@ -9488,7 +10341,7 @@ function extractDocCompact(body, heading) {
9488
10341
  }
9489
10342
  function compactDoc(filePath, heading) {
9490
10343
  try {
9491
- const body = fs19.readFileSync(filePath, "utf-8");
10344
+ const body = fs20.readFileSync(filePath, "utf-8");
9492
10345
  const compact = extractDocCompact(body, heading);
9493
10346
  return compact || null;
9494
10347
  } catch {
@@ -9498,21 +10351,21 @@ function compactDoc(filePath, heading) {
9498
10351
 
9499
10352
  // src/evidence_cache.ts
9500
10353
  import crypto2 from "node:crypto";
9501
- import fs20 from "node:fs";
9502
- import path16 from "node:path";
10354
+ import fs21 from "node:fs";
10355
+ import path17 from "node:path";
9503
10356
  var MAX_ENTRIES = 500;
9504
10357
  var MAX_TEXT_BYTES = 128 * 1024;
9505
10358
  var MAX_SEMANTIC_CANDIDATES = 100;
9506
10359
  var CACHE_FILE = "workspace-evidence.json";
9507
10360
  function cachePath() {
9508
- return path16.join(dataDir(), CACHE_FILE);
10361
+ return path17.join(dataDir(), CACHE_FILE);
9509
10362
  }
9510
10363
  function hash(text) {
9511
10364
  return crypto2.createHash("sha256").update(text).digest("hex");
9512
10365
  }
9513
10366
  function load2() {
9514
10367
  try {
9515
- const parsed = JSON.parse(fs20.readFileSync(cachePath(), "utf8"));
10368
+ const parsed = JSON.parse(fs21.readFileSync(cachePath(), "utf8"));
9516
10369
  if (!Array.isArray(parsed)) return [];
9517
10370
  return parsed.filter(
9518
10371
  (entry) => typeof entry === "object" && entry !== null && typeof entry.id === "string" && typeof entry.projectRoot === "string" && typeof entry.source === "string" && (entry.representation === "file" || entry.representation === "tool-output") && typeof entry.contentHash === "string" && typeof entry.text === "string" && typeof entry.createdAt === "number" && (entry.embedding === void 0 || typeof entry.embedding === "string")
@@ -9524,7 +10377,7 @@ function load2() {
9524
10377
  function save(entries) {
9525
10378
  try {
9526
10379
  ensureDirSync(dataDir());
9527
- fs20.writeFileSync(cachePath(), JSON.stringify(entries.slice(0, MAX_ENTRIES)), "utf8");
10380
+ fs21.writeFileSync(cachePath(), JSON.stringify(entries.slice(0, MAX_ENTRIES)), "utf8");
9528
10381
  } catch {
9529
10382
  }
9530
10383
  }
@@ -9615,7 +10468,7 @@ function buildDeltaCapsule(projectRoot, limit = 8) {
9615
10468
  const root = normalizePath(projectRoot);
9616
10469
  const changed = load2().filter((entry) => entry.projectRoot === root && entry.representation === "file").filter((entry) => {
9617
10470
  try {
9618
- return hash(fs20.readFileSync(entry.source, "utf8")) !== entry.contentHash;
10471
+ return hash(fs21.readFileSync(entry.source, "utf8")) !== entry.contentHash;
9619
10472
  } catch {
9620
10473
  return true;
9621
10474
  }
@@ -9626,8 +10479,8 @@ ${changed.map((entry) => `- ${entry.source} (use a fresh surgical read)`).join("
9626
10479
  }
9627
10480
 
9628
10481
  // src/notebook_compact.ts
9629
- import * as fs21 from "node:fs";
9630
- import * as path17 from "node:path";
10482
+ import * as fs22 from "node:fs";
10483
+ import * as path18 from "node:path";
9631
10484
  var NB_STRIP_MIN_SAVINGS = 4096;
9632
10485
  function stripNotebook(nbDict) {
9633
10486
  const cells = [];
@@ -9647,23 +10500,23 @@ function stripNotebook(nbDict) {
9647
10500
  var SIDECAR_DEFAULT_MAX_COUNT = 200;
9648
10501
  var SIDECAR_DEFAULT_MAX_AGE_MS = 24 * 3600 * 1e3;
9649
10502
  function pruneSidecars(cacheRoot, maxCount = SIDECAR_DEFAULT_MAX_COUNT, maxAgeMs = SIDECAR_DEFAULT_MAX_AGE_MS) {
9650
- const nbStripDir = path17.join(cacheRoot, "nb_strip");
10503
+ const nbStripDir = path18.join(cacheRoot, "nb_strip");
9651
10504
  let removed = 0;
9652
10505
  try {
9653
- if (!fs21.existsSync(nbStripDir)) return 0;
10506
+ if (!fs22.existsSync(nbStripDir)) return 0;
9654
10507
  const cutoff = Date.now() - maxAgeMs;
9655
10508
  const kept = [];
9656
- for (const entry of fs21.readdirSync(nbStripDir)) {
9657
- const dir = path17.join(nbStripDir, entry);
10509
+ for (const entry of fs22.readdirSync(nbStripDir)) {
10510
+ const dir = path18.join(nbStripDir, entry);
9658
10511
  let mtime;
9659
10512
  try {
9660
- mtime = fs21.statSync(dir).mtimeMs;
10513
+ mtime = fs22.statSync(dir).mtimeMs;
9661
10514
  } catch {
9662
10515
  continue;
9663
10516
  }
9664
10517
  if (mtime < cutoff) {
9665
10518
  try {
9666
- fs21.rmSync(dir, { recursive: true, force: true });
10519
+ fs22.rmSync(dir, { recursive: true, force: true });
9667
10520
  removed++;
9668
10521
  } catch {
9669
10522
  continue;
@@ -9676,7 +10529,7 @@ function pruneSidecars(cacheRoot, maxCount = SIDECAR_DEFAULT_MAX_COUNT, maxAgeMs
9676
10529
  kept.sort((a, b) => a[1] - b[1]);
9677
10530
  for (const [dir] of kept.slice(0, kept.length - maxCount)) {
9678
10531
  try {
9679
- fs21.rmSync(dir, { recursive: true, force: true });
10532
+ fs22.rmSync(dir, { recursive: true, force: true });
9680
10533
  removed++;
9681
10534
  } catch {
9682
10535
  continue;
@@ -9690,9 +10543,9 @@ function pruneSidecars(cacheRoot, maxCount = SIDECAR_DEFAULT_MAX_COUNT, maxAgeMs
9690
10543
  }
9691
10544
  function getOrCreateSidecar(rawBytes, cacheRoot, opts = {}) {
9692
10545
  const sha = fingerprintContent(rawBytes);
9693
- const sidecarDir = path17.join(cacheRoot, "nb_strip", sha);
9694
- const sidecarPath = path17.join(sidecarDir, "stripped.ipynb");
9695
- if (fs21.existsSync(sidecarPath)) {
10546
+ const sidecarDir = path18.join(cacheRoot, "nb_strip", sha);
10547
+ const sidecarPath = path18.join(sidecarDir, "stripped.ipynb");
10548
+ if (fs22.existsSync(sidecarPath)) {
9696
10549
  return [sidecarPath, false];
9697
10550
  }
9698
10551
  let nb;
@@ -9709,7 +10562,7 @@ function getOrCreateSidecar(rawBytes, cacheRoot, opts = {}) {
9709
10562
  try {
9710
10563
  ensureDirSync(sidecarDir);
9711
10564
  } catch (err) {
9712
- if (!fs21.existsSync(sidecarDir)) {
10565
+ if (!fs22.existsSync(sidecarDir)) {
9713
10566
  throw err;
9714
10567
  }
9715
10568
  }
@@ -9742,8 +10595,8 @@ function isNodeModulesPath(p) {
9742
10595
  return check.includes("/node_modules/") || check.includes("\\node_modules\\");
9743
10596
  }
9744
10597
  function relPathWithinRoot(root, target) {
9745
- const rel = path18.relative(root, target).replace(/\\/g, "/");
9746
- if (rel.startsWith("..") || path18.isAbsolute(rel)) return null;
10598
+ const rel = path19.relative(root, target).replace(/\\/g, "/");
10599
+ if (rel.startsWith("..") || path19.isAbsolute(rel)) return null;
9747
10600
  return rel;
9748
10601
  }
9749
10602
  function _isDocFile(filePath) {
@@ -9774,7 +10627,7 @@ function scanRequestedSlice(absPath, offset, limit) {
9774
10627
  const windowEnd = offset + limit;
9775
10628
  let fd;
9776
10629
  try {
9777
- fd = fs22.openSync(absPath, "r");
10630
+ fd = fs23.openSync(absPath, "r");
9778
10631
  } catch {
9779
10632
  return null;
9780
10633
  }
@@ -9788,7 +10641,7 @@ function scanRequestedSlice(absPath, offset, limit) {
9788
10641
  const nearSingleLine = lineNumber < NEAR_SINGLE_LINE_SCAN_THRESHOLD;
9789
10642
  return { bytes: sliceBytes, trustworthy: nearSingleLine, nearSingleLine };
9790
10643
  }
9791
- const bytesRead = fs22.readSync(fd, buf, 0, buf.length, null);
10644
+ const bytesRead = fs23.readSync(fd, buf, 0, buf.length, null);
9792
10645
  if (bytesRead === 0) {
9793
10646
  return {
9794
10647
  bytes: sliceBytes,
@@ -9807,7 +10660,7 @@ function scanRequestedSlice(absPath, offset, limit) {
9807
10660
  }
9808
10661
  } finally {
9809
10662
  try {
9810
- fs22.closeSync(fd);
10663
+ fs23.closeSync(fd);
9811
10664
  } catch {
9812
10665
  }
9813
10666
  }
@@ -9844,7 +10697,7 @@ var BINARY_FILE_TYPE_EXTS = /* @__PURE__ */ new Set(["pdf", "docx", "xlsx", "ppt
9844
10697
  var TEXT_FILE_TYPE_EXTS = /* @__PURE__ */ new Set(["html", "htm", "xhtml", "txt", "log", "out", "err", "trace", "csv", "tsv", "vtt", "srt"]);
9845
10698
  var DISPATCHED_FILE_TYPE_EXTS = /* @__PURE__ */ new Set([...BINARY_FILE_TYPE_EXTS, ...TEXT_FILE_TYPE_EXTS]);
9846
10699
  function isDispatchedFileType(basename12) {
9847
- return DISPATCHED_FILE_TYPE_EXTS.has(path18.extname(basename12).slice(1).toLowerCase());
10700
+ return DISPATCHED_FILE_TYPE_EXTS.has(path19.extname(basename12).slice(1).toLowerCase());
9848
10701
  }
9849
10702
  function surgicalHint(filePath, basename12, lineCount) {
9850
10703
  if (lineCount < loadConfig().hints.min_file_lines_for_hint) return "";
@@ -9907,7 +10760,7 @@ function loadSnapshotDiff(sessionId, normalized, basename12) {
9907
10760
  try {
9908
10761
  const sz = statSize(normalized);
9909
10762
  if (sz === null || sz > 256 * 1024) return { kind: "none" };
9910
- const currentContent = fs22.readFileSync(normalized, "utf8");
10763
+ const currentContent = fs23.readFileSync(normalized, "utf8");
9911
10764
  const TRUNC_MARKER = "\n<snapshot truncated at ";
9912
10765
  const oldRaw = oldSnap.toString("utf8");
9913
10766
  const truncIdx = oldRaw.indexOf(TRUNC_MARKER);
@@ -9993,7 +10846,7 @@ function preReadHandlerInner(event) {
9993
10846
  try {
9994
10847
  const cwd = getCwd(event) ?? process.cwd();
9995
10848
  const project = findProject(cwd) ?? makeProjectAt(cwd);
9996
- const current = fs22.readFileSync(normalized, "utf8");
10849
+ const current = fs23.readFileSync(normalized, "utf8");
9997
10850
  const evidence = findVerifiedFileEvidence(project.root, normalized, current);
9998
10851
  if (evidence !== null) {
9999
10852
  recordStat("evidence_cache_hit", 0);
@@ -10004,7 +10857,7 @@ function preReadHandlerInner(event) {
10004
10857
  } catch {
10005
10858
  }
10006
10859
  }
10007
- const basename12 = path18.basename(normalized);
10860
+ const basename12 = path19.basename(normalized);
10008
10861
  if (isLockFile(basename12)) {
10009
10862
  return denyOutput(
10010
10863
  'Lock files are rarely useful to read in full. Use `token-goat section "' + shown + '::<section>"` to extract a specific dependency, or read the relevant manifest instead.'
@@ -10043,7 +10896,7 @@ function preReadHandlerInner(event) {
10043
10896
  const skillName = detectSkillFile(normalized);
10044
10897
  if (skillName && basename12 === "SKILL.md") {
10045
10898
  try {
10046
- const body = fs22.readFileSync(normalized, "utf-8");
10899
+ const body = fs23.readFileSync(normalized, "utf-8");
10047
10900
  const bodySha = contentHash(body);
10048
10901
  const compact = getCompactAnySessionSync(skillName);
10049
10902
  const stale = isCompactStale(compact, skillName, bodySha);
@@ -10074,9 +10927,9 @@ function preReadHandlerInner(event) {
10074
10927
  const isNotebook = /\.ipynb$/i.test(basename12);
10075
10928
  if (event.toolName !== "Grep" && isNotebook) {
10076
10929
  try {
10077
- const rawBytes = fs22.readFileSync(normalized);
10930
+ const rawBytes = fs23.readFileSync(normalized);
10078
10931
  const [sidecarPath] = getOrCreateSidecar(rawBytes, dataDir());
10079
- const sidecarContent = fs22.readFileSync(sidecarPath, "utf-8");
10932
+ const sidecarContent = fs23.readFileSync(sidecarPath, "utf-8");
10080
10933
  const savedBytes = rawBytes.length - sidecarContent.length;
10081
10934
  if (savedBytes >= NB_STRIP_MIN_SAVINGS) {
10082
10935
  recordActualRead(event, normalized);
@@ -10096,7 +10949,7 @@ function preReadHandlerInner(event) {
10096
10949
  const sz = statSize(normalized);
10097
10950
  if (sz !== null && sz >= MARKDOWN_SIZE_THRESHOLD) {
10098
10951
  markdownSize = sz;
10099
- fileContent = fs22.readFileSync(normalized, "utf8");
10952
+ fileContent = fs23.readFileSync(normalized, "utf8");
10100
10953
  }
10101
10954
  } catch {
10102
10955
  }
@@ -10325,7 +11178,7 @@ function preReadHandlerInner(event) {
10325
11178
  "Note: " + shown + " is large (" + kb + "KB). " + hint + contextPressureAdvisorySuffix()
10326
11179
  );
10327
11180
  }
10328
- const fileTypeExt = path18.extname(normalized).slice(1).toLowerCase();
11181
+ const fileTypeExt = path19.extname(normalized).slice(1).toLowerCase();
10329
11182
  const fileStatSize = size ?? statSize(normalized) ?? 0;
10330
11183
  const isKnownFileType = DISPATCHED_FILE_TYPE_EXTS.has(fileTypeExt);
10331
11184
  if (event.toolName !== "Grep" && !isImagePath(normalized) && (isKnownFileType || fileStatSize >= FILE_TYPE_THRESHOLDS.generic)) {
@@ -10334,7 +11187,7 @@ function preReadHandlerInner(event) {
10334
11187
  let ftContent = "";
10335
11188
  if (!BINARY_FILE_TYPE_EXTS.has(fileTypeExt) && fileStatSize <= SLICE_ESTIMATE_SCAN_CAP_BYTES) {
10336
11189
  try {
10337
- ftContent = fs22.readFileSync(normalized, "utf8");
11190
+ ftContent = fs23.readFileSync(normalized, "utf8");
10338
11191
  } catch {
10339
11192
  }
10340
11193
  }
@@ -10364,7 +11217,7 @@ function estimateTruncatedLineCount(normalized) {
10364
11217
  try {
10365
11218
  const sz = statSize(normalized);
10366
11219
  if (sz !== null && sz <= SLICE_ESTIMATE_SCAN_CAP_BYTES) {
10367
- return countTextLines(fs22.readFileSync(normalized, "utf8"));
11220
+ return countTextLines(fs23.readFileSync(normalized, "utf8"));
10368
11221
  }
10369
11222
  } catch {
10370
11223
  }
@@ -10387,13 +11240,13 @@ function postReadHandlerInner(event) {
10387
11240
  if (respText.includes("[Truncated:") || respText.includes("Truncated: PARTIAL view")) {
10388
11241
  markFileTruncated(normalized);
10389
11242
  }
10390
- const postBasename = path18.basename(normalized);
11243
+ const postBasename = path19.basename(normalized);
10391
11244
  const diffSourcesEnabled = loadConfig().hints.serve_diff_on_reread;
10392
11245
  if (/\.(md|mdx|markdown|rst|txt)$/i.test(postBasename) || isSessionArtifactFile(normalized) || diffSourcesEnabled && DIFFABLE_SOURCE_RE.test(postBasename)) {
10393
11246
  try {
10394
11247
  const sz = statSize(normalized);
10395
11248
  if (sz !== null && sz <= 256 * 1024) {
10396
- const content = fs22.readFileSync(normalized);
11249
+ const content = fs23.readFileSync(normalized);
10397
11250
  store(getSessionId(), normalized, content);
10398
11251
  }
10399
11252
  } catch {
@@ -10403,7 +11256,7 @@ function postReadHandlerInner(event) {
10403
11256
  try {
10404
11257
  const cwd = getCwd(event) ?? process.cwd();
10405
11258
  const project = findProject(cwd) ?? makeProjectAt(cwd);
10406
- const source = decodeSource(fs22.readFileSync(normalized));
11259
+ const source = decodeSource(fs23.readFileSync(normalized));
10407
11260
  recordEvidence({ projectRoot: project.root, source: normalized, representation: "file", text: source });
10408
11261
  } catch {
10409
11262
  }
@@ -10434,7 +11287,7 @@ function postReadHandlerInner(event) {
10434
11287
  try {
10435
11288
  const sz = statSize(normalized);
10436
11289
  if (sz !== null && sz <= SLICE_ESTIMATE_SCAN_CAP_BYTES) {
10437
- const lineCount = countTextLines(fs22.readFileSync(normalized, "utf8"));
11290
+ const lineCount = countTextLines(fs23.readFileSync(normalized, "utf8"));
10438
11291
  const minLines = loadConfig().post_read_code_compress.min_lines;
10439
11292
  if (lineCount >= minLines && meetsSavingsFloor(sz)) {
10440
11293
  recordStat("session_hint", 0, 0);
@@ -10454,9 +11307,9 @@ function postReadHandler(event) {
10454
11307
  registerHook("post_tool_use", postReadHandler, { toolName: "Read" });
10455
11308
 
10456
11309
  // src/cli_context_stats.ts
10457
- import * as fs24 from "node:fs";
11310
+ import * as fs25 from "node:fs";
10458
11311
  import * as os8 from "node:os";
10459
- import * as path20 from "node:path";
11312
+ import * as path21 from "node:path";
10460
11313
 
10461
11314
  // src/confirm_apply.ts
10462
11315
  import * as readline from "node:readline";
@@ -10510,8 +11363,8 @@ ${diff}
10510
11363
  }
10511
11364
 
10512
11365
  // src/memory_prune.ts
10513
- import * as fs23 from "node:fs";
10514
- import * as path19 from "node:path";
11366
+ import * as fs24 from "node:fs";
11367
+ import * as path20 from "node:path";
10515
11368
  var ENTRY_RE = /^\s*-\s*\[(?<title>[^\]]+)\]\((?<target>[^)]+?\.md)\)/;
10516
11369
  var URL_SCHEME_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
10517
11370
  function parseIndex(text) {
@@ -10556,10 +11409,10 @@ function pruneIndex(memoryDir, opts) {
10556
11409
  changed: false,
10557
11410
  tokensSaved: 0
10558
11411
  };
10559
- const memoryMd = path19.join(memoryDir, "MEMORY.md");
11412
+ const memoryMd = path20.join(memoryDir, "MEMORY.md");
10560
11413
  let text;
10561
11414
  try {
10562
- text = fs23.readFileSync(memoryMd, "utf-8");
11415
+ text = fs24.readFileSync(memoryMd, "utf-8");
10563
11416
  } catch {
10564
11417
  return result;
10565
11418
  }
@@ -10570,7 +11423,7 @@ function pruneIndex(memoryDir, opts) {
10570
11423
  const dups = [];
10571
11424
  for (const entry of entries) {
10572
11425
  const isUrl = URL_SCHEME_RE.test(entry.target);
10573
- const targetExists = isUrl ? true : path19.isAbsolute(entry.target) ? fs23.existsSync(entry.target) : fs23.existsSync(path19.join(memoryDir, entry.target));
11426
+ const targetExists = isUrl ? true : path20.isAbsolute(entry.target) ? fs24.existsSync(entry.target) : fs24.existsSync(path20.join(memoryDir, entry.target));
10574
11427
  const foldedTarget = foldPath(entry.target);
10575
11428
  if (!targetExists) {
10576
11429
  dead.push(entry);
@@ -10624,7 +11477,7 @@ function jaccard(a, b) {
10624
11477
  function siblingSnippet(filePath) {
10625
11478
  let text;
10626
11479
  try {
10627
- text = fs23.readFileSync(filePath, { encoding: "utf-8" });
11480
+ text = fs24.readFileSync(filePath, { encoding: "utf-8" });
10628
11481
  } catch {
10629
11482
  return "";
10630
11483
  }
@@ -10714,7 +11567,7 @@ async function tryEmbeddingClusters(siblings, snippets, threshold) {
10714
11567
  }
10715
11568
  async function findContentDuplicates(memoryDir, _opts) {
10716
11569
  const threshold = _opts?.threshold ?? 0.92;
10717
- const siblings = fs23.readdirSync(memoryDir).filter((name) => name.toLowerCase().endsWith(".md") && name.toLowerCase() !== "memory.md").map((name) => path19.join(memoryDir, name)).sort();
11570
+ const siblings = fs24.readdirSync(memoryDir).filter((name) => name.toLowerCase().endsWith(".md") && name.toLowerCase() !== "memory.md").map((name) => path20.join(memoryDir, name)).sort();
10718
11571
  if (siblings.length < 2) {
10719
11572
  return [];
10720
11573
  }
@@ -10738,7 +11591,7 @@ function auditClaudeMd(files) {
10738
11591
  for (const filePath of files) {
10739
11592
  let text;
10740
11593
  try {
10741
- text = fs23.readFileSync(filePath, { encoding: "utf-8" });
11594
+ text = fs24.readFileSync(filePath, { encoding: "utf-8" });
10742
11595
  } catch {
10743
11596
  continue;
10744
11597
  }
@@ -10798,7 +11651,7 @@ function auditClaudeMd(files) {
10798
11651
  const overlaps = [];
10799
11652
  for (const [stripped, filesSet] of lineToFiles) {
10800
11653
  if (filesSet.has(report.path) && filesSet.size > 1) {
10801
- const others = Array.from(filesSet).filter((p) => p !== report.path).map((p) => path19.basename(p));
11654
+ const others = Array.from(filesSet).filter((p) => p !== report.path).map((p) => path20.basename(p));
10802
11655
  if (others.length > 0) {
10803
11656
  if (stripped.length > 60) {
10804
11657
  overlaps.push(
@@ -10818,7 +11671,7 @@ function auditClaudeMd(files) {
10818
11671
  // src/cli_context_stats.ts
10819
11672
  function tok(filePath) {
10820
11673
  try {
10821
- const size = fs24.statSync(filePath).size;
11674
+ const size = fs25.statSync(filePath).size;
10822
11675
  return Math.floor(size / 4);
10823
11676
  } catch {
10824
11677
  return 0;
@@ -10827,42 +11680,42 @@ function tok(filePath) {
10827
11680
  function findClaudeMdFiles(projectRoot, homeDir = os8.homedir()) {
10828
11681
  const found = [];
10829
11682
  const seen = /* @__PURE__ */ new Set();
10830
- let current = path20.resolve(projectRoot);
11683
+ let current = path21.resolve(projectRoot);
10831
11684
  while (true) {
10832
- const candidate = path20.join(current, "CLAUDE.md");
10833
- if (!seen.has(candidate) && fs24.existsSync(candidate)) {
11685
+ const candidate = path21.join(current, "CLAUDE.md");
11686
+ if (!seen.has(candidate) && fs25.existsSync(candidate)) {
10834
11687
  found.push(candidate);
10835
11688
  seen.add(candidate);
10836
11689
  }
10837
- const parent = path20.dirname(current);
11690
+ const parent = path21.dirname(current);
10838
11691
  if (parent === current) break;
10839
11692
  current = parent;
10840
11693
  }
10841
- const globalMd = path20.join(homeDir, ".claude", "CLAUDE.md");
10842
- if (!seen.has(globalMd) && fs24.existsSync(globalMd)) {
11694
+ const globalMd = path21.join(homeDir, ".claude", "CLAUDE.md");
11695
+ if (!seen.has(globalMd) && fs25.existsSync(globalMd)) {
10843
11696
  found.push(globalMd);
10844
11697
  }
10845
11698
  return found;
10846
11699
  }
10847
11700
  function findMemoryMd(projectRoot, homeDir = os8.homedir(), alternateRoots = []) {
10848
11701
  try {
10849
- const projectsDir = path20.join(homeDir, ".claude", "projects");
10850
- if (!fs24.existsSync(projectsDir)) return null;
10851
- const rootStr = path20.resolve(projectRoot);
11702
+ const projectsDir = path21.join(homeDir, ".claude", "projects");
11703
+ if (!fs25.existsSync(projectsDir)) return null;
11704
+ const rootStr = path21.resolve(projectRoot);
10852
11705
  const candidateRoots = [rootStr];
10853
11706
  try {
10854
- const realRoot = fs24.realpathSync.native(rootStr);
11707
+ const realRoot = fs25.realpathSync.native(rootStr);
10855
11708
  if (realRoot !== rootStr) candidateRoots.push(realRoot);
10856
11709
  } catch {
10857
11710
  }
10858
11711
  for (const alternate of alternateRoots) {
10859
- const resolved = path20.resolve(alternate);
11712
+ const resolved = path21.resolve(alternate);
10860
11713
  if (!candidateRoots.includes(resolved)) candidateRoots.push(resolved);
10861
11714
  }
10862
11715
  for (const root of candidateRoots) {
10863
11716
  const expectedSlug = root.replace(/[^A-Za-z0-9]/g, "-");
10864
- const candidate = path20.join(projectsDir, expectedSlug, "memory", "MEMORY.md");
10865
- if (fs24.existsSync(candidate)) return candidate;
11717
+ const candidate = path21.join(projectsDir, expectedSlug, "memory", "MEMORY.md");
11718
+ if (fs25.existsSync(candidate)) return candidate;
10866
11719
  }
10867
11720
  return null;
10868
11721
  } catch {
@@ -10876,10 +11729,10 @@ function buildStats(projectRoot, homeDir = os8.homedir(), alternateRoots = []) {
10876
11729
  for (const p of claudeMds) {
10877
11730
  const t = tok(p);
10878
11731
  claudeMdTotal += t;
10879
- const parentDir = path20.basename(path20.dirname(p));
11732
+ const parentDir = path21.basename(path21.dirname(p));
10880
11733
  const label = parentDir === ".claude" ? "~/.claude/CLAUDE.md" : (() => {
10881
11734
  try {
10882
- return path20.relative(projectRoot, p);
11735
+ return path21.relative(projectRoot, p);
10883
11736
  } catch {
10884
11737
  return p;
10885
11738
  }
@@ -10943,11 +11796,11 @@ async function runContextStats(opts = {}) {
10943
11796
  process.stdout.write("[--fix] No MEMORY.md found; nothing to prune.\n");
10944
11797
  } else {
10945
11798
  const memPath = result.memory_md_path;
10946
- const pruneResult = pruneIndex(path20.dirname(memPath), { dryRun: true });
11799
+ const pruneResult = pruneIndex(path21.dirname(memPath), { dryRun: true });
10947
11800
  if (!pruneResult.changed || pruneResult.after === void 0) {
10948
11801
  process.stdout.write("[--fix] MEMORY.md already clean; nothing to prune.\n");
10949
11802
  } else {
10950
- const before = fs24.readFileSync(memPath, "utf-8");
11803
+ const before = fs25.readFileSync(memPath, "utf-8");
10951
11804
  const applyResult = await confirmAndApply(
10952
11805
  [{ path: memPath, before, after: pruneResult.after, label: "MEMORY.md" }],
10953
11806
  opts.yes === true ? { yes: true } : {}
@@ -10974,8 +11827,8 @@ async function runContextStats(opts = {}) {
10974
11827
  }
10975
11828
 
10976
11829
  // src/baseline.ts
10977
- import * as fs25 from "node:fs";
10978
- import * as path21 from "node:path";
11830
+ import * as fs26 from "node:fs";
11831
+ import * as path22 from "node:path";
10979
11832
  var SKIP_DIRS = /* @__PURE__ */ new Set([
10980
11833
  "node_modules",
10981
11834
  ".git",
@@ -11013,12 +11866,12 @@ function walkProject(rootDir, opts = {}) {
11013
11866
  if (dir === void 0) break;
11014
11867
  let entries;
11015
11868
  try {
11016
- entries = fs25.readdirSync(dir, { withFileTypes: true });
11869
+ entries = fs26.readdirSync(dir, { withFileTypes: true });
11017
11870
  } catch {
11018
11871
  continue;
11019
11872
  }
11020
11873
  for (const entry of entries) {
11021
- const full = path21.join(dir, entry.name);
11874
+ const full = path22.join(dir, entry.name);
11022
11875
  if (entry.isDirectory()) {
11023
11876
  if (SKIP_DIRS.has(entry.name) || extraSkipDirs.includes(entry.name)) continue;
11024
11877
  if (entry.name.startsWith(".") && entry.name !== ".") {
@@ -11093,7 +11946,7 @@ function fetchTopSymbols(limit, dbPath, rootDir) {
11093
11946
  }
11094
11947
  }
11095
11948
  function buildProjectMap(rootDir = process.cwd(), opts = {}) {
11096
- const root = path21.resolve(rootDir);
11949
+ const root = path22.resolve(rootDir);
11097
11950
  const config = loadConfig();
11098
11951
  const { files, languages } = walkProject(root, { excludeTests: config.repomap.exclude_tests });
11099
11952
  const compact = opts.compact === true || files.length > config.repomap.compact_file_threshold;
@@ -11102,12 +11955,12 @@ function buildProjectMap(rootDir = process.cwd(), opts = {}) {
11102
11955
  const recentFiles = files.map((f) => {
11103
11956
  let mtime;
11104
11957
  try {
11105
- mtime = fs25.statSync(f).mtimeMs;
11958
+ mtime = fs26.statSync(f).mtimeMs;
11106
11959
  } catch {
11107
11960
  mtime = 0;
11108
11961
  }
11109
11962
  return { f, mtime };
11110
- }).sort((a, b) => b.mtime - a.mtime).slice(0, compact ? 5 : 15).map((x) => path21.relative(root, x.f));
11963
+ }).sort((a, b) => b.mtime - a.mtime).slice(0, compact ? 5 : 15).map((x) => path22.relative(root, x.f));
11111
11964
  return {
11112
11965
  rootDir: root,
11113
11966
  fileCount: files.length,
@@ -11119,7 +11972,7 @@ function buildProjectMap(rootDir = process.cwd(), opts = {}) {
11119
11972
  }
11120
11973
  function formatProjectMap(map, compact = false) {
11121
11974
  const lines2 = [];
11122
- const rel = path21.basename(map.rootDir);
11975
+ const rel = path22.basename(map.rootDir);
11123
11976
  lines2.push(`# Project map: ${rel}`);
11124
11977
  lines2.push(`Files: ${map.fileCount}`);
11125
11978
  const langPairs = Object.entries(map.languages).sort((a, b) => b[1] - a[1]);
@@ -11147,13 +12000,13 @@ function formatProjectMap(map, compact = false) {
11147
12000
  }
11148
12001
  function mapLookupBytesSaved(map, emittedText) {
11149
12002
  const referencedFiles = /* @__PURE__ */ new Set([
11150
- ...map.recentFiles.map((f) => normalizePath(path21.resolve(map.rootDir, f))),
12003
+ ...map.recentFiles.map((f) => normalizePath(path22.resolve(map.rootDir, f))),
11151
12004
  ...map.topSymbols.map((s) => normalizePath(s.filePath))
11152
12005
  ]);
11153
12006
  let fullSourceBytes = 0;
11154
12007
  for (const fp of referencedFiles) {
11155
12008
  try {
11156
- fullSourceBytes += fs25.statSync(fp).size;
12009
+ fullSourceBytes += fs26.statSync(fp).size;
11157
12010
  } catch {
11158
12011
  }
11159
12012
  }
@@ -11167,14 +12020,14 @@ function findMemSuggestionCandidates(projectRoot) {
11167
12020
  const claudeMdFiles = findClaudeMdFiles(projectRoot);
11168
12021
  const candidateFiles = new Set(claudeMdFiles);
11169
12022
  for (const claudeMd of claudeMdFiles) {
11170
- const agentsMd = path21.join(path21.dirname(claudeMd), "AGENTS.md");
11171
- if (fs25.existsSync(agentsMd)) candidateFiles.add(agentsMd);
12023
+ const agentsMd = path22.join(path22.dirname(claudeMd), "AGENTS.md");
12024
+ if (fs26.existsSync(agentsMd)) candidateFiles.add(agentsMd);
11172
12025
  }
11173
12026
  const suggestions = [];
11174
12027
  for (const filePath of candidateFiles) {
11175
12028
  let text;
11176
12029
  try {
11177
- text = fs25.readFileSync(filePath, { encoding: "utf-8" });
12030
+ text = fs26.readFileSync(filePath, { encoding: "utf-8" });
11178
12031
  } catch {
11179
12032
  continue;
11180
12033
  }
@@ -11203,7 +12056,7 @@ function formatMemSuggestions(projectRoot) {
11203
12056
  if (suggestions.length === 0) return "";
11204
12057
  const lines2 = ["", "## mem suggestions"];
11205
12058
  for (const s of suggestions) {
11206
- const basename12 = path21.basename(s.path);
12059
+ const basename12 = path22.basename(s.path);
11207
12060
  lines2.push(
11208
12061
  "Consider: mem import --from-md " + s.path + " # migrates " + s.count + " preference-shaped lines from " + basename12 + " as pending facts for review"
11209
12062
  );
@@ -11343,9 +12196,9 @@ function getFileEntry(filePath, dbPath = globalDbPath()) {
11343
12196
  embedSha: row.embed_sha ?? ""
11344
12197
  };
11345
12198
  }
11346
- function sanitizeFtsQuery(query, join22 = "AND") {
12199
+ function sanitizeFtsQuery(query, join23 = "AND") {
11347
12200
  const terms = query.split(/\s+/).filter((t) => t.length > 0).map((t) => '"' + t.replace(/"/g, '""') + '"');
11348
- return terms.join(join22 === "OR" ? " OR " : " ");
12201
+ return terms.join(join23 === "OR" ? " OR " : " ");
11349
12202
  }
11350
12203
  function runFtsQuery(db, match, limit, scope, rootDir) {
11351
12204
  const sql = `SELECT s.file_path, s.name, s.kind, s.line_start, s.line_end, s.body, s.docstring, s.parent FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE symbols_fts MATCH ?${scope !== void 0 ? ` AND ${scope.clause}` : ""} ORDER BY bm25(symbols_fts) LIMIT ?`;
@@ -11374,9 +12227,9 @@ function searchSymbolsFts(query, limit = 50, dbPath = globalDbPath(), rootDir) {
11374
12227
  }
11375
12228
 
11376
12229
  // src/parser.ts
11377
- import * as fs26 from "node:fs";
12230
+ import * as fs27 from "node:fs";
11378
12231
  import { createRequire as createRequire4 } from "node:module";
11379
- import * as path26 from "node:path";
12232
+ import * as path27 from "node:path";
11380
12233
 
11381
12234
  // src/languages/csharp.ts
11382
12235
  var USING_RE = /^(?:global\s+)?using\s+(?:static\s+)?([A-Za-z_][A-Za-z0-9_.]*)\s*(?:=\s*([A-Za-z_][A-Za-z0-9_.<>,\s]*))?\s*;/;
@@ -11809,7 +12662,7 @@ function extractHtml(content, filePath) {
11809
12662
  }
11810
12663
 
11811
12664
  // src/languages/liquid.ts
11812
- import * as path22 from "node:path";
12665
+ import * as path23 from "node:path";
11813
12666
  var INCLUDE_RE = /{%-?\s*include\s+(['"])((?:(?!\1)[\s\S])+?)\1/gi;
11814
12667
  var SECTION_RE = /{%-?\s*section\s+(['"])((?:(?!\1)[\s\S])+?)\1/gi;
11815
12668
  var RENDER_RE = /{%-?\s*render\s+(['"])((?:(?!\1)[\s\S])+?)\1/gi;
@@ -11852,7 +12705,7 @@ function extractLiquid(content, filePath, relPath) {
11852
12705
  const resolvedRel = relPath ?? filePath;
11853
12706
  const relPosix = resolvedRel.replace(/\\/g, "/");
11854
12707
  if (relPosix.startsWith("sections/") || relPosix.includes("/sections/")) {
11855
- const stem = path22.basename(resolvedRel, path22.extname(resolvedRel));
12708
+ const stem = path23.basename(resolvedRel, path23.extname(resolvedRel));
11856
12709
  symbols.push({ filePath, name: stem, kind: "liquid_section_file", lineStart: 1, lineEnd: 1, body: "", docstring: "", parent: "" });
11857
12710
  }
11858
12711
  const totalLines = countContentLines(content);
@@ -14155,7 +15008,7 @@ function extractApex(content, filePath) {
14155
15008
  }
14156
15009
 
14157
15010
  // src/languages/salesforce_metadata.ts
14158
- import * as path23 from "node:path";
15011
+ import * as path24 from "node:path";
14159
15012
  var MAX_SYMBOLS10 = 1e3;
14160
15013
  var MAX_REFS = 1e3;
14161
15014
  var FLOW_TAG_KIND = {
@@ -14213,7 +15066,7 @@ function normalizedPath(filePath) {
14213
15066
  return filePath.replace(/\\/g, "/");
14214
15067
  }
14215
15068
  function basenameWithout(filePath, suffix) {
14216
- const base = path23.basename(filePath);
15069
+ const base = path24.basename(filePath);
14217
15070
  return base.toLowerCase().endsWith(suffix.toLowerCase()) ? base.slice(0, base.length - suffix.length) : base;
14218
15071
  }
14219
15072
  function objectNameFromPath(filePath) {
@@ -14252,12 +15105,12 @@ function snakeCase(value) {
14252
15105
  return value.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
14253
15106
  }
14254
15107
  function companionName(filePath) {
14255
- const base = path23.basename(filePath);
15108
+ const base = path24.basename(filePath);
14256
15109
  const match = /^(.+)\.(?:cls|trigger|page|component|cmp|app|evt|intf|design|auradoc|tokens|js)-meta\.xml$/i.exec(base);
14257
15110
  return match?.[1] === void 0 ? null : `${match[1]}.metadata`;
14258
15111
  }
14259
15112
  function metadataArtifactName(filePath) {
14260
- const base = path23.basename(filePath);
15113
+ const base = path24.basename(filePath);
14261
15114
  const match = /^(.+)\.[^.]+-meta\.xml$/i.exec(base);
14262
15115
  return match?.[1] ?? basenameWithout(filePath, "-meta.xml");
14263
15116
  }
@@ -14348,7 +15201,7 @@ function extractSalesforceMetadata(rawContent, filePath) {
14348
15201
  const seen = /* @__PURE__ */ new Set();
14349
15202
  const refs = [];
14350
15203
  const seenRefs = /* @__PURE__ */ new Set();
14351
- const base = path23.basename(filePath).toLowerCase();
15204
+ const base = path24.basename(filePath).toLowerCase();
14352
15205
  const whole = wholeFileSpan(content);
14353
15206
  const root = rootElement(content);
14354
15207
  if (root === null) return { symbols, refs };
@@ -14492,14 +15345,14 @@ function extractSalesforceMetadata(rawContent, filePath) {
14492
15345
  }
14493
15346
 
14494
15347
  // src/languages/salesforce_frontend.ts
14495
- import * as path24 from "node:path";
15348
+ import * as path25 from "node:path";
14496
15349
  function lines(content) {
14497
15350
  return content.split("\n");
14498
15351
  }
14499
15352
  function bundleName(filePath) {
14500
15353
  const normalized = filePath.replaceAll("\\", "/");
14501
- const parent = path24.posix.basename(path24.posix.dirname(normalized));
14502
- const base = path24.posix.basename(normalized).replace(/\.[^.]+$/, "");
15354
+ const parent = path25.posix.basename(path25.posix.dirname(normalized));
15355
+ const base = path25.posix.basename(normalized).replace(/\.[^.]+$/, "");
14503
15356
  return parent === "lwc" || parent === "aura" ? base : parent;
14504
15357
  }
14505
15358
  function lwcTagAlias(name) {
@@ -14603,7 +15456,7 @@ var MARKUP_KIND = {
14603
15456
  };
14604
15457
  function markupArtifactName(filePath, extension) {
14605
15458
  if (extension === ".page" || extension === ".component" || extension === ".email") {
14606
- return path24.posix.basename(filePath.replaceAll("\\", "/")).replace(new RegExp(`${extension.replace(".", "\\.")}$`, "i"), "");
15459
+ return path25.posix.basename(filePath.replaceAll("\\", "/")).replace(new RegExp(`${extension.replace(".", "\\.")}$`, "i"), "");
14607
15460
  }
14608
15461
  return bundleName(filePath);
14609
15462
  }
@@ -14623,7 +15476,7 @@ function attributeRefs(refs, content, filePath, attribute, split = false) {
14623
15476
  }
14624
15477
  function extractSalesforceMarkup(content, filePath) {
14625
15478
  const normalized = filePath.replaceAll("\\", "/");
14626
- const extension = path24.posix.extname(normalized).toLowerCase();
15479
+ const extension = path25.posix.extname(normalized).toLowerCase();
14627
15480
  const kind = MARKUP_KIND[extension] ?? "salesforce_markup";
14628
15481
  const symbols = [
14629
15482
  symbol(filePath, markupArtifactName(normalized, extension), kind, 1, countContentLines(content))
@@ -14655,7 +15508,7 @@ function extractSalesforceMarkup(content, filePath) {
14655
15508
  }
14656
15509
 
14657
15510
  // src/languages/sfc_idx.ts
14658
- import * as path25 from "node:path";
15511
+ import * as path26 from "node:path";
14659
15512
  var MAX_SYMBOLS11 = 500;
14660
15513
  function dedupe2(values, key) {
14661
15514
  const seen = /* @__PURE__ */ new Set();
@@ -14674,7 +15527,7 @@ function finalize(symbols, refs) {
14674
15527
  }
14675
15528
  function componentName(filePath) {
14676
15529
  const normalized = filePath.replaceAll("\\", "/");
14677
- return path25.posix.basename(normalized).replace(/\.[^.]+$/, "");
15530
+ return path26.posix.basename(normalized).replace(/\.[^.]+$/, "");
14678
15531
  }
14679
15532
  function matchLine2(content, offset) {
14680
15533
  return content.slice(0, offset).split("\n").length;
@@ -14986,8 +15839,8 @@ function countNewlines(s) {
14986
15839
  return n;
14987
15840
  }
14988
15841
  function loadGrammar(lang, filePath, content) {
14989
- const useTsx = lang === "typescript" && filePath !== void 0 && path26.extname(filePath).toLowerCase() === ".tsx";
14990
- const useCppHeader = lang === "c" && filePath !== void 0 && path26.extname(filePath).toLowerCase() === ".h" && content !== void 0 && CPP_HEADER_SNIFF_RE.test(content);
15842
+ const useTsx = lang === "typescript" && filePath !== void 0 && path27.extname(filePath).toLowerCase() === ".tsx";
15843
+ const useCppHeader = lang === "c" && filePath !== void 0 && path27.extname(filePath).toLowerCase() === ".h" && content !== void 0 && CPP_HEADER_SNIFF_RE.test(content);
14991
15844
  const cacheKey = useTsx ? "typescript:tsx" : useCppHeader ? "c:cpp-header" : lang;
14992
15845
  const cached = _grammarCache.get(cacheKey);
14993
15846
  if (cached !== void 0) return cached;
@@ -15135,6 +15988,9 @@ function makeSymbol(filePath, name, kind, node, lines2, style) {
15135
15988
  parent: ""
15136
15989
  };
15137
15990
  }
15991
+ function fanOutElidesBodies(nameCount, declarationChars) {
15992
+ return nameCount > 1 && nameCount * declarationChars > MAX_SYMBOL_BODY_CHARS;
15993
+ }
15138
15994
  function collectPatternBindings(node) {
15139
15995
  const names = [];
15140
15996
  const walk = (n) => {
@@ -15142,6 +15998,16 @@ function collectPatternBindings(node) {
15142
15998
  if (n.text !== "") names.push(n.text);
15143
15999
  return;
15144
16000
  }
16001
+ if (n.type === "assignment_pattern" || n.type === "object_assignment_pattern") {
16002
+ const left = n.childForFieldName("left");
16003
+ if (left !== null) walk(left);
16004
+ return;
16005
+ }
16006
+ if (n.type === "pair_pattern") {
16007
+ const value = n.childForFieldName("value");
16008
+ if (value !== null) walk(value);
16009
+ return;
16010
+ }
15145
16011
  for (const child of n.namedChildren) walk(child);
15146
16012
  };
15147
16013
  walk(node);
@@ -15203,7 +16069,7 @@ function extractTsJsSymbols(root, filePath, lines2) {
15203
16069
  out.push(makeSymbol(filePath, name.text, isFn ? "function" : "variable", child, lines2, "c"));
15204
16070
  } else {
15205
16071
  const bindings = collectPatternBindings(name);
15206
- const elideBodies = bindings.length > 1 && bindings.length * child.text.length > MAX_SYMBOL_BODY_CHARS;
16072
+ const elideBodies = fanOutElidesBodies(bindings.length, child.text.length);
15207
16073
  for (const bound of bindings) {
15208
16074
  const sym = makeSymbol(filePath, bound, "variable", child, lines2, "c");
15209
16075
  out.push(elideBodies ? { ...sym, body: "" } : sym);
@@ -15321,14 +16187,26 @@ var GO_LOCAL_KINDS = /* @__PURE__ */ new Set([
15321
16187
  // the interface type declaring them does not.
15322
16188
  "method_elem"
15323
16189
  ]);
16190
+ var GO_MULTI_NAME_SPECS = /* @__PURE__ */ new Set(["var_spec", "const_spec"]);
15324
16191
  function extractGoSymbols(root, filePath, lines2) {
15325
16192
  const out = [];
15326
16193
  const visit = (node, insideFunction) => {
15327
16194
  const kind = GO_KIND_BY_TYPE.get(node.type);
15328
16195
  if (kind !== void 0 && !(insideFunction && GO_LOCAL_KINDS.has(node.type))) {
15329
- const name = nodeName(node);
15330
- if (name !== null && name !== "") {
15331
- out.push(makeSymbol(filePath, name, kind, node, lines2, "c"));
16196
+ if (GO_MULTI_NAME_SPECS.has(node.type)) {
16197
+ const declared = node.namedChildren.filter(
16198
+ (c) => c.type === "identifier" && c.text !== "" && c.text !== "_"
16199
+ );
16200
+ const elideBodies = fanOutElidesBodies(declared.length, node.text.length);
16201
+ for (const child of declared) {
16202
+ const sym = makeSymbol(filePath, child.text, kind, node, lines2, "c");
16203
+ out.push(elideBodies ? { ...sym, body: "" } : sym);
16204
+ }
16205
+ } else {
16206
+ const name = nodeName(node);
16207
+ if (name !== null && name !== "") {
16208
+ out.push(makeSymbol(filePath, name, kind, node, lines2, "c"));
16209
+ }
15332
16210
  }
15333
16211
  }
15334
16212
  const childInside = insideFunction || GO_FN_SCOPE_TYPES.has(node.type);
@@ -16654,9 +17532,9 @@ function isUnderSkipDir(filePath, skipDirs) {
16654
17532
  }
16655
17533
  function isParseSkipEligible(filePath, cfg) {
16656
17534
  if (isUnderSkipDir(filePath, cfg.skip_dirs)) return true;
16657
- if (cfg.skip_files.includes(path26.basename(filePath))) return true;
17535
+ if (cfg.skip_files.includes(path27.basename(filePath))) return true;
16658
17536
  try {
16659
- const stat2 = fs26.statSync(filePath);
17537
+ const stat2 = fs27.statSync(filePath);
16660
17538
  if (stat2.size > cfg.large_file_skip_kb * 1024) return true;
16661
17539
  } catch {
16662
17540
  }
@@ -16698,7 +17576,8 @@ function writeParseResult(filePath, content, result, dbPath) {
16698
17576
  });
16699
17577
  writeAll.immediate();
16700
17578
  }
16701
- function indexFileSync(filePath, dbPath = globalDbPath(), preReadBytes) {
17579
+ function indexFileSync(rawPath, dbPath = globalDbPath(), preReadBytes) {
17580
+ const filePath = canonicalizeIndexPath(rawPath);
16702
17581
  const ixCfg = loadConfig().indexing;
16703
17582
  if (ixCfg !== void 0 && isParseSkipEligible(filePath, ixCfg)) {
16704
17583
  const db = getDb(dbPath);
@@ -16712,7 +17591,7 @@ function indexFileSync(filePath, dbPath = globalDbPath(), preReadBytes) {
16712
17591
  raw = preReadBytes;
16713
17592
  } else {
16714
17593
  try {
16715
- raw = fs26.readFileSync(filePath);
17594
+ raw = fs27.readFileSync(filePath);
16716
17595
  } catch (err) {
16717
17596
  if (err.code === "ENOENT") return;
16718
17597
  throw err;
@@ -16743,14 +17622,32 @@ var UNAVAILABLE_EMBED_SHA_PREFIX = "unavailable:";
16743
17622
  function unavailableEmbedSha(sha) {
16744
17623
  return UNAVAILABLE_EMBED_SHA_PREFIX + sha;
16745
17624
  }
17625
+ function canonicalizeIndexPath(absPath) {
17626
+ if (!isCaseInsensitiveFs()) return absPath;
17627
+ let real;
17628
+ try {
17629
+ real = fs27.realpathSync.native(absPath);
17630
+ } catch {
17631
+ return absPath;
17632
+ }
17633
+ const cut = Math.max(absPath.lastIndexOf("/"), absPath.lastIndexOf("\\"));
17634
+ const base = absPath.slice(cut + 1);
17635
+ const realNorm = normalizePath(real);
17636
+ const realBase = path27.basename(realNorm);
17637
+ if (base === realBase) return absPath;
17638
+ if (foldPath(base) !== foldPath(realBase)) return absPath;
17639
+ const callerDir = cut < 0 ? normalizePath(path27.resolve(".")) : normalizePath(absPath.slice(0, cut));
17640
+ if (foldPath(callerDir) !== foldPath(path27.dirname(realNorm))) return absPath;
17641
+ return absPath.slice(0, cut + 1) + realBase;
17642
+ }
16746
17643
  function indexedPathSpellingIsStale(storedPath, absPath) {
16747
17644
  if (!isCaseInsensitiveFs()) return false;
16748
17645
  const stored = normalizePath(storedPath);
16749
- const candidate = normalizePath(path26.resolve(absPath));
17646
+ const candidate = normalizePath(path27.resolve(absPath));
16750
17647
  if (foldPath(stored) !== foldPath(candidate)) return false;
16751
17648
  let real;
16752
17649
  try {
16753
- real = normalizePath(fs26.realpathSync.native(absPath));
17650
+ real = normalizePath(fs27.realpathSync.native(absPath));
16754
17651
  } catch {
16755
17652
  return false;
16756
17653
  }
@@ -16779,7 +17676,8 @@ function isEmbedFresh(storedEmbedSha, sha, embeddingsEnabled, depsAvailable) {
16779
17676
  if (!depsAvailable && storedEmbedSha === unavailableEmbedSha(sha)) return true;
16780
17677
  return false;
16781
17678
  }
16782
- async function indexFileEmbeddings(filePath, dbPath = globalDbPath(), sha, onError) {
17679
+ async function indexFileEmbeddings(rawPath, dbPath = globalDbPath(), sha, onError) {
17680
+ const filePath = canonicalizeIndexPath(rawPath);
16783
17681
  const ixCfg = loadConfig().indexing;
16784
17682
  if (!ixCfg.embeddings_enabled) {
16785
17683
  stampEmbedSha(getDb(dbPath), filePath, sha, disabledEmbedSha);
@@ -16816,7 +17714,7 @@ async function indexFileEmbeddings(filePath, dbPath = globalDbPath(), sha, onErr
16816
17714
  }
16817
17715
  let content;
16818
17716
  try {
16819
- content = decodeSource(await fs26.promises.readFile(filePath));
17717
+ content = decodeSource(await fs27.promises.readFile(filePath));
16820
17718
  } catch {
16821
17719
  return;
16822
17720
  }
@@ -16862,15 +17760,15 @@ function stampEmbedSha(db, filePath, sha, makeValue) {
16862
17760
  }
16863
17761
  function safeMtime(filePath) {
16864
17762
  try {
16865
- return fs26.statSync(filePath).mtimeMs / 1e3;
17763
+ return fs27.statSync(filePath).mtimeMs / 1e3;
16866
17764
  } catch {
16867
17765
  return 0;
16868
17766
  }
16869
17767
  }
16870
17768
 
16871
17769
  // src/index_prune.ts
16872
- import * as fs27 from "node:fs";
16873
- import * as path27 from "node:path";
17770
+ import * as fs28 from "node:fs";
17771
+ import * as path28 from "node:path";
16874
17772
  function removeFileFromIndex(db, filePath) {
16875
17773
  const tx = db.transaction(() => {
16876
17774
  deleteFileRows(db, filePath);
@@ -16907,7 +17805,7 @@ function findDeletablePaths(rootPrefix, dbPath) {
16907
17805
  for (const p of foldedPathsUnderRoot(rootPrefix, dbPath)) {
16908
17806
  let stillExists;
16909
17807
  try {
16910
- const st = fs27.statSync(p, { throwIfNoEntry: false });
17808
+ const st = fs28.statSync(p, { throwIfNoEntry: false });
16911
17809
  stillExists = st !== void 0 && st.isFile();
16912
17810
  } catch {
16913
17811
  continue;
@@ -16935,7 +17833,7 @@ function removeDeletedFilesBestEffort(db, paths) {
16935
17833
  for (const p of paths) {
16936
17834
  let gone;
16937
17835
  try {
16938
- const st = fs27.statSync(p, { throwIfNoEntry: false });
17836
+ const st = fs28.statSync(p, { throwIfNoEntry: false });
16939
17837
  gone = st === void 0 || !st.isFile();
16940
17838
  } catch {
16941
17839
  continue;
@@ -17002,7 +17900,7 @@ function pruneOrphanedChunks(dbPath = globalDbPath()) {
17002
17900
  return removed;
17003
17901
  }
17004
17902
  function recordKnownRoot(filePath, dbPath = globalDbPath()) {
17005
- const project = findProject(path27.dirname(filePath));
17903
+ const project = findProject(path28.dirname(filePath));
17006
17904
  if (project === null || isTooShallowToPrune(project.root)) return;
17007
17905
  const db = getDb(dbPath);
17008
17906
  db.prepare(
@@ -17025,7 +17923,7 @@ function sweepKnownRoots(dbPath = globalDbPath(), opts) {
17025
17923
  if (isTooShallowToPrune(root)) continue;
17026
17924
  let reachable;
17027
17925
  try {
17028
- reachable = fs27.statSync(root, { throwIfNoEntry: false })?.isDirectory() === true;
17926
+ reachable = fs28.statSync(root, { throwIfNoEntry: false })?.isDirectory() === true;
17029
17927
  } catch {
17030
17928
  reachable = false;
17031
17929
  }
@@ -17060,19 +17958,19 @@ function sweepKnownRoots(dbPath = globalDbPath(), opts) {
17060
17958
  }
17061
17959
  var KNOWN_ROOT_RECORD_MIN_INTERVAL_MS = 60 * 60 * 1e3;
17062
17960
  function knownRootRecordMarkerPath(dir, filePath) {
17063
- return path27.join(dir, `known-root-record-${shortFingerprint(path27.dirname(filePath))}.marker`);
17961
+ return path28.join(dir, `known-root-record-${shortFingerprint(path28.dirname(filePath))}.marker`);
17064
17962
  }
17065
17963
  var KNOWN_ROOT_MARKER_PREFIX = "known-root-record-";
17066
17964
  function sweepExpiredKnownRootMarkers(dir = dataDir()) {
17067
17965
  let removed = 0;
17068
17966
  try {
17069
17967
  const cutoff = Date.now() - KNOWN_ROOT_RECORD_MIN_INTERVAL_MS;
17070
- for (const file of fs27.readdirSync(dir)) {
17968
+ for (const file of fs28.readdirSync(dir)) {
17071
17969
  if (!file.startsWith(KNOWN_ROOT_MARKER_PREFIX) || !file.endsWith(".marker")) continue;
17072
- const full = path27.join(dir, file);
17970
+ const full = path28.join(dir, file);
17073
17971
  try {
17074
- if (fs27.statSync(full).mtimeMs < cutoff) {
17075
- fs27.unlinkSync(full);
17972
+ if (fs28.statSync(full).mtimeMs < cutoff) {
17973
+ fs28.unlinkSync(full);
17076
17974
  removed += 1;
17077
17975
  }
17078
17976
  } catch {
@@ -17085,13 +17983,13 @@ function sweepExpiredKnownRootMarkers(dir = dataDir()) {
17085
17983
  function recordKnownRootThrottled(filePath, dir = dataDir(), dbPath = globalDbPath()) {
17086
17984
  const markerPath = knownRootRecordMarkerPath(dir, filePath);
17087
17985
  try {
17088
- const stat2 = fs27.statSync(markerPath);
17986
+ const stat2 = fs28.statSync(markerPath);
17089
17987
  if (Date.now() - stat2.mtimeMs < KNOWN_ROOT_RECORD_MIN_INTERVAL_MS) return;
17090
17988
  } catch {
17091
17989
  }
17092
17990
  try {
17093
17991
  ensureDirSync(dir);
17094
- fs27.writeFileSync(markerPath, "");
17992
+ fs28.writeFileSync(markerPath, "");
17095
17993
  } catch {
17096
17994
  }
17097
17995
  recordKnownRoot(filePath, dbPath);
@@ -17099,8 +17997,8 @@ function recordKnownRootThrottled(filePath, dir = dataDir(), dbPath = globalDbPa
17099
17997
 
17100
17998
  // src/worker.ts
17101
17999
  import { spawn as spawn2 } from "node:child_process";
17102
- import * as fs28 from "node:fs";
17103
- import * as path28 from "node:path";
18000
+ import * as fs29 from "node:fs";
18001
+ import * as path29 from "node:path";
17104
18002
  import { fileURLToPath as fileURLToPath2 } from "node:url";
17105
18003
  var DEFAULT_POLL_INTERVAL_MS = 2e3;
17106
18004
  function resolvePollIntervalMs(explicit) {
@@ -17120,7 +18018,7 @@ var unclearedDrainingSnapshots = /* @__PURE__ */ new Map();
17120
18018
  function drainingSnapshotStamp(file, content) {
17121
18019
  let identity = "unknown";
17122
18020
  try {
17123
- const stat2 = fs28.statSync(file);
18021
+ const stat2 = fs29.statSync(file);
17124
18022
  identity = `${stat2.mtimeMs}:${stat2.size}:${stat2.ino}`;
17125
18023
  } catch {
17126
18024
  }
@@ -17129,15 +18027,15 @@ function drainingSnapshotStamp(file, content) {
17129
18027
  var DRAINING_READ_ATTEMPTS = 5;
17130
18028
  var DRAINING_READ_RETRY_DELAY_MS = 50;
17131
18029
  function listDrainingFiles(queuePath) {
17132
- const dir = path28.dirname(queuePath);
17133
- const base = `${path28.basename(queuePath)}.draining`;
18030
+ const dir = path29.dirname(queuePath);
18031
+ const base = `${path29.basename(queuePath)}.draining`;
17134
18032
  let entries;
17135
18033
  try {
17136
- entries = fs28.readdirSync(dir);
18034
+ entries = fs29.readdirSync(dir);
17137
18035
  } catch {
17138
18036
  return [];
17139
18037
  }
17140
- return entries.filter((name) => name === base || name.startsWith(`${base}.alt-`) && !name.includes(".corrupt-")).sort().map((name) => path28.join(dir, name));
18038
+ return entries.filter((name) => name === base || name.startsWith(`${base}.alt-`) && !name.includes(".corrupt-")).sort().map((name) => path29.join(dir, name));
17141
18039
  }
17142
18040
  var MAX_TRANSIENT_RETRIES = 5;
17143
18041
  function bumpRetryCount(dbPath, absPath) {
@@ -17165,17 +18063,17 @@ function clearRetryCount(dbPath, absPath) {
17165
18063
  }
17166
18064
  }
17167
18065
  function dirtyQueuePathFor(dir) {
17168
- return path28.join(dir, "queue", "dirty.txt");
18066
+ return path29.join(dir, "queue", "dirty.txt");
17169
18067
  }
17170
18068
  function drainHeartbeatPathFor(dir) {
17171
- return path28.join(dir, "queue", "drain-heartbeat");
18069
+ return path29.join(dir, "queue", "drain-heartbeat");
17172
18070
  }
17173
18071
  function writeDrainHeartbeat(dir, force = false) {
17174
18072
  const now = Date.now();
17175
18073
  if (!force && now - (heartbeatWriteTimes.get(dir) ?? 0) < WORKER_HEARTBEAT_REFRESH_MS) return;
17176
18074
  try {
17177
- ensureDirSync(path28.dirname(drainHeartbeatPathFor(dir)));
17178
- fs28.writeFileSync(drainHeartbeatPathFor(dir), `${process.pid}
18075
+ ensureDirSync(path29.dirname(drainHeartbeatPathFor(dir)));
18076
+ fs29.writeFileSync(drainHeartbeatPathFor(dir), `${process.pid}
17179
18077
  `);
17180
18078
  heartbeatWriteTimes.set(dir, now);
17181
18079
  } catch {
@@ -17184,15 +18082,15 @@ function writeDrainHeartbeat(dir, force = false) {
17184
18082
  function hasFreshWorkerHeartbeat(dir, pid) {
17185
18083
  try {
17186
18084
  const heartbeatPath = drainHeartbeatPathFor(dir);
17187
- if (Date.now() - fs28.statSync(heartbeatPath).mtimeMs > WORKER_HEARTBEAT_STALE_MS) return false;
17188
- return fs28.readFileSync(heartbeatPath, "utf8").trim() === String(pid);
18085
+ if (Date.now() - fs29.statSync(heartbeatPath).mtimeMs > WORKER_HEARTBEAT_STALE_MS) return false;
18086
+ return fs29.readFileSync(heartbeatPath, "utf8").trim() === String(pid);
17189
18087
  } catch {
17190
18088
  return false;
17191
18089
  }
17192
18090
  }
17193
18091
  function pidFileIsWithinStartupGrace(dir) {
17194
18092
  try {
17195
- return Date.now() - fs28.statSync(workerPidPath(dir)).mtimeMs < WORKER_STARTUP_GRACE_MS;
18093
+ return Date.now() - fs29.statSync(workerPidPath(dir)).mtimeMs < WORKER_STARTUP_GRACE_MS;
17196
18094
  } catch {
17197
18095
  return false;
17198
18096
  }
@@ -17226,28 +18124,28 @@ function parseDirtyQueueLines(raw) {
17226
18124
  return out;
17227
18125
  }
17228
18126
  function workerPidPath(dir = dataDir()) {
17229
- return path28.join(dir, "worker.pid");
18127
+ return path29.join(dir, "worker.pid");
17230
18128
  }
17231
18129
  function getDirtyPathsFor(dir) {
17232
18130
  let raw;
17233
18131
  try {
17234
- raw = fs28.readFileSync(dirtyQueuePathFor(dir), "utf8");
18132
+ raw = fs29.readFileSync(dirtyQueuePathFor(dir), "utf8");
17235
18133
  } catch {
17236
18134
  return [];
17237
18135
  }
17238
18136
  return parseDirtyQueueLines(raw);
17239
18137
  }
17240
18138
  function workerErrorLogPath(dir) {
17241
- return path28.join(dir, "worker-errors.log");
18139
+ return path29.join(dir, "worker-errors.log");
17242
18140
  }
17243
18141
  var WORKER_ERROR_LOG_MAX_BYTES = 5 * 1024 * 1024;
17244
18142
  var CORRUPT_QUARANTINE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
17245
18143
  function cleanupWorkerStateFiles(dir) {
17246
18144
  try {
17247
18145
  const logPath = workerErrorLogPath(dir);
17248
- const stat2 = fs28.statSync(logPath);
18146
+ const stat2 = fs29.statSync(logPath);
17249
18147
  if (stat2.size > WORKER_ERROR_LOG_MAX_BYTES) {
17250
- fs28.writeFileSync(
18148
+ fs29.writeFileSync(
17251
18149
  logPath,
17252
18150
  `${(/* @__PURE__ */ new Date()).toISOString()} worker-errors.log rotated (exceeded ${WORKER_ERROR_LOG_MAX_BYTES} bytes)
17253
18151
  `
@@ -17256,13 +18154,13 @@ function cleanupWorkerStateFiles(dir) {
17256
18154
  } catch {
17257
18155
  }
17258
18156
  try {
17259
- const queueDir = path28.dirname(dirtyQueuePathFor(dir));
18157
+ const queueDir = path29.dirname(dirtyQueuePathFor(dir));
17260
18158
  const cutoff = Date.now() - CORRUPT_QUARANTINE_MAX_AGE_MS;
17261
- for (const file of fs28.readdirSync(queueDir)) {
18159
+ for (const file of fs29.readdirSync(queueDir)) {
17262
18160
  if (!file.includes(".corrupt-")) continue;
17263
- const full = path28.join(queueDir, file);
18161
+ const full = path29.join(queueDir, file);
17264
18162
  try {
17265
- if (fs28.statSync(full).mtimeMs < cutoff) fs28.unlinkSync(full);
18163
+ if (fs29.statSync(full).mtimeMs < cutoff) fs29.unlinkSync(full);
17266
18164
  } catch {
17267
18165
  }
17268
18166
  }
@@ -17293,7 +18191,7 @@ registerReset(() => {
17293
18191
  function embedFileSerialized(absPath, dbPath, sha) {
17294
18192
  const key = foldPath(absPath);
17295
18193
  const prior = inFlightEmbeddings.get(key);
17296
- const dir = path28.dirname(dbPath);
18194
+ const dir = path29.dirname(dbPath);
17297
18195
  const onEmbedError = (err) => {
17298
18196
  const message = extractErrorMessage(err);
17299
18197
  appendWorkerErrorLog(dir, `${(/* @__PURE__ */ new Date()).toISOString()} indexFileEmbeddings failed for ${absPath}: ${message}
@@ -17327,7 +18225,7 @@ function embedFileSerialized(absPath, dbPath, sha) {
17327
18225
  }
17328
18226
  function appendWorkerErrorLog(dir, line) {
17329
18227
  try {
17330
- fs28.appendFileSync(workerErrorLogPath(dir), line);
18228
+ fs29.appendFileSync(workerErrorLogPath(dir), line);
17331
18229
  } catch {
17332
18230
  }
17333
18231
  }
@@ -17344,7 +18242,7 @@ function logTransientReadFailure(dir, absPath) {
17344
18242
  );
17345
18243
  }
17346
18244
  function bumpAndCheckRetry(dir, absPath) {
17347
- const dbPath = path28.join(dir, "global.db");
18245
+ const dbPath = path29.join(dir, "global.db");
17348
18246
  const attempts = bumpRetryCount(dbPath, absPath);
17349
18247
  if (attempts > MAX_TRANSIENT_RETRIES) {
17350
18248
  if (attempts === MAX_TRANSIENT_RETRIES + 1) {
@@ -17361,14 +18259,14 @@ function bumpAndCheckRetry(dir, absPath) {
17361
18259
  function appendToDirtyQueue(dir, absPath) {
17362
18260
  const queuePath = dirtyQueuePathFor(dir);
17363
18261
  try {
17364
- ensureDirSync(path28.dirname(queuePath));
18262
+ ensureDirSync(path29.dirname(queuePath));
17365
18263
  let leadingNewline = "";
17366
18264
  try {
17367
- const existing = fs28.readFileSync(queuePath, "utf8");
18265
+ const existing = fs29.readFileSync(queuePath, "utf8");
17368
18266
  if (existing.length > 0 && !existing.endsWith("\n")) leadingNewline = "\n";
17369
18267
  } catch {
17370
18268
  }
17371
- fs28.appendFileSync(queuePath, `${leadingNewline}${encodeDirtyQueueLine(absPath)}
18269
+ fs29.appendFileSync(queuePath, `${leadingNewline}${encodeDirtyQueueLine(absPath)}
17372
18270
  `);
17373
18271
  } catch {
17374
18272
  }
@@ -17377,7 +18275,7 @@ function requeueDirtyPath(dir, absPath) {
17377
18275
  if (bumpAndCheckRetry(dir, absPath)) appendToDirtyQueue(dir, absPath);
17378
18276
  }
17379
18277
  function makeIndexer(dbPath) {
17380
- const dir = path28.dirname(dbPath);
18278
+ const dir = path29.dirname(dbPath);
17381
18279
  return (absPath, sha) => {
17382
18280
  try {
17383
18281
  const ixCfgForSkip = loadConfig().indexing;
@@ -17429,16 +18327,16 @@ function processDirtyBatch(paths, index = makeIndexer(globalDbPath()), remove =
17429
18327
  requeue(dir, p);
17430
18328
  continue;
17431
18329
  }
17432
- clearRetryCount(path28.join(dir, "global.db"), p);
18330
+ clearRetryCount(path29.join(dir, "global.db"), p);
17433
18331
  try {
17434
- const dirname17 = path28.dirname(p);
18332
+ const dirname19 = path29.dirname(p);
17435
18333
  let root;
17436
- if (projectRootCache.has(dirname17)) {
17437
- root = projectRootCache.get(dirname17) ?? null;
18334
+ if (projectRootCache.has(dirname19)) {
18335
+ root = projectRootCache.get(dirname19) ?? null;
17438
18336
  } else {
17439
- const project = findProject(dirname17);
18337
+ const project = findProject(dirname19);
17440
18338
  root = project?.root ?? null;
17441
- projectRootCache.set(dirname17, root);
18339
+ projectRootCache.set(dirname19, root);
17442
18340
  }
17443
18341
  if (root) lastKnownProjectRoots.set(dir, root);
17444
18342
  } catch {
@@ -17457,7 +18355,7 @@ function sleepSyncMs(ms) {
17457
18355
  function drainOnce(dir, index, remove) {
17458
18356
  const queuePath = dirtyQueuePathFor(dir);
17459
18357
  const draining = `${queuePath}.draining`;
17460
- const dbPath = path28.join(dir, "global.db");
18358
+ const dbPath = path29.join(dir, "global.db");
17461
18359
  const indexFn = index ?? makeIndexer(dbPath);
17462
18360
  const removeFn = remove ?? makeRemover(dbPath);
17463
18361
  let processed = 0;
@@ -17474,7 +18372,7 @@ function drainOnce(dir, index, remove) {
17474
18372
  let drainingContent = null;
17475
18373
  for (let attempt = 0; attempt < DRAINING_READ_ATTEMPTS; attempt++) {
17476
18374
  try {
17477
- drainingContent = fs28.readFileSync(drainingFile, "utf8");
18375
+ drainingContent = fs29.readFileSync(drainingFile, "utf8");
17478
18376
  break;
17479
18377
  } catch {
17480
18378
  if (attempt < DRAINING_READ_ATTEMPTS - 1) sleepSyncMs(DRAINING_READ_RETRY_DELAY_MS);
@@ -17482,7 +18380,7 @@ function drainOnce(dir, index, remove) {
17482
18380
  }
17483
18381
  if (drainingContent === null) {
17484
18382
  try {
17485
- fs28.renameSync(drainingFile, `${drainingFile}.corrupt-${Date.now()}`);
18383
+ fs29.renameSync(drainingFile, `${drainingFile}.corrupt-${Date.now()}`);
17486
18384
  unclearedDrainingSnapshots.delete(drainingFile);
17487
18385
  } catch {
17488
18386
  }
@@ -17492,23 +18390,23 @@ function drainOnce(dir, index, remove) {
17492
18390
  processed += processDirtyBatch(parseDirtyQueueLines(drainingContent), indexFn, removeFn, dir, requeueFn);
17493
18391
  }
17494
18392
  try {
17495
- fs28.rmSync(drainingFile, { force: true });
18393
+ fs29.rmSync(drainingFile, { force: true });
17496
18394
  unclearedDrainingSnapshots.delete(drainingFile);
17497
18395
  } catch {
17498
18396
  try {
17499
- fs28.renameSync(drainingFile, `${drainingFile}.corrupt-${Date.now()}`);
18397
+ fs29.renameSync(drainingFile, `${drainingFile}.corrupt-${Date.now()}`);
17500
18398
  unclearedDrainingSnapshots.delete(drainingFile);
17501
18399
  } catch {
17502
18400
  unclearedDrainingSnapshots.set(drainingFile, drainingSnapshotStamp(drainingFile, drainingContent));
17503
18401
  }
17504
18402
  }
17505
18403
  }
17506
- if (fs28.existsSync(queuePath)) {
17507
- const claimTarget = fs28.existsSync(draining) ? `${draining}.alt-${Date.now()}` : draining;
18404
+ if (fs29.existsSync(queuePath)) {
18405
+ const claimTarget = fs29.existsSync(draining) ? `${draining}.alt-${Date.now()}` : draining;
17508
18406
  let claimed = false;
17509
18407
  for (let attempt = 0; attempt < 5; attempt++) {
17510
18408
  try {
17511
- fs28.renameSync(queuePath, claimTarget);
18409
+ fs29.renameSync(queuePath, claimTarget);
17512
18410
  claimed = true;
17513
18411
  break;
17514
18412
  } catch {
@@ -17519,14 +18417,14 @@ function drainOnce(dir, index, remove) {
17519
18417
  let claimedContent = "";
17520
18418
  let readOk = false;
17521
18419
  try {
17522
- claimedContent = fs28.readFileSync(claimTarget, "utf8");
18420
+ claimedContent = fs29.readFileSync(claimTarget, "utf8");
17523
18421
  readOk = true;
17524
18422
  } catch {
17525
18423
  }
17526
18424
  if (readOk) {
17527
18425
  processed += processDirtyBatch(parseDirtyQueueLines(claimedContent), indexFn, removeFn, dir, requeueFn);
17528
18426
  try {
17529
- const recheck = fs28.readFileSync(claimTarget, "utf8");
18427
+ const recheck = fs29.readFileSync(claimTarget, "utf8");
17530
18428
  if (recheck !== claimedContent) {
17531
18429
  const extra = recheck.startsWith(claimedContent) ? recheck.slice(claimedContent.length) : recheck;
17532
18430
  for (const p of parseDirtyQueueLines(extra)) appendToDirtyQueue(dir, p);
@@ -17534,10 +18432,10 @@ function drainOnce(dir, index, remove) {
17534
18432
  } catch {
17535
18433
  }
17536
18434
  try {
17537
- fs28.rmSync(claimTarget, { force: true });
18435
+ fs29.rmSync(claimTarget, { force: true });
17538
18436
  } catch {
17539
18437
  try {
17540
- fs28.renameSync(claimTarget, `${claimTarget}.corrupt-${Date.now()}`);
18438
+ fs29.renameSync(claimTarget, `${claimTarget}.corrupt-${Date.now()}`);
17541
18439
  } catch {
17542
18440
  unclearedDrainingSnapshots.set(claimTarget, drainingSnapshotStamp(claimTarget, claimedContent));
17543
18441
  }
@@ -17570,7 +18468,7 @@ function pidAlive(pid) {
17570
18468
  }
17571
18469
  function readPidFile(dir) {
17572
18470
  try {
17573
- const raw = fs28.readFileSync(workerPidPath(dir), "utf8").trim();
18471
+ const raw = fs29.readFileSync(workerPidPath(dir), "utf8").trim();
17574
18472
  if (!/^\d+$/.test(raw)) return null;
17575
18473
  return parseInt(raw, 10);
17576
18474
  } catch {
@@ -17583,20 +18481,20 @@ function isWorkerRunning(dir = dataDir()) {
17583
18481
  return pidAlive(pid) && hasFreshWorkerHeartbeat(dir, pid);
17584
18482
  }
17585
18483
  function workerHealthCheckMarkerPath(dir) {
17586
- return path28.join(dir, "worker-healthcheck.marker");
18484
+ return path29.join(dir, "worker-healthcheck.marker");
17587
18485
  }
17588
18486
  var WORKER_HEALTHCHECK_MIN_INTERVAL_MS = 5 * 60 * 1e3;
17589
18487
  function ensureWorkerAlive(dir = dataDir()) {
17590
18488
  if (process.env["TOKEN_GOAT_NO_WORKER_SPAWN"] === "1") return;
17591
18489
  const markerPath = workerHealthCheckMarkerPath(dir);
17592
18490
  try {
17593
- const stat2 = fs28.statSync(markerPath);
18491
+ const stat2 = fs29.statSync(markerPath);
17594
18492
  if (Date.now() - stat2.mtimeMs < WORKER_HEALTHCHECK_MIN_INTERVAL_MS) return;
17595
18493
  } catch {
17596
18494
  }
17597
18495
  try {
17598
18496
  ensureDirSync(dir);
17599
- fs28.writeFileSync(markerPath, "");
18497
+ fs29.writeFileSync(markerPath, "");
17600
18498
  } catch {
17601
18499
  }
17602
18500
  if (isWorkerRunning(dir)) return;
@@ -17626,7 +18524,7 @@ function stopWorker(dir = dataDir()) {
17626
18524
  }
17627
18525
  if (readPidFile(dir) === pid) {
17628
18526
  try {
17629
- fs28.rmSync(workerPidPath(dir), { force: true });
18527
+ fs29.rmSync(workerPidPath(dir), { force: true });
17630
18528
  } catch {
17631
18529
  }
17632
18530
  }
@@ -17641,7 +18539,7 @@ var WorkerAlreadyRunningError = class extends Error {
17641
18539
  function claimWorkerPidFile(dir, pid) {
17642
18540
  const pidPath = workerPidPath(dir);
17643
18541
  try {
17644
- fs28.writeFileSync(pidPath, `${pid}
18542
+ fs29.writeFileSync(pidPath, `${pid}
17645
18543
  `, { flag: "wx" });
17646
18544
  return true;
17647
18545
  } catch (e) {
@@ -17658,11 +18556,11 @@ function claimWorkerPidFile(dir, pid) {
17658
18556
  }
17659
18557
  }
17660
18558
  try {
17661
- fs28.rmSync(pidPath, { force: true });
18559
+ fs29.rmSync(pidPath, { force: true });
17662
18560
  } catch {
17663
18561
  }
17664
18562
  try {
17665
- fs28.writeFileSync(pidPath, `${pid}
18563
+ fs29.writeFileSync(pidPath, `${pid}
17666
18564
  `, { flag: "wx" });
17667
18565
  return true;
17668
18566
  } catch (e2) {
@@ -17672,9 +18570,9 @@ function claimWorkerPidFile(dir, pid) {
17672
18570
  }
17673
18571
  function daemonEntryScript() {
17674
18572
  const self = fileURLToPath2(import.meta.url);
17675
- const launcher = path28.join(path28.dirname(self), "token-goat.mjs");
18573
+ const launcher = path29.join(path29.dirname(self), "token-goat.mjs");
17676
18574
  try {
17677
- if (fs28.existsSync(launcher)) return launcher;
18575
+ if (fs29.existsSync(launcher)) return launcher;
17678
18576
  } catch {
17679
18577
  }
17680
18578
  return self;
@@ -17685,7 +18583,7 @@ function startDetachedWorker(opts) {
17685
18583
  try {
17686
18584
  ensureDirSync(dir);
17687
18585
  } catch (e) {
17688
- if (e.code !== "EEXIST" || !fs28.existsSync(dir)) throw e;
18586
+ if (e.code !== "EEXIST" || !fs29.existsSync(dir)) throw e;
17689
18587
  }
17690
18588
  const child = spawn2(
17691
18589
  process.execPath,
@@ -17722,7 +18620,7 @@ async function runWorkerLoop(dir, pollIntervalMs, shouldStop = () => false) {
17722
18620
  let lastKnownRootsSweepMs = 0;
17723
18621
  let ownedPidFile = false;
17724
18622
  while (!shouldStop()) {
17725
- if (!fs28.existsSync(dir)) break;
18623
+ if (!fs29.existsSync(dir)) break;
17726
18624
  const pidOwner = readPidFile(dir);
17727
18625
  if (pidOwner === process.pid) ownedPidFile = true;
17728
18626
  else if (ownedPidFile && pidOwner !== null) break;
@@ -17747,7 +18645,7 @@ async function runWorkerLoop(dir, pollIntervalMs, shouldStop = () => false) {
17747
18645
  }
17748
18646
  if (Date.now() - lastKnownRootsSweepMs >= KNOWN_ROOTS_SWEEP_INTERVAL_MS) {
17749
18647
  try {
17750
- const result = sweepKnownRoots(path28.join(dir, "global.db"));
18648
+ const result = sweepKnownRoots(path29.join(dir, "global.db"));
17751
18649
  if (result.flaggedRoots.length > 0) {
17752
18650
  appendWorkerErrorLog(
17753
18651
  dir,
@@ -17770,7 +18668,7 @@ function runDetachedWorkerDaemon() {
17770
18668
  process.on("exit", () => {
17771
18669
  if (readPidFile(dir) === process.pid) {
17772
18670
  try {
17773
- fs28.rmSync(workerPidPath(dir), { force: true });
18671
+ fs29.rmSync(workerPidPath(dir), { force: true });
17774
18672
  } catch {
17775
18673
  }
17776
18674
  }
@@ -17780,26 +18678,26 @@ function runDetachedWorkerDaemon() {
17780
18678
  }
17781
18679
 
17782
18680
  // src/hooks_index.ts
17783
- import * as fs29 from "node:fs";
17784
- import * as path29 from "node:path";
18681
+ import * as fs30 from "node:fs";
18682
+ import * as path30 from "node:path";
17785
18683
  function dirtyQueuePath() {
17786
- return path29.join(dataDir(), "queue", "dirty.txt");
18684
+ return path30.join(dataDir(), "queue", "dirty.txt");
17787
18685
  }
17788
18686
  function appendDirtyPath(normalizedPath2) {
17789
18687
  const queuePath = dirtyQueuePath();
17790
- const dir = path29.dirname(queuePath);
18688
+ const dir = path30.dirname(queuePath);
17791
18689
  try {
17792
18690
  ensureDirSync(dir);
17793
18691
  } catch (e) {
17794
- if (e.code !== "EEXIST" || !fs29.existsSync(dir)) throw e;
18692
+ if (e.code !== "EEXIST" || !fs30.existsSync(dir)) throw e;
17795
18693
  }
17796
18694
  let leadingNewline = "";
17797
18695
  try {
17798
- const existing = fs29.readFileSync(queuePath, "utf8");
18696
+ const existing = fs30.readFileSync(queuePath, "utf8");
17799
18697
  if (existing.length > 0 && !existing.endsWith("\n")) leadingNewline = "\n";
17800
18698
  } catch {
17801
18699
  }
17802
- fs29.appendFileSync(queuePath, `${leadingNewline}${encodeDirtyQueueLine(normalizedPath2)}
18700
+ fs30.appendFileSync(queuePath, `${leadingNewline}${encodeDirtyQueueLine(normalizedPath2)}
17803
18701
  `);
17804
18702
  }
17805
18703
  function enqueueDirtyPathSafe(filePath, opts) {
@@ -17812,7 +18710,7 @@ function getDirtyPaths() {
17812
18710
  const queuePath = dirtyQueuePath();
17813
18711
  let raw;
17814
18712
  try {
17815
- raw = fs29.readFileSync(queuePath, "utf8");
18713
+ raw = fs30.readFileSync(queuePath, "utf8");
17816
18714
  } catch {
17817
18715
  return [];
17818
18716
  }
@@ -17821,9 +18719,9 @@ function getDirtyPaths() {
17821
18719
  function preCompactIndexHandler(_event) {
17822
18720
  const paths = getDirtyPaths();
17823
18721
  if (paths.length > 0) {
17824
- const sidecar = path29.join(dataDir(), "queue", "pending.txt");
18722
+ const sidecar = path30.join(dataDir(), "queue", "pending.txt");
17825
18723
  try {
17826
- ensureDirSync(path29.dirname(sidecar));
18724
+ ensureDirSync(path30.dirname(sidecar));
17827
18725
  atomicWriteBytes(sidecar, Buffer.from(`${paths.join("\n")}
17828
18726
  `, "utf8"));
17829
18727
  } catch {
@@ -18017,6 +18915,8 @@ export {
18017
18915
  getGlobMatchCount,
18018
18916
  setLastTabContext,
18019
18917
  getLastTabContext,
18918
+ hasSeenImage,
18919
+ recordSeenImage,
18020
18920
  recordOutstandingAgentSpawn,
18021
18921
  getOutstandingAgentSpawns,
18022
18922
  removeOutstandingAgentSpawn,
@@ -18040,6 +18940,8 @@ export {
18040
18940
  installCodex,
18041
18941
  uninstallCodex,
18042
18942
  isCodexInstalled,
18943
+ copilotCliUserRoot,
18944
+ copilotCliMcpToolsDir,
18043
18945
  copilotCliConfigPath,
18044
18946
  copilotCliScriptPath,
18045
18947
  installCopilotCli,
@@ -18063,11 +18965,13 @@ export {
18063
18965
  loadBlob,
18064
18966
  listBlobs,
18065
18967
  pruneBlobs,
18968
+ estimateTokensFromLength,
18066
18969
  estimateTokens,
18067
18970
  trimToBudget,
18068
18971
  capJsonRows,
18069
18972
  SESSIONS_SUBDIR,
18070
18973
  AGENT_SALT_MARKER,
18974
+ sessionSidecarPath,
18071
18975
  listSiblingSessionStates,
18072
18976
  loadSessionState,
18073
18977
  saveSessionState,
@@ -18106,6 +19010,7 @@ export {
18106
19010
  formatShrinkSummary,
18107
19011
  isImagePath,
18108
19012
  probeImageMeta,
19013
+ imageQualifiesForShrink,
18109
19014
  shrinkImage,
18110
19015
  compactPathFor,
18111
19016
  isCompactFresh,
@@ -18116,6 +19021,8 @@ export {
18116
19021
  compactDoc,
18117
19022
  OVER_FETCH_FACTOR,
18118
19023
  MAX_OVER_FETCH,
19024
+ isAvailable,
19025
+ embeddingBackendLoadError,
18119
19026
  searchSemantic,
18120
19027
  mergeNearbyHits,
18121
19028
  embeddingsDepsAvailable,