token-goat 2.6.33 → 2.6.34

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 (24) hide show
  1. package/README.md +4 -0
  2. package/dist/{token-goat-chunk-WFRR2DBG.mjs → token-goat-chunk-7FWBSPCT.mjs} +446 -339
  3. package/dist/{token-goat-chunk-HPWPUBAK.mjs → token-goat-chunk-CKRATWCJ.mjs} +5 -5
  4. package/dist/{token-goat-chunk-TDYTBCT4.mjs → token-goat-chunk-CN7GUPX6.mjs} +328 -86
  5. package/dist/{token-goat-chunk-L6FVTDPD.mjs → token-goat-chunk-FRMEOIRU.mjs} +361 -23
  6. package/dist/{token-goat-chunk-RPOXYJWK.mjs → token-goat-chunk-GXBPEQR2.mjs} +70 -25
  7. package/dist/{token-goat-chunk-77CX3MCN.mjs → token-goat-chunk-I6EQH27G.mjs} +2 -2
  8. package/dist/{token-goat-chunk-FN6WLFIX.mjs → token-goat-chunk-IYYQOPY4.mjs} +801 -51
  9. package/dist/{token-goat-chunk-72R7M5H7.mjs → token-goat-chunk-L7QLEVO7.mjs} +3 -3
  10. package/dist/{token-goat-chunk-OHQ7RBLL.mjs → token-goat-chunk-OA3GFKP2.mjs} +2 -2
  11. package/dist/{token-goat-hook-chunk-HXHFLKEL.mjs → token-goat-chunk-WA2ER6TZ.mjs} +7 -2
  12. package/dist/{token-goat-chunk-RQQ4SY2P.mjs → token-goat-hook-chunk-2ISZ4AZR.mjs} +7 -2
  13. package/dist/{token-goat-hook-chunk-ZT47M3IA.mjs → token-goat-hook-chunk-6C5RN3CM.mjs} +2 -2
  14. package/dist/{token-goat-hook-chunk-UWV4MTYI.mjs → token-goat-hook-chunk-6QTWMSRO.mjs} +328 -86
  15. package/dist/{token-goat-hook-chunk-XBOWDNJB.mjs → token-goat-hook-chunk-ATEGFQAU.mjs} +5 -5
  16. package/dist/{token-goat-hook-chunk-QD2DX2U7.mjs → token-goat-hook-chunk-BU5OFDGQ.mjs} +361 -23
  17. package/dist/{token-goat-hook-chunk-E6AIPKNF.mjs → token-goat-hook-chunk-CNLMPHXT.mjs} +442 -335
  18. package/dist/{token-goat-hook-chunk-WBBHV6TZ.mjs → token-goat-hook-chunk-I5VM5PO5.mjs} +70 -25
  19. package/dist/{token-goat-hook-chunk-VDXQXYJK.mjs → token-goat-hook-chunk-KGLMNOGD.mjs} +801 -51
  20. package/dist/{token-goat-hook-chunk-EKGDLI4D.mjs → token-goat-hook-chunk-OQML5EMA.mjs} +3 -3
  21. package/dist/{token-goat-hook-chunk-26DLRODR.mjs → token-goat-hook-chunk-QTZ6YUMQ.mjs} +2 -2
  22. package/dist/token-goat-hook.mjs +5 -5
  23. package/dist/token-goat.core.mjs +5 -5
  24. package/package.json +8 -8
@@ -10,15 +10,20 @@ import {
10
10
  atomicWriteText,
11
11
  backupFile,
12
12
  buildLineIndex,
13
+ countContentLines,
13
14
  countNoun,
14
15
  dataDir,
16
+ decodeSource,
15
17
  detectHarness,
16
18
  detectLanguage,
19
+ displaySafePath,
20
+ displaySafeText,
17
21
  ensureDirSync,
18
22
  escapeRegExp,
19
23
  extractEnv,
20
24
  extractErrorMessage,
21
25
  extractIni,
26
+ fileIsAbsent,
22
27
  findHtmlHeadingMatches,
23
28
  findMatchingBraceEndLine,
24
29
  findProject,
@@ -82,7 +87,7 @@ import {
82
87
  withFileLock,
83
88
  writeIfDifferent,
84
89
  writeJsonSettings
85
- } from "./token-goat-chunk-WFRR2DBG.mjs";
90
+ } from "./token-goat-chunk-7FWBSPCT.mjs";
86
91
  import {
87
92
  registerReset
88
93
  } from "./token-goat-chunk-AO2QD2AG.mjs";
@@ -514,7 +519,7 @@ var ResizeableBuffer = class {
514
519
  }
515
520
  toString(encoding) {
516
521
  if (encoding) {
517
- return this.buf.slice(0, this.length).toString(encoding);
522
+ return this.buf.toString(encoding, 0, this.length);
518
523
  } else {
519
524
  return Uint8Array.prototype.slice.call(this.buf.slice(0, this.length));
520
525
  }
@@ -1766,14 +1771,19 @@ var transform = function(original_options = {}) {
1766
1771
  const obj = {};
1767
1772
  for (let i = 0, l = record.length; i < l; i++) {
1768
1773
  if (columns[i] === void 0 || columns[i].disabled) continue;
1769
- if (group_columns_by_name === true && obj[columns[i].name] !== void 0) {
1774
+ if (group_columns_by_name === true && Object.hasOwn(obj, columns[i].name)) {
1770
1775
  if (Array.isArray(obj[columns[i].name])) {
1771
1776
  obj[columns[i].name] = obj[columns[i].name].concat(record[i]);
1772
1777
  } else {
1773
1778
  obj[columns[i].name] = [obj[columns[i].name], record[i]];
1774
1779
  }
1775
1780
  } else {
1776
- obj[columns[i].name] = record[i];
1781
+ Object.defineProperty(obj, columns[i].name, {
1782
+ value: record[i],
1783
+ enumerable: true,
1784
+ writable: true,
1785
+ configurable: true
1786
+ });
1777
1787
  }
1778
1788
  }
1779
1789
  if (raw === true || info2 === true) {
@@ -2268,9 +2278,12 @@ function profileCsv(content, opts = {}) {
2268
2278
  }
2269
2279
  function formatCsvProfile(profiles) {
2270
2280
  return profiles.map((p) => {
2271
- const lines2 = [`${p.name} (${p.inferredType})`, ` nulls: ${p.nullCount} distinct: ${p.distinctCount}`];
2272
- if (p.min !== void 0) lines2.push(` range: ${p.min} .. ${p.max}`);
2273
- if (p.topValues !== void 0) lines2.push(` values: ${p.topValues.map((t) => `${t.value} (${t.count})`).join(", ")}`);
2281
+ const lines2 = [
2282
+ `${displaySafeText(p.name)} (${p.inferredType})`,
2283
+ ` nulls: ${p.nullCount} distinct: ${p.distinctCount}`
2284
+ ];
2285
+ if (p.min !== void 0) lines2.push(` range: ${displaySafeText(p.min)} .. ${displaySafeText(p.max ?? "")}`);
2286
+ if (p.topValues !== void 0) lines2.push(` values: ${p.topValues.map((t) => `${displaySafeText(t.value)} (${t.count})`).join(", ")}`);
2274
2287
  return lines2.join("\n");
2275
2288
  }).join("\n\n");
2276
2289
  }
@@ -3035,10 +3048,11 @@ function logHintEmission(category, sessionId, correlator, compensateSelfResolve
3035
3048
  try {
3036
3049
  const db = getDb(globalDbPath());
3037
3050
  const resolved = correlator === null ? 1 : 0;
3051
+ const actedOn = resolved === 1 && isSuppressionCategory(category) ? 1 : 0;
3038
3052
  const window = ACTED_ON_WINDOW + (compensateSelfResolve ? 1 : 0);
3039
3053
  db.prepare(
3040
3054
  `INSERT INTO hint_emissions (category, session_id, harness, correlator, emitted_at, resolved, acted_on, calls_remaining, bytes_emitted)
3041
- VALUES (@category, @sessionId, @harness, @correlator, @emittedAt, @resolved, 0, @callsRemaining, @bytesEmitted)`
3055
+ VALUES (@category, @sessionId, @harness, @correlator, @emittedAt, @resolved, @actedOn, @callsRemaining, @bytesEmitted)`
3042
3056
  ).run({
3043
3057
  category,
3044
3058
  sessionId,
@@ -3046,6 +3060,7 @@ function logHintEmission(category, sessionId, correlator, compensateSelfResolve
3046
3060
  correlator,
3047
3061
  emittedAt: Date.now(),
3048
3062
  resolved,
3063
+ actedOn,
3049
3064
  callsRemaining: correlator === null ? 0 : window,
3050
3065
  bytesEmitted
3051
3066
  });
@@ -3065,6 +3080,30 @@ function commandMentionsCorrelator(command, correlator) {
3065
3080
  }
3066
3081
  return false;
3067
3082
  }
3083
+ var SUPPRESSION_HINT_CATEGORIES = /* @__PURE__ */ new Set([
3084
+ "read_reread_dedup",
3085
+ "edit_reread_suggest"
3086
+ ]);
3087
+ function isSuppressionCategory(category) {
3088
+ return SUPPRESSION_HINT_CATEGORIES.has(category);
3089
+ }
3090
+ var EVENT_PATH_KEYS = ["file_path", "filePath", "notebook_path", "path"];
3091
+ function eventTargetText(event) {
3092
+ if (event.toolName === "Bash") {
3093
+ const c = event.toolInput["command"];
3094
+ return typeof c === "string" ? c : "";
3095
+ }
3096
+ for (const key of EVENT_PATH_KEYS) {
3097
+ const v = event.toolInput[key];
3098
+ if (typeof v === "string" && v !== "") return v;
3099
+ }
3100
+ return "";
3101
+ }
3102
+ function isDefiance(correlator, target) {
3103
+ if (target === "") return false;
3104
+ if (TOKEN_GOAT_INVOCATION_RE.test(target)) return false;
3105
+ return commandMentionsCorrelator(target, correlator);
3106
+ }
3068
3107
  function isActedOn(category, correlator, command) {
3069
3108
  if (!TOKEN_GOAT_INVOCATION_RE.test(command)) return false;
3070
3109
  if (category === "bash_recall") {
@@ -3076,10 +3115,29 @@ function resolvePendingHintsForEvent(event) {
3076
3115
  try {
3077
3116
  const db = getDb(globalDbPath());
3078
3117
  const command = event.toolName === "Bash" && typeof event.toolInput["command"] === "string" ? event.toolInput["command"] : "";
3118
+ const target = eventTargetText(event);
3079
3119
  const pending = db.prepare(`SELECT id, category, correlator, calls_remaining FROM hint_emissions WHERE session_id = ? AND resolved = 0`).all(event.sessionId);
3080
3120
  for (const row of pending) {
3081
3121
  if (!isHintCategory(row.category) || row.correlator === null) {
3082
- db.prepare(`UPDATE hint_emissions SET resolved = 1 WHERE id = ?`).run(row.id);
3122
+ const unobservable = isHintCategory(row.category) && isSuppressionCategory(row.category) ? 1 : 0;
3123
+ db.prepare(`UPDATE hint_emissions SET acted_on = ?, resolved = 1 WHERE id = ?`).run(unobservable, row.id);
3124
+ continue;
3125
+ }
3126
+ if (isSuppressionCategory(row.category)) {
3127
+ if (isDefiance(row.correlator, target)) {
3128
+ db.prepare(`UPDATE hint_emissions SET acted_on = 0, resolved = 1 WHERE id = ?`).run(row.id);
3129
+ continue;
3130
+ }
3131
+ if (command !== "" && isActedOn(row.category, row.correlator, command)) {
3132
+ db.prepare(`UPDATE hint_emissions SET acted_on = 1, resolved = 1 WHERE id = ?`).run(row.id);
3133
+ continue;
3134
+ }
3135
+ const left = row.calls_remaining - 1;
3136
+ if (left <= 0) {
3137
+ db.prepare(`UPDATE hint_emissions SET acted_on = 1, resolved = 1 WHERE id = ?`).run(row.id);
3138
+ } else {
3139
+ db.prepare(`UPDATE hint_emissions SET calls_remaining = ? WHERE id = ?`).run(left, row.id);
3140
+ }
3083
3141
  continue;
3084
3142
  }
3085
3143
  if (command !== "" && isActedOn(row.category, row.correlator, command)) {
@@ -3137,15 +3195,18 @@ function manualMarks(category) {
3137
3195
  }
3138
3196
  }
3139
3197
  function getHintStatsSummary() {
3198
+ const probeThresholds = loadConfig().hints.backoff_thresholds.filter((t) => t > 0);
3140
3199
  return HINT_CATEGORIES.map((category) => {
3141
3200
  const { emitted, actedOn, bytesEmitted, legacyEmissions } = categoryStats(category);
3142
3201
  const marks = manualMarks(category);
3202
+ const suppressed = shouldSuppress(category, "");
3143
3203
  return {
3144
3204
  category,
3145
3205
  emitted,
3146
3206
  actedOn: actedOn ?? 0,
3147
3207
  efficacyPct: emitted === 0 ? null : Math.round(1e3 * (actedOn ?? 0) / emitted) / 10,
3148
- suppressed: shouldSuppress(category, ""),
3208
+ suppressed,
3209
+ suppressionPermanent: suppressed && probeThresholds.length === 0,
3149
3210
  manualEffective: marks.effective,
3150
3211
  manualIneffective: marks.ineffective,
3151
3212
  bytesEmitted,
@@ -3709,12 +3770,12 @@ function buildGuidanceBody(fallbackToolClause, opts = {}) {
3709
3770
  '- searching for a *concept* rather than a literal string \u2192 `semantic "description"`',
3710
3771
  "- re-reading output you already captured \u2192 `bash-output`/`web-output`/`mcp-output` by ID",
3711
3772
  "- a directory listing or recursive wildcard walk to orient in an unfamiliar repo \u2192 `map --compact`",
3712
- "- pulling one value or subtree out of a JSON/YAML file (manifest, lockfile, spec, config) \u2192 `json-query file 'a.b.c'` / `yaml-query file 'a.b.c'`",
3773
+ "- pulling one value or subtree out of a JSON/YAML/XML file (manifest, lockfile, spec, config) \u2192 `json-query file 'a.b.c'` / `yaml-query file 'a.b.c'` / `xml-query file 'a.b.c'`",
3713
3774
  "- opening an image to check its dimensions, format, or size \u2192 `image-meta file`",
3714
3775
  "- opening a screenshot, diagram, or scan to read the text in it \u2192 `image-text file`",
3715
3776
  "- opening a PDF or Office document \u2192 inspect its format first, then read a narrow slice: PDF `pdf-meta`/`pdf-outline` then `pdf-extract`; Word `docx-outline` then `docx-text`; PowerPoint `pptx-outline` then `pptx-slide`/`pptx-notes`; Excel `xlsx-sheets` then `xlsx-head`/`xlsx-range`/`xlsx-query`",
3716
3777
  "",
3717
- 'Commands: `symbol NAME`, `read "file::symbol"`, `brief "file::symbol"`, `section "file::Heading"`, `semantic "description"`, `outline file`/`skeleton file`, `map --compact`, `refs file::symbol --callers`, `changed --symbol`, `config-get file KEY`, `json-query file \'a.b.c\'`/`yaml-query`, `json-outline file`/`yaml-outline`, `bash-output`/`web-output`/`mcp-output`, ' + (gdrive ? "`gdrive-sections <file-id>`, " : "") + "`image-meta file`/`image-text file`, `pdf-meta`/`pdf-outline`/`pdf-extract`, `docx-outline`/`docx-text`, `pptx-outline`/`pptx-slide`/`pptx-notes`/`pptx-text`, `xlsx-sheets`/`xlsx-head`/`xlsx-range`/`xlsx-query`.",
3778
+ 'Commands: `symbol NAME`, `read "file::symbol"`, `brief "file::symbol"`, `section "file::Heading"`, `semantic "description"`, `outline file`/`skeleton file`, `map --compact`, `refs file::symbol --callers`, `changed --symbol`, `config-get file KEY`, `json-query file \'a.b.c\'`/`yaml-query`/`xml-query`, `json-outline file`/`yaml-outline`/`xml-outline`, `bash-output`/`web-output`/`mcp-output`, ' + (gdrive ? "`gdrive-sections <file-id>`, " : "") + "`image-meta file`/`image-text file`, `pdf-meta`/`pdf-outline`/`pdf-extract`, `docx-outline`/`docx-text`, `pptx-outline`/`pptx-slide`/`pptx-notes`/`pptx-text`, `xlsx-sheets`/`xlsx-head`/`xlsx-range`/`xlsx-query`.",
3718
3779
  "",
3719
3780
  "Sub-agent briefs must carry this gate verbatim: a sub-agent inherits none of this context and its reads spend the same token budget.",
3720
3781
  "",
@@ -6666,7 +6727,7 @@ async function ocrImage(input) {
6666
6727
  const entryPath = resolveTesseractEntry();
6667
6728
  if (entryPath === null) return null;
6668
6729
  if (ocrBlockedOffline()) return null;
6669
- return new Promise((resolve9) => {
6730
+ return new Promise((resolve10) => {
6670
6731
  let settled = false;
6671
6732
  let child;
6672
6733
  try {
@@ -6675,7 +6736,7 @@ async function ocrImage(input) {
6675
6736
  });
6676
6737
  } catch {
6677
6738
  _ocrUnavailableThisProcess = true;
6678
- resolve9(null);
6739
+ resolve10(null);
6679
6740
  return;
6680
6741
  }
6681
6742
  const chunks = [];
@@ -6688,7 +6749,7 @@ async function ocrImage(input) {
6688
6749
  child.kill();
6689
6750
  } catch {
6690
6751
  }
6691
- resolve9(result);
6752
+ resolve10(result);
6692
6753
  };
6693
6754
  const timer = setTimeout(() => finish(null, true), _ocrTimeoutMs);
6694
6755
  child.stdout?.on("data", (c) => chunks.push(c));
@@ -6813,11 +6874,11 @@ function statInfo(absPath) {
6813
6874
  function imageShrinkCacheDir() {
6814
6875
  return path12.join(tokenGoatHome(), "image_shrink_cache");
6815
6876
  }
6816
- function shrinkCacheKey(originalPath, size, mtimeMs) {
6817
- return createHash2("sha256").update(`${originalPath}:${size}:${mtimeMs}`).digest("hex").slice(0, 16);
6877
+ function shrinkCacheKey(originalPath, size, mtimeMs, quality) {
6878
+ return createHash2("sha256").update(`${originalPath}:${size}:${mtimeMs}:${quality}`).digest("hex").slice(0, 16);
6818
6879
  }
6819
- function findCachedShrink(originalPath, size, mtimeMs) {
6820
- const key = shrinkCacheKey(originalPath, size, mtimeMs);
6880
+ function findCachedShrink(originalPath, size, mtimeMs, quality) {
6881
+ const key = shrinkCacheKey(originalPath, size, mtimeMs, quality);
6821
6882
  const dir = imageShrinkCacheDir();
6822
6883
  const candidates = [
6823
6884
  { ext: ".webp", format: "webp" },
@@ -6829,11 +6890,11 @@ function findCachedShrink(originalPath, size, mtimeMs) {
6829
6890
  }
6830
6891
  return null;
6831
6892
  }
6832
- function writeCachedShrink(originalPath, result, mtimeMs) {
6893
+ function writeCachedShrink(originalPath, result, mtimeMs, quality) {
6833
6894
  try {
6834
6895
  const dir = imageShrinkCacheDir();
6835
6896
  ensureDirSync(dir);
6836
- const key = shrinkCacheKey(originalPath, result.originalBytes, mtimeMs);
6897
+ const key = shrinkCacheKey(originalPath, result.originalBytes, mtimeMs, quality);
6837
6898
  const ext = result.format === "jpeg" ? ".jpg" : ".webp";
6838
6899
  atomicWriteBytes(path12.join(dir, `token-goat-shrink-${key}${ext}`), result.data);
6839
6900
  } catch {
@@ -6887,7 +6948,8 @@ async function preReadImageHandler(event) {
6887
6948
  const stat2 = statInfo(filePath);
6888
6949
  if (stat2 === null) return passOutput();
6889
6950
  const size = stat2.size;
6890
- const cached = findCachedShrink(filePath, stat2.size, stat2.mtimeMs);
6951
+ const quality = loadConfig().image_shrink.jpeg_quality;
6952
+ const cached = findCachedShrink(filePath, stat2.size, stat2.mtimeMs, quality);
6891
6953
  if (cached !== null) {
6892
6954
  let cachedData;
6893
6955
  try {
@@ -6940,12 +7002,12 @@ async function preReadImageHandler(event) {
6940
7002
  return passOutput();
6941
7003
  }
6942
7004
  }
6943
- const result = await shrinkImage(input, { sizeThresholdBytes: 0 });
7005
+ const result = await shrinkImage(input, { quality, sizeThresholdBytes: 0 });
6944
7006
  if (result === null) {
6945
7007
  recordStat("image_shrink_skipped");
6946
7008
  return passOutput();
6947
7009
  }
6948
- writeCachedShrink(filePath, result, stat2.mtimeMs);
7010
+ writeCachedShrink(filePath, result, stat2.mtimeMs, quality);
6949
7011
  return finalizeShrinkResult(result, filePath);
6950
7012
  }
6951
7013
  registerHook("pre_tool_use", preReadImageHandler, { toolName: "Read" });
@@ -6986,7 +7048,7 @@ registerReset(() => {
6986
7048
  PIPELINE_RETRY_DELAY_MS = DEFAULT_PIPELINE_RETRY_DELAY_MS;
6987
7049
  });
6988
7050
  function sleep(ms) {
6989
- return new Promise((resolve9) => setTimeout(resolve9, ms));
7051
+ return new Promise((resolve10) => setTimeout(resolve10, ms));
6990
7052
  }
6991
7053
  async function buildExtractorWithRetry(pipelineFn, modelName) {
6992
7054
  let lastError;
@@ -7536,6 +7598,7 @@ function trimToBudget(text, budgetTokens, command) {
7536
7598
  return text;
7537
7599
  }
7538
7600
  const lines2 = text.split("\n");
7601
+ if (lines2.length > 1 && lines2[lines2.length - 1] === "") lines2.pop();
7539
7602
  const totalLines = lines2.length;
7540
7603
  const bodyBudget = Math.max(1, budgetTokens - markerMarginTokens);
7541
7604
  const charBudget = bodyBudget * 3;
@@ -8806,6 +8869,7 @@ ${recall}`
8806
8869
  if (htmlResult.shouldBlock) return htmlResult;
8807
8870
  }
8808
8871
  const lines2 = content.split("\n");
8872
+ if (lines2.length > 1 && lines2[lines2.length - 1] === "") lines2.pop();
8809
8873
  const preview = [
8810
8874
  "--- first 5 lines ---",
8811
8875
  ...lines2.slice(0, 5),
@@ -9421,7 +9485,8 @@ function isSessionArtifactFile(filePath) {
9421
9485
  if (/[/\\]tool-results[/\\][a-z0-9]+\.txt$/i.test(filePath)) return true;
9422
9486
  return false;
9423
9487
  }
9424
- function sessionArtifactRecall(filePath) {
9488
+ function sessionArtifactRecall(rawPath) {
9489
+ const filePath = displaySafePath(rawPath);
9425
9490
  return 'Use `token-goat bash-output --file "' + filePath + '" --tail 50` (or `--grep PATTERN`) to read a slice instead of the full file.';
9426
9491
  }
9427
9492
  function readIntToolInput(event, key) {
@@ -9488,7 +9553,8 @@ function estimateRequestedSlice(event, absPath) {
9488
9553
  if (scan.trustworthy) return { kind: "bytes", bytes: scan.bytes };
9489
9554
  return { kind: "unbounded" };
9490
9555
  }
9491
- function describeSliceAdvice(slice, absPath) {
9556
+ function describeSliceAdvice(slice, rawAbsPath) {
9557
+ const absPath = displaySafePath(rawAbsPath);
9492
9558
  if (slice.kind === "nearSingleLine") {
9493
9559
  return BYTE_RANGE_ADVICE(absPath);
9494
9560
  }
@@ -9647,6 +9713,7 @@ function preReadHandlerInner(event) {
9647
9713
  }
9648
9714
  if (filePath === void 0) return passOutput();
9649
9715
  const normalized = normalizePath(filePath);
9716
+ const shown = displaySafePath(normalized);
9650
9717
  if (isNodeModulesPath(normalized)) {
9651
9718
  return denyOutput(
9652
9719
  "node_modules is typically noise; use npm ls, npm outdated, or npm audit instead for dependency info. To force access, use: token-goat read node_modules/package/file.js::symbol-name or token-goat section node_modules/package/file.js::heading"
@@ -9670,7 +9737,7 @@ function preReadHandlerInner(event) {
9670
9737
  const basename12 = path18.basename(normalized);
9671
9738
  if (isLockFile(basename12)) {
9672
9739
  return denyOutput(
9673
- 'Lock files are rarely useful to read in full. Use `token-goat section "' + normalized + '::<section>"` to extract a specific dependency, or read the relevant manifest instead.'
9740
+ '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.'
9674
9741
  );
9675
9742
  }
9676
9743
  if (normalized.toLowerCase().endsWith(".tsbuildinfo")) {
@@ -9694,13 +9761,13 @@ function preReadHandlerInner(event) {
9694
9761
  if (isTsConfigFile(basename12) && wasFileReadThisSession(normalized)) {
9695
9762
  recordActualRead(event, normalized);
9696
9763
  return quietContextOutput(
9697
- "Already read " + basename12 + '. Use `token-goat section "' + normalized + '::compilerOptions"` to extract compiler options, or `token-goat config-get ' + normalized + " compilerOptions.target` for a single value."
9764
+ "Already read " + basename12 + '. Use `token-goat section "' + shown + '::compilerOptions"` to extract compiler options, or `token-goat config-get ' + shown + " compilerOptions.target` for a single value."
9698
9765
  );
9699
9766
  }
9700
9767
  if (isManifestFile(basename12) && wasFileReadThisSession(normalized)) {
9701
9768
  recordActualRead(event, normalized);
9702
9769
  return quietContextOutput(
9703
- "You've already read " + basename12 + '. Use `token-goat section "' + normalized + '::<field>"` or `token-goat config-get ' + normalized + " <key>` to extract just the value you need."
9770
+ "You've already read " + basename12 + '. Use `token-goat section "' + shown + '::<field>"` or `token-goat config-get ' + shown + " <key>` to extract just the value you need."
9704
9771
  );
9705
9772
  }
9706
9773
  const skillName = detectSkillFile(normalized);
@@ -9729,7 +9796,7 @@ function preReadHandlerInner(event) {
9729
9796
  const savedBytes = Math.max(0, fullSize - compactBody.length);
9730
9797
  recordStat("session_hint", savedBytes, Math.round(savedBytes / 4));
9731
9798
  return denyOutput(
9732
- "Serving the extractive compact sidecar in place of the full file (source unchanged since the last `compact-doc` build):\n\n" + fenceUntrustedFileContent(compactBody) + '\n\nUse `token-goat compact-doc "' + normalized + '" --force` to rebuild it, or `token-goat compact-doc "' + normalized + '" --show` to view it directly. ' + editAnywayHint(normalized)
9799
+ "Serving the extractive compact sidecar in place of the full file (source unchanged since the last `compact-doc` build):\n\n" + fenceUntrustedFileContent(compactBody) + '\n\nUse `token-goat compact-doc "' + shown + '" --force` to rebuild it, or `token-goat compact-doc "' + shown + '" --show` to view it directly. ' + editAnywayHint(normalized)
9733
9800
  );
9734
9801
  }
9735
9802
  }
@@ -9770,7 +9837,7 @@ function preReadHandlerInner(event) {
9770
9837
  const hintText = formatHeadingTree(headings, normalized);
9771
9838
  const headingTextsLower = new Set(headings.map((h) => h.text.trim().toLowerCase()));
9772
9839
  const wellKnown = getWellKnownSections(basename12).filter((s) => headingTextsLower.has(s.trim().toLowerCase()));
9773
- const wellKnownText = wellKnown.length > 0 ? "\nQuick access: " + wellKnown.map((s) => 'token-goat section "' + normalized + "::" + s + '"').join(" | ") : "";
9840
+ const wellKnownText = wellKnown.length > 0 ? "\nQuick access: " + wellKnown.map((s) => 'token-goat section "' + shown + "::" + s + '"').join(" | ") : "";
9774
9841
  const changelogExtra = basename12.toLowerCase() === "changelog.md" ? extractChangelogVersionHint(fileContent, normalized) : "";
9775
9842
  let message = fenceUntrustedFileContent(hintText + changelogExtra) + wellKnownText;
9776
9843
  const slice = estimateRequestedSlice(event, normalized);
@@ -9794,7 +9861,7 @@ function preReadHandlerInner(event) {
9794
9861
  recordStat("session_hint", 0, 0);
9795
9862
  const isMainMemory = basename12.toLowerCase() === "memory.md";
9796
9863
  return denyOutput(
9797
- isMainMemory ? "MEMORY.md was read this session. Its content is in the compact manifest as 'session memory'." : normalized + ' was already read this session. Memory files rarely change mid-session. Use `token-goat section "' + normalized + '::SectionHeading"` to extract one section.'
9864
+ isMainMemory ? "MEMORY.md was read this session. Its content is in the compact manifest as 'session memory'." : shown + ' was already read this session. Memory files rarely change mid-session. Use `token-goat section "' + shown + '::SectionHeading"` to extract one section.'
9798
9865
  );
9799
9866
  }
9800
9867
  if (/^\.improve-state-.*\.json$/.test(basename12) && wasFileReadThisSession(normalized)) {
@@ -9808,7 +9875,7 @@ function preReadHandlerInner(event) {
9808
9875
  recordActualRead(event, normalized);
9809
9876
  recordStat("session_hint", 0, 0);
9810
9877
  return denyOutput(
9811
- normalized + " was already read this session. Environment files rarely change mid-session. Use `token-goat config-get " + normalized + " KEY_NAME` to extract a specific variable."
9878
+ shown + " was already read this session. Environment files rarely change mid-session. Use `token-goat config-get " + shown + " KEY_NAME` to extract a specific variable."
9812
9879
  );
9813
9880
  }
9814
9881
  if (isSessionArtifactFile(normalized)) {
@@ -9839,7 +9906,7 @@ function preReadHandlerInner(event) {
9839
9906
  recordActualRead(event, normalized);
9840
9907
  recordStat("session_hint", 0, 0);
9841
9908
  return denyOutput(
9842
- normalized + " was already read this session. " + sessionArtifactRecall(normalized)
9909
+ shown + " was already read this session. " + sessionArtifactRecall(normalized)
9843
9910
  );
9844
9911
  }
9845
9912
  {
@@ -9898,7 +9965,7 @@ function preReadHandlerInner(event) {
9898
9965
  if (scanCrossSessionManifests(project.root, project.hash, normalized, ttlSecs)) {
9899
9966
  recordActualRead(event, normalized);
9900
9967
  return quietContextOutput(
9901
- "This file may have already been read by another agent/session working in this project recently. If you are a subagent continuing shared work, consider whether you already have this content from context, or use `token-goat read " + normalized + "::SymbolName` for a narrower slice instead of a full re-read."
9968
+ "This file may have already been read by another agent/session working in this project recently. If you are a subagent continuing shared work, consider whether you already have this content from context, or use `token-goat read " + shown + "::SymbolName` for a narrower slice instead of a full re-read."
9902
9969
  );
9903
9970
  }
9904
9971
  }
@@ -9930,7 +9997,7 @@ function preReadHandlerInner(event) {
9930
9997
  if (/\.(md|mdx|markdown|rst)$/i.test(basename12)) {
9931
9998
  recordStat("session_hint", rereadCredit, Math.round(rereadCredit / 4));
9932
9999
  return denyOutput(
9933
- 'Markdown file already read this session. Use `token-goat section "' + normalized + '::HeadingName"` to read one section. ' + editAnywayHint(normalized)
10000
+ 'Markdown file already read this session. Use `token-goat section "' + shown + '::HeadingName"` to read one section. ' + editAnywayHint(normalized)
9934
10001
  );
9935
10002
  }
9936
10003
  const isSourceExt = isSourceExtension(basename12);
@@ -9938,22 +10005,22 @@ function preReadHandlerInner(event) {
9938
10005
  recordStat("read_count_deny", rereadCredit, Math.round(rereadCredit / 4));
9939
10006
  recordStat("session_hint", rereadCredit, Math.round(rereadCredit / 4));
9940
10007
  return denyOutput(
9941
- "Read this file " + reads + ' times already \u2014 use `token-goat read "' + normalized + '::Symbol"`, `token-goat skeleton ' + normalized + "`, or `token-goat outline " + normalized + "` to pull just the part you need. " + editAnywayHint(normalized)
10008
+ "Read this file " + reads + ' times already \u2014 use `token-goat read "' + shown + '::Symbol"`, `token-goat skeleton ' + shown + "`, or `token-goat outline " + shown + "` to pull just the part you need. " + editAnywayHint(normalized)
9942
10009
  );
9943
10010
  }
9944
10011
  }
9945
- const hint = _isDocFile(normalized) ? 'Use `token-goat section "' + normalized + '::SectionName"` to read one section.' : "Use token-goat read/section/symbol to re-read surgically.";
10012
+ const hint = _isDocFile(normalized) ? 'Use `token-goat section "' + shown + '::SectionName"` to read one section.' : "Use token-goat read/section/symbol to re-read surgically.";
9946
10013
  if (config2.hints.reread_deny && !protectedRead && (rereadBytes >= config2.hints.reread_deny_min_bytes || reads >= 2)) {
9947
10014
  recordStat("session_hint", rereadCredit, Math.round(rereadCredit / 4));
9948
10015
  return denyOutput(
9949
- normalized + " was already read this session (" + reads + " " + plural + "). " + hint + " " + editAnywayHint(normalized)
10016
+ shown + " was already read this session (" + reads + " " + plural + "). " + hint + " " + editAnywayHint(normalized)
9950
10017
  );
9951
10018
  }
9952
10019
  if (!isWithinQuietHours(config2.hints.quiet_hours)) {
9953
10020
  recordStat("session_hint", 0, 0);
9954
10021
  }
9955
10022
  return quietContextOutput(
9956
- "Note: " + normalized + " was already read this session (" + reads + " " + plural + "). " + hint
10023
+ "Note: " + shown + " was already read this session (" + reads + " " + plural + "). " + hint
9957
10024
  );
9958
10025
  }
9959
10026
  const size = statSize(normalized);
@@ -9966,12 +10033,12 @@ function preReadHandlerInner(event) {
9966
10033
  }
9967
10034
  const kb = toKB(size);
9968
10035
  const config2 = loadConfig();
9969
- const hint = _isDocFile(normalized) ? 'Use `token-goat section "' + normalized + '::SectionName"` to read one section.' : "Consider token-goat skeleton or token-goat section.";
10036
+ const hint = _isDocFile(normalized) ? 'Use `token-goat section "' + shown + '::SectionName"` to read one section.' : "Consider token-goat skeleton or token-goat section.";
9970
10037
  if (gateSize >= largeFileDenyBytes()) {
9971
10038
  const denyCredit = Math.min(size, PER_FILE_COUNTERFACTUAL_CEILING);
9972
10039
  recordStat("session_hint", denyCredit, Math.round(denyCredit / 4));
9973
10040
  return denyOutput(
9974
- normalized + " is very large (" + kb + "KB). " + hint + " " + describeSliceAdvice(slice, normalized) + " " + editAnywayHint(normalized)
10041
+ shown + " is very large (" + kb + "KB). " + hint + " " + describeSliceAdvice(slice, normalized) + " " + editAnywayHint(normalized)
9975
10042
  );
9976
10043
  }
9977
10044
  recordActualRead(event, normalized);
@@ -9985,7 +10052,7 @@ function preReadHandlerInner(event) {
9985
10052
  recordStat("session_hint", 0, 0);
9986
10053
  }
9987
10054
  return quietContextOutput(
9988
- "Note: " + normalized + " is large (" + kb + "KB). " + hint + contextPressureAdvisorySuffix()
10055
+ "Note: " + shown + " is large (" + kb + "KB). " + hint + contextPressureAdvisorySuffix()
9989
10056
  );
9990
10057
  }
9991
10058
  const fileTypeExt = path18.extname(normalized).slice(1).toLowerCase();
@@ -10033,16 +10100,19 @@ function estimateTruncatedLineCount(normalized) {
10033
10100
  }
10034
10101
  return Infinity;
10035
10102
  }
10036
- function editAnywayHint(normalized) {
10103
+ function editAnywayHint(rawPath) {
10104
+ const normalized = displaySafePath(rawPath);
10037
10105
  return 'To edit it anyway, use `token-goat replace "' + normalized + '" --old-b64 <base64> --new-b64 <base64>` (preferred \u2014 no temp files needed) or `--old-from <oldfile> --new-from <newfile>` for a snippet edit, or `token-goat write-file "' + normalized + "\" --b64 <base64>` (or `--from <newfile>`) to rewrite the whole file \u2014 Read/Edit's own precondition can't be satisfied after this deny.";
10038
10106
  }
10039
- function truncatedReadDenyMessage(normalized) {
10107
+ function truncatedReadDenyMessage(rawPath) {
10108
+ const normalized = displaySafePath(rawPath);
10040
10109
  return 'File was truncated on last read (>33K tokens). Use `token-goat skeleton "' + normalized + '"` for structure or `token-goat read "' + normalized + '::SymbolName"` for one function. ' + editAnywayHint(normalized);
10041
10110
  }
10042
10111
  function postReadHandlerInner(event) {
10043
10112
  const filePath = getFilePath(event);
10044
10113
  if (filePath === void 0) return passOutput();
10045
10114
  const normalized = normalizePath(filePath);
10115
+ const shown = displaySafePath(normalized);
10046
10116
  const respText = extractReadOutput(event.raw);
10047
10117
  if (respText.includes("[Truncated:") || respText.includes("Truncated: PARTIAL view")) {
10048
10118
  markFileTruncated(normalized);
@@ -10063,7 +10133,7 @@ function postReadHandlerInner(event) {
10063
10133
  try {
10064
10134
  const cwd = getCwd(event) ?? process.cwd();
10065
10135
  const project = findProject(cwd) ?? makeProjectAt(cwd);
10066
- const source = fs22.readFileSync(normalized, "utf8");
10136
+ const source = decodeSource(fs22.readFileSync(normalized));
10067
10137
  recordEvidence({ projectRoot: project.root, source: normalized, representation: "file", text: source });
10068
10138
  } catch {
10069
10139
  }
@@ -10099,7 +10169,7 @@ function postReadHandlerInner(event) {
10099
10169
  if (lineCount >= minLines && meetsSavingsFloor(sz)) {
10100
10170
  recordStat("session_hint", 0, 0);
10101
10171
  return quietContextOutput(
10102
- normalized + " is " + lineCount + ' lines. Use `token-goat skeleton "' + normalized + '"` or `token-goat outline "' + normalized + '"` for structural navigation instead of a future full re-read.'
10172
+ shown + " is " + lineCount + ' lines. Use `token-goat skeleton "' + shown + '"` or `token-goat outline "' + shown + '"` for structural navigation instead of a future full re-read.'
10103
10173
  );
10104
10174
  }
10105
10175
  }
@@ -10123,7 +10193,7 @@ import * as readline from "node:readline";
10123
10193
  async function defaultConfirm(question) {
10124
10194
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
10125
10195
  try {
10126
- const answer = await new Promise((resolve9) => rl.question(question, resolve9));
10196
+ const answer = await new Promise((resolve10) => rl.question(question, resolve10));
10127
10197
  return /^y(es)?$/i.test(answer.trim());
10128
10198
  } finally {
10129
10199
  rl.close();
@@ -11412,7 +11482,7 @@ function extractHtml(content, filePath) {
11412
11482
  }
11413
11483
  }
11414
11484
  sections.sort((a, b) => a.line - b.line);
11415
- const totalLines = code.split("\n").length;
11485
+ const totalLines = countContentLines(code);
11416
11486
  assignFlatEndLines(sections, totalLines);
11417
11487
  const seenId = /* @__PURE__ */ new Set();
11418
11488
  const seenClass = /* @__PURE__ */ new Set();
@@ -11515,7 +11585,7 @@ function extractLiquid(content, filePath, relPath) {
11515
11585
  const stem = path22.basename(resolvedRel, path22.extname(resolvedRel));
11516
11586
  symbols.push({ filePath, name: stem, kind: "liquid_section_file", lineStart: 1, lineEnd: 1, body: "", docstring: "", parent: "" });
11517
11587
  }
11518
- const totalLines = content.split("\n").length;
11588
+ const totalLines = countContentLines(content);
11519
11589
  for (const hm of findHtmlHeadingMatches(content)) {
11520
11590
  if (hm.heading) {
11521
11591
  const line = offsetToLine(lineIndex, hm.offset);
@@ -12492,12 +12562,64 @@ function extractZig(content, filePath) {
12492
12562
 
12493
12563
  // src/languages/r.ts
12494
12564
  var FUNC_ASSIGN_RE = /^([A-Za-z_][A-Za-z0-9_.]*)\s*(?:<-|=)\s*(?:function|\\)\s*\(/;
12495
- var SETCLASS_RE = /setClass\s*\(\s*["']([A-Za-z_][A-Za-z0-9_.]*)/;
12496
- var SETMETHOD_RE = /setMethod\s*\(\s*["']([A-Za-z_][A-Za-z0-9_.]*)/;
12565
+ var SETCLASS_RE = /^(?:[A-Za-z_][A-Za-z0-9_.]*\s*(?:<-|=)\s*)?setClass\s*\(\s*["']([A-Za-z_][A-Za-z0-9_.]*)/;
12566
+ var SETMETHOD_RE = /^(?:[A-Za-z_][A-Za-z0-9_.]*\s*(?:<-|=)\s*)?setMethod\s*\(\s*["']([A-Za-z_][A-Za-z0-9_.]*)/;
12567
+ function matchingParenIndex(content, openIndex) {
12568
+ let depth = 0;
12569
+ let quote = null;
12570
+ for (let i = openIndex; i < content.length; i++) {
12571
+ const ch = content[i];
12572
+ if (quote !== null) {
12573
+ if (ch === "\\") {
12574
+ i++;
12575
+ continue;
12576
+ }
12577
+ if (ch === quote) quote = null;
12578
+ continue;
12579
+ }
12580
+ if (ch === "#") {
12581
+ while (i < content.length && content[i] !== "\n") i++;
12582
+ continue;
12583
+ }
12584
+ if (ch === '"' || ch === "'" || ch === "`") {
12585
+ quote = ch;
12586
+ continue;
12587
+ }
12588
+ if (ch === "(") depth++;
12589
+ else if (ch === ")") {
12590
+ depth--;
12591
+ if (depth === 0) return i;
12592
+ }
12593
+ }
12594
+ return null;
12595
+ }
12596
+ function bracedBodyEndLine(content, lineIndex, parenIndex, totalLines, fallback) {
12597
+ const close = matchingParenIndex(content, parenIndex);
12598
+ if (close === null) return fallback;
12599
+ let j = close + 1;
12600
+ while (j < content.length) {
12601
+ const ch = content[j];
12602
+ if (ch === "#") {
12603
+ while (j < content.length && content[j] !== "\n") j++;
12604
+ continue;
12605
+ }
12606
+ if (ch !== " " && ch !== " " && ch !== "\r" && ch !== "\n") break;
12607
+ j++;
12608
+ }
12609
+ if (content[j] !== "{") return fallback;
12610
+ return findMatchingBraceEndLine(content, j, totalLines, lineIndex, "#");
12611
+ }
12612
+ function callEndLine(content, lineIndex, parenIndex, fallback) {
12613
+ const close = matchingParenIndex(content, parenIndex);
12614
+ return close === null ? fallback : offsetToLine(lineIndex, close);
12615
+ }
12497
12616
  function extractR(content, filePath) {
12498
12617
  const symbols = [];
12499
12618
  const imports = [];
12500
12619
  const lines2 = content.split(/\r?\n/);
12620
+ const lineIndex = buildLineIndex(content);
12621
+ const totalLines = countContentLines(content);
12622
+ const spanBody = (startLine, endLine) => lines2.slice(startLine - 1, endLine).join("\n");
12501
12623
  for (let i = 0; i < lines2.length; i++) {
12502
12624
  const rawLine = lines2[i] ?? "";
12503
12625
  const lineNum = i + 1;
@@ -12510,17 +12632,21 @@ function extractR(content, filePath) {
12510
12632
  if (!isIndented) {
12511
12633
  const fm = FUNC_ASSIGN_RE.exec(stripped);
12512
12634
  if (fm) {
12513
- symbols.push(makeLineSymbol(filePath, fm[1] ?? "", "function", lineNum, stripped.slice(0, 200), void 0, lines2, "hash"));
12635
+ const parenIndex = (lineIndex[i] ?? 0) + fm[0].length - 1;
12636
+ const endLine = bracedBodyEndLine(content, lineIndex, parenIndex, totalLines, lineNum);
12637
+ symbols.push(makeSpanSymbol(filePath, fm[1] ?? "", "function", { startLine: lineNum, endLine, body: spanBody(lineNum, endLine) }, void 0, lines2, "hash"));
12514
12638
  continue;
12515
12639
  }
12516
12640
  const cm = SETCLASS_RE.exec(stripped);
12517
12641
  if (cm) {
12518
- symbols.push(makeLineSymbol(filePath, cm[1] ?? "", "class", lineNum, stripped.slice(0, 200), void 0, lines2, "hash"));
12642
+ const endLine = callEndLine(content, lineIndex, (lineIndex[i] ?? 0) + (cm.index + cm[0].indexOf("(")), lineNum);
12643
+ symbols.push(makeSpanSymbol(filePath, cm[1] ?? "", "class", { startLine: lineNum, endLine, body: spanBody(lineNum, endLine) }, void 0, lines2, "hash"));
12519
12644
  continue;
12520
12645
  }
12521
12646
  const mm = SETMETHOD_RE.exec(stripped);
12522
12647
  if (mm) {
12523
- symbols.push(makeLineSymbol(filePath, mm[1] ?? "", "function", lineNum, stripped.slice(0, 200), void 0, lines2, "hash"));
12648
+ const endLine = callEndLine(content, lineIndex, (lineIndex[i] ?? 0) + (mm.index + mm[0].indexOf("(")), lineNum);
12649
+ symbols.push(makeSpanSymbol(filePath, mm[1] ?? "", "function", { startLine: lineNum, endLine, body: spanBody(lineNum, endLine) }, void 0, lines2, "hash"));
12524
12650
  }
12525
12651
  }
12526
12652
  }
@@ -12587,7 +12713,7 @@ function extractGraphql(content, filePath) {
12587
12713
  }
12588
12714
  }
12589
12715
  const stripped = stripHashComments(descriptionsStripped);
12590
- const totalLines = content.split("\n").length;
12716
+ const totalLines = countContentLines(content);
12591
12717
  const lineIndex = buildLineIndex(stripped);
12592
12718
  for (const m of stripped.matchAll(TYPE_RE)) {
12593
12719
  const keyword = m.groups?.["keyword"] ?? "";
@@ -12863,7 +12989,7 @@ function extractSql(content, filePath) {
12863
12989
  const sections = [];
12864
12990
  const seen = /* @__PURE__ */ new Set();
12865
12991
  const emit2 = makeSymbolEmitter(symbols, sections, seen, filePath, MAX_SYMBOLS3, MAX_HEADING_LEN2);
12866
- const totalLines = content.split("\n").length;
12992
+ const totalLines = countContentLines(content);
12867
12993
  const lineIndex = buildLineIndex(content);
12868
12994
  const noStrings = stripSqlStringLiterals(content);
12869
12995
  const singleLineEndLines = /* @__PURE__ */ new Map();
@@ -13065,7 +13191,7 @@ function extractMakefile(content, filePath) {
13065
13191
  const seen = /* @__PURE__ */ new Set();
13066
13192
  const emit2 = makeSymbolEmitter(symbols, sections, seen, filePath, MAX_SYMBOLS5, MAX_HEADING_LEN3);
13067
13193
  const stripped = stripComments(content);
13068
- const totalLines = content.split("\n").length;
13194
+ const totalLines = countContentLines(content);
13069
13195
  const lineIndex = buildLineIndex(stripped);
13070
13196
  const { noContinuation: strippedNoContinuation, forTargets: strippedForTargets } = maskContinuationAndDefines(stripped);
13071
13197
  for (const m of strippedForTargets.matchAll(TARGET_RE)) {
@@ -13145,7 +13271,7 @@ function extractProto(content, filePath) {
13145
13271
  const imports = [];
13146
13272
  const emit2 = makeSymbolEmitter(symbols, sections, seen, filePath, MAX_SYMBOLS6, MAX_HEADING_LEN4);
13147
13273
  const stripped = stripComments2(content);
13148
- const totalLines = content.split("\n").length;
13274
+ const totalLines = countContentLines(content);
13149
13275
  const lineIndex = buildLineIndex(stripped);
13150
13276
  const blockEndLines = /* @__PURE__ */ new Map();
13151
13277
  for (const m of stripped.matchAll(IMPORT_RE4)) {
@@ -13278,7 +13404,7 @@ function extractTerraform(content, filePath) {
13278
13404
  const seen = /* @__PURE__ */ new Set();
13279
13405
  const emit2 = makeSymbolEmitter(symbols, sections, seen, filePath, MAX_SYMBOLS7, MAX_HEADING_LEN5);
13280
13406
  const stripped = stripComments3(maskHeredocs(content));
13281
- const totalLines = content.split("\n").length;
13407
+ const totalLines = countContentLines(content);
13282
13408
  const lineIndex = buildLineIndex(stripped);
13283
13409
  const blockEndLines = /* @__PURE__ */ new Map();
13284
13410
  const matches = [
@@ -14076,8 +14202,8 @@ function extractLwcJavaScript(content, filePath) {
14076
14202
  const sourceLines = lines(content);
14077
14203
  const bundle = bundleName(filePath);
14078
14204
  const symbols = [
14079
- symbol(filePath, bundle, "lwc_bundle", 1, sourceLines.length),
14080
- symbol(filePath, lwcTagAlias(bundle), "lwc_component_alias", 1, sourceLines.length)
14205
+ symbol(filePath, bundle, "lwc_bundle", 1, countContentLines(content)),
14206
+ symbol(filePath, lwcTagAlias(bundle), "lwc_component_alias", 1, countContentLines(content))
14081
14207
  ];
14082
14208
  const refs = [];
14083
14209
  const commentFree = stripJsComments(content);
@@ -14175,10 +14301,9 @@ function attributeRefs(refs, content, filePath, attribute, split = false) {
14175
14301
  function extractSalesforceMarkup(content, filePath) {
14176
14302
  const normalized = filePath.replaceAll("\\", "/");
14177
14303
  const extension = path24.posix.extname(normalized).toLowerCase();
14178
- const sourceLines = lines(content);
14179
14304
  const kind = MARKUP_KIND[extension] ?? "salesforce_markup";
14180
14305
  const symbols = [
14181
- symbol(filePath, markupArtifactName(normalized, extension), kind, 1, sourceLines.length)
14306
+ symbol(filePath, markupArtifactName(normalized, extension), kind, 1, countContentLines(content))
14182
14307
  ];
14183
14308
  const refs = [];
14184
14309
  const isAura = [".cmp", ".app", ".evt", ".intf", ".design", ".auradoc", ".tokens"].includes(extension);
@@ -14330,7 +14455,7 @@ function componentSymbol(filePath, name, kind, totalLines) {
14330
14455
  return { filePath, name, kind, lineStart: 1, lineEnd: totalLines, body: "", docstring: "", parent: "" };
14331
14456
  }
14332
14457
  function extractVue(content, filePath) {
14333
- const totalLines = content.split("\n").length;
14458
+ const totalLines = countContentLines(content);
14334
14459
  const lineIndex = buildLineIndex(content);
14335
14460
  const name = componentName(filePath);
14336
14461
  const symbols = [componentSymbol(filePath, name, "vue_component", totalLines)];
@@ -14345,7 +14470,7 @@ function extractVue(content, filePath) {
14345
14470
  return finalize(symbols, refs);
14346
14471
  }
14347
14472
  function extractSvelte(content, filePath) {
14348
- const totalLines = content.split("\n").length;
14473
+ const totalLines = countContentLines(content);
14349
14474
  const lineIndex = buildLineIndex(content);
14350
14475
  const name = componentName(filePath);
14351
14476
  const symbols = [componentSymbol(filePath, name, "svelte_component", totalLines)];
@@ -14378,7 +14503,7 @@ function detectAstroFrontmatter(content) {
14378
14503
  return null;
14379
14504
  }
14380
14505
  function extractAstro(content, filePath) {
14381
- const totalLines = content.split("\n").length;
14506
+ const totalLines = countContentLines(content);
14382
14507
  const lineIndex = buildLineIndex(content);
14383
14508
  const name = componentName(filePath);
14384
14509
  const symbols = [componentSymbol(filePath, name, "astro_component", totalLines)];
@@ -14593,6 +14718,12 @@ var TSJS_KIND_BY_TYPE = /* @__PURE__ */ new Map([
14593
14718
  ["method_signature", "method"],
14594
14719
  ["property_signature", "var"],
14595
14720
  ["abstract_method_signature", "method"],
14721
+ // `declare function f(): void` parses as a `function_signature` inside an
14722
+ // ambient_declaration -- a distinct node type from `function_declaration`, which always
14723
+ // carries a body. Without an entry here every ambient function in a .d.ts file was
14724
+ // missing from the index entirely, the same container-drop shape as the module entries
14725
+ // below rather than a wrong span.
14726
+ ["function_signature", "function"],
14596
14727
  // `namespace Foo { ... }` (and the legacy `module Foo { ... }` synonym) parses as
14597
14728
  // `internal_module`; `declare module "some-string" { ... }` (an ambient module declaration,
14598
14729
  // common in .d.ts files) parses as `module` -- a distinct node type from either. Neither had a
@@ -14641,15 +14772,41 @@ function nodeName(node) {
14641
14772
  if (named !== null) return named.text;
14642
14773
  return null;
14643
14774
  }
14775
+ var SPEC_DECLARATION_OWNER = /* @__PURE__ */ new Map([
14776
+ ["variable_declarator", /* @__PURE__ */ new Set(["lexical_declaration", "variable_declaration"])],
14777
+ ["var_spec", /* @__PURE__ */ new Set(["var_declaration"])],
14778
+ ["const_spec", /* @__PURE__ */ new Set(["const_declaration"])],
14779
+ ["type_spec", /* @__PURE__ */ new Set(["type_declaration"])]
14780
+ ]);
14781
+ var PREFIX_WRAPPER_TYPES = /* @__PURE__ */ new Set(["export_statement", "ambient_declaration"]);
14782
+ function widenToDeclaration(node) {
14783
+ let widened = node;
14784
+ const owner = SPEC_DECLARATION_OWNER.get(widened.type);
14785
+ if (owner !== void 0) {
14786
+ const decl = widened.parent;
14787
+ if (decl === null || !owner.has(decl.type)) return widened;
14788
+ let specs = 0;
14789
+ for (const c of decl.namedChildren) if (c.type === widened.type) specs++;
14790
+ if (specs !== 1) return widened;
14791
+ if (decl.startPosition.row !== widened.startPosition.row) return widened;
14792
+ widened = decl;
14793
+ }
14794
+ for (; ; ) {
14795
+ const parent = widened.parent;
14796
+ if (parent === null || !PREFIX_WRAPPER_TYPES.has(parent.type)) return widened;
14797
+ widened = parent;
14798
+ }
14799
+ }
14644
14800
  function makeSymbol(filePath, name, kind, node, lines2, style) {
14645
- const lineStart = node.startPosition.row + 1;
14801
+ const ranged = widenToDeclaration(node);
14802
+ const lineStart = ranged.startPosition.row + 1;
14646
14803
  return {
14647
14804
  filePath,
14648
14805
  name,
14649
14806
  kind,
14650
14807
  lineStart,
14651
- lineEnd: node.endPosition.row + 1,
14652
- body: node.text,
14808
+ lineEnd: ranged.endPosition.row + 1,
14809
+ body: ranged.text,
14653
14810
  docstring: lines2 !== void 0 && style !== void 0 ? precedingDocComment(lines2, lineStart, style) : "",
14654
14811
  parent: ""
14655
14812
  };
@@ -14688,13 +14845,23 @@ function extractTsJsSymbols(root, filePath, lines2) {
14688
14845
  out.push(makeSymbol(filePath, name, kind, node, lines2, "c"));
14689
14846
  } else {
14690
14847
  const lineStart = decorators[0].startPosition.row + 1;
14848
+ const decoratedEnd = widenToDeclaration(node).endPosition.row + 1;
14691
14849
  out.push({
14692
14850
  filePath,
14693
14851
  name,
14694
14852
  kind,
14695
14853
  lineStart,
14696
- lineEnd: node.endPosition.row + 1,
14697
- body: [...decorators, node].map((n) => n.text).join("\n"),
14854
+ lineEnd: decoratedEnd,
14855
+ // Read the body off the file rather than gluing the decorator and the node
14856
+ // together with a newline: `@dec export class X {}` has no newline between
14857
+ // them, and the glued form both invents one and drops the `export` that sits
14858
+ // between the two nodes. A decorated declaration yields one symbol, so taking
14859
+ // its whole span cannot fan out.
14860
+ // The trailing replace keeps a convention the rest of the index follows: a
14861
+ // tree-sitter node never carries the indentation of its own first line,
14862
+ // because it starts at the first real character. Reading the span off the
14863
+ // file would otherwise make decorated symbols the one shape that does.
14864
+ body: lines2.slice(lineStart - 1, decoratedEnd).join("\n").replace(/^[ \t]+/, ""),
14698
14865
  docstring: precedingDocComment(lines2, lineStart, "c"),
14699
14866
  parent: ""
14700
14867
  });
@@ -14711,8 +14878,11 @@ function extractTsJsSymbols(root, filePath, lines2) {
14711
14878
  const isFn = value !== null && (value.type === "arrow_function" || value.type === "function_expression" || value.type === "function");
14712
14879
  out.push(makeSymbol(filePath, name.text, isFn ? "function" : "variable", child, lines2, "c"));
14713
14880
  } else {
14714
- for (const bound of collectPatternBindings(name)) {
14715
- out.push(makeSymbol(filePath, bound, "variable", child, lines2, "c"));
14881
+ const bindings = collectPatternBindings(name);
14882
+ const elideBodies = bindings.length > 1 && bindings.length * child.text.length > MAX_SYMBOL_BODY_CHARS;
14883
+ for (const bound of bindings) {
14884
+ const sym = makeSymbol(filePath, bound, "variable", child, lines2, "c");
14885
+ out.push(elideBodies ? { ...sym, body: "" } : sym);
14716
14886
  }
14717
14887
  }
14718
14888
  }
@@ -16224,7 +16394,7 @@ function indexFileSync(filePath, dbPath = globalDbPath(), preReadBytes) {
16224
16394
  throw err;
16225
16395
  }
16226
16396
  }
16227
- const content = raw.toString("utf8");
16397
+ const content = decodeSource(raw);
16228
16398
  const { symbols, refs } = parseContent(content, filePath, language);
16229
16399
  writeParseResult(filePath, raw, { symbols, refs, language, duration: 0 }, dbPath);
16230
16400
  }
@@ -16249,6 +16419,19 @@ var UNAVAILABLE_EMBED_SHA_PREFIX = "unavailable:";
16249
16419
  function unavailableEmbedSha(sha) {
16250
16420
  return UNAVAILABLE_EMBED_SHA_PREFIX + sha;
16251
16421
  }
16422
+ function indexedPathSpellingIsStale(storedPath, absPath) {
16423
+ if (!isCaseInsensitiveFs()) return false;
16424
+ const stored = normalizePath(storedPath);
16425
+ const candidate = normalizePath(path26.resolve(absPath));
16426
+ if (foldPath(stored) !== foldPath(candidate)) return false;
16427
+ let real;
16428
+ try {
16429
+ real = normalizePath(fs26.realpathSync.native(absPath));
16430
+ } catch {
16431
+ return false;
16432
+ }
16433
+ return real !== stored && foldPath(real) === foldPath(stored);
16434
+ }
16252
16435
  function isEmbedFresh(storedEmbedSha, sha, embeddingsEnabled, depsAvailable) {
16253
16436
  if (storedEmbedSha === void 0) return false;
16254
16437
  if (!embeddingsEnabled) return storedEmbedSha === disabledEmbedSha(sha);
@@ -16293,7 +16476,7 @@ async function indexFileEmbeddings(filePath, dbPath = globalDbPath(), sha, onErr
16293
16476
  }
16294
16477
  let content;
16295
16478
  try {
16296
- content = await fs26.promises.readFile(filePath, "utf8");
16479
+ content = decodeSource(await fs26.promises.readFile(filePath));
16297
16480
  } catch {
16298
16481
  return;
16299
16482
  }
@@ -16431,6 +16614,41 @@ function pruneSystemTempFiles(dbPath = globalDbPath()) {
16431
16614
  const db = getDb(dbPath);
16432
16615
  return removeFilesBestEffort(db, findSystemTempFiles(dbPath));
16433
16616
  }
16617
+ function findOrphanedChunkPaths(dbPath = globalDbPath()) {
16618
+ return orphanedChunkGroups(getDb(dbPath)).map((g) => g.representative);
16619
+ }
16620
+ function orphanedChunkGroups(db) {
16621
+ const known = new Set(
16622
+ db.prepare("SELECT DISTINCT path FROM files").all().map(
16623
+ (r) => foldPath(normalizePath(r.path))
16624
+ )
16625
+ );
16626
+ const rows = db.prepare("SELECT DISTINCT file_path FROM chunks").all();
16627
+ const byFolded = /* @__PURE__ */ new Map();
16628
+ for (const { file_path: raw } of rows) {
16629
+ const folded = foldPath(normalizePath(raw));
16630
+ if (known.has(folded)) continue;
16631
+ const group = byFolded.get(folded);
16632
+ if (group === void 0) byFolded.set(folded, { representative: raw, spellings: [raw] });
16633
+ else group.spellings.push(raw);
16634
+ }
16635
+ return [...byFolded.values()];
16636
+ }
16637
+ function pruneOrphanedChunks(dbPath = globalDbPath()) {
16638
+ const db = getDb(dbPath);
16639
+ const removed = [];
16640
+ const run = db.transaction(() => {
16641
+ for (const group of orphanedChunkGroups(db)) {
16642
+ try {
16643
+ for (const spelling of group.spellings) deleteFileEmbeddings(db, spelling);
16644
+ removed.push(group.representative);
16645
+ } catch {
16646
+ }
16647
+ }
16648
+ });
16649
+ run.immediate();
16650
+ return removed;
16651
+ }
16434
16652
  function recordKnownRoot(filePath, dbPath = globalDbPath()) {
16435
16653
  const project = findProject(path27.dirname(filePath));
16436
16654
  if (project === null || isTooShallowToPrune(project.root)) return;
@@ -16485,7 +16703,8 @@ function sweepKnownRoots(dbPath = globalDbPath(), opts) {
16485
16703
  prunedRows += removeDeletedFilesBestEffort(db, deletable).length;
16486
16704
  prunedRoots.push(root);
16487
16705
  }
16488
- return { prunedRows, prunedRoots, flaggedRoots };
16706
+ const prunedOrphanChunkPaths = pruneOrphanedChunks(dbPath);
16707
+ return { prunedRows, prunedRoots, flaggedRoots, prunedOrphanChunkPaths };
16489
16708
  }
16490
16709
  var KNOWN_ROOT_RECORD_MIN_INTERVAL_MS = 60 * 60 * 1e3;
16491
16710
  function knownRootRecordMarkerPath(dir, filePath) {
@@ -16626,11 +16845,25 @@ function pidFileIsWithinStartupGrace(dir) {
16626
16845
  return false;
16627
16846
  }
16628
16847
  }
16848
+ var ENCODED_LINE_MARKER = "!";
16849
+ function encodeDirtyQueueLine(absPath) {
16850
+ const needsEncoding = /[\r\n]/.test(absPath) || absPath !== absPath.trim();
16851
+ return needsEncoding ? ENCODED_LINE_MARKER + JSON.stringify(absPath) : absPath;
16852
+ }
16853
+ function decodeDirtyQueueLine(line) {
16854
+ if (!line.startsWith(ENCODED_LINE_MARKER)) return line;
16855
+ try {
16856
+ const decoded = JSON.parse(line.slice(ENCODED_LINE_MARKER.length));
16857
+ return typeof decoded === "string" ? decoded : line;
16858
+ } catch {
16859
+ return line;
16860
+ }
16861
+ }
16629
16862
  function parseDirtyQueueLines(raw) {
16630
16863
  const seen = /* @__PURE__ */ new Set();
16631
16864
  const out = [];
16632
16865
  for (const line of raw.split("\n")) {
16633
- const trimmed = line.trim();
16866
+ const trimmed = decodeDirtyQueueLine(line.trim());
16634
16867
  if (trimmed === "") continue;
16635
16868
  const normalized = normalizePath(trimmed);
16636
16869
  const dedupeKey = foldPath(normalized);
@@ -16718,10 +16951,10 @@ function embedFileSerialized(absPath, dbPath, sha) {
16718
16951
  activeEmbedSlots += 1;
16719
16952
  return dispatchEmbed();
16720
16953
  }
16721
- return new Promise((resolve9) => {
16954
+ return new Promise((resolve10) => {
16722
16955
  embedSlotWaiters.push(() => {
16723
16956
  activeEmbedSlots += 1;
16724
- resolve9(dispatchEmbed());
16957
+ resolve10(dispatchEmbed());
16725
16958
  });
16726
16959
  });
16727
16960
  };
@@ -16775,7 +17008,7 @@ function appendToDirtyQueue(dir, absPath) {
16775
17008
  if (existing.length > 0 && !existing.endsWith("\n")) leadingNewline = "\n";
16776
17009
  } catch {
16777
17010
  }
16778
- fs28.appendFileSync(queuePath, `${leadingNewline}${absPath}
17011
+ fs28.appendFileSync(queuePath, `${leadingNewline}${encodeDirtyQueueLine(absPath)}
16779
17012
  `);
16780
17013
  } catch {
16781
17014
  }
@@ -16793,7 +17026,7 @@ function makeIndexer(dbPath) {
16793
17026
  return true;
16794
17027
  }
16795
17028
  const entry = getFileEntry(absPath, dbPath);
16796
- const parseUnchanged = entry?.sha === sha;
17029
+ const parseUnchanged = entry?.sha === sha && !(entry !== null && indexedPathSpellingIsStale(entry.filePath, absPath));
16797
17030
  if (!parseUnchanged) {
16798
17031
  indexFileSync(absPath, dbPath);
16799
17032
  }
@@ -16826,7 +17059,7 @@ function processDirtyBatch(paths, index = makeIndexer(globalDbPath()), remove =
16826
17059
  if (!p) continue;
16827
17060
  writeDrainHeartbeat(dir);
16828
17061
  if (isUnderBlockedRoot(p, blockedRoots)) continue;
16829
- if (!fs28.existsSync(p)) {
17062
+ if (fileIsAbsent(p)) {
16830
17063
  remove(p);
16831
17064
  continue;
16832
17065
  }
@@ -17058,6 +17291,12 @@ function claimWorkerPidFile(dir, pid) {
17058
17291
  if (existingPid !== null && pidAlive(existingPid) && (hasFreshWorkerHeartbeat(dir, existingPid) || pidFileIsWithinStartupGrace(dir))) {
17059
17292
  return false;
17060
17293
  }
17294
+ if (existingPid !== null && pidAlive(existingPid) && existingPid !== pid && existingPid !== process.pid) {
17295
+ try {
17296
+ process.kill(existingPid, "SIGTERM");
17297
+ } catch {
17298
+ }
17299
+ }
17061
17300
  try {
17062
17301
  fs28.rmSync(pidPath, { force: true });
17063
17302
  } catch {
@@ -17157,7 +17396,7 @@ async function runWorkerLoop(dir, pollIntervalMs, shouldStop = () => false) {
17157
17396
  lastKnownRootsSweepMs = Date.now();
17158
17397
  }
17159
17398
  if (shouldStop()) break;
17160
- await new Promise((resolve9) => setTimeout(resolve9, pollIntervalMs));
17399
+ await new Promise((resolve10) => setTimeout(resolve10, pollIntervalMs));
17161
17400
  }
17162
17401
  }
17163
17402
  function runDetachedWorkerDaemon() {
@@ -17196,7 +17435,7 @@ function appendDirtyPath(normalizedPath2) {
17196
17435
  if (existing.length > 0 && !existing.endsWith("\n")) leadingNewline = "\n";
17197
17436
  } catch {
17198
17437
  }
17199
- fs29.appendFileSync(queuePath, `${leadingNewline}${normalizedPath2}
17438
+ fs29.appendFileSync(queuePath, `${leadingNewline}${encodeDirtyQueueLine(normalizedPath2)}
17200
17439
  `);
17201
17440
  }
17202
17441
  function enqueueDirtyPathSafe(filePath, opts) {
@@ -17552,6 +17791,7 @@ export {
17552
17791
  tomlBracketDelta,
17553
17792
  isParseSkipEligible,
17554
17793
  indexFileSync,
17794
+ indexedPathSpellingIsStale,
17555
17795
  isEmbedFresh,
17556
17796
  indexFileEmbeddings,
17557
17797
  removeFileFromIndex,
@@ -17559,6 +17799,8 @@ export {
17559
17799
  pruneBlockedRoot,
17560
17800
  findSystemTempFiles,
17561
17801
  pruneSystemTempFiles,
17802
+ findOrphanedChunkPaths,
17803
+ pruneOrphanedChunks,
17562
17804
  recordKnownRootThrottled,
17563
17805
  WORKER_HEARTBEAT_STALE_MS,
17564
17806
  dirtyQueuePathFor,