token-goat 2.9.10 → 2.9.12

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 (27) hide show
  1. package/README.md +54 -11
  2. package/SECURITY.md +17 -19
  3. package/THIRD_PARTY_NOTICES.md +667 -0
  4. package/dist/{token-goat-chunk-YFXFUG3L.mjs → token-goat-chunk-4CK445AW.mjs} +1275 -655
  5. package/dist/token-goat-chunk-6F5TLJC7.mjs +3728 -0
  6. package/dist/{token-goat-chunk-ATVQ6IIT.mjs → token-goat-chunk-6NIGPPN6.mjs} +10 -2
  7. package/dist/{token-goat-chunk-SLGOIXKF.mjs → token-goat-chunk-A6QTLAWO.mjs} +85 -328
  8. package/dist/{token-goat-chunk-7ALFFPJW.mjs → token-goat-chunk-GCXX67HM.mjs} +2699 -2175
  9. package/dist/{token-goat-chunk-PA5UXNOT.mjs → token-goat-chunk-GM6QZWCF.mjs} +5088 -7746
  10. package/dist/{token-goat-chunk-DSHYDU53.mjs → token-goat-chunk-HKFOH6JH.mjs} +11 -7
  11. package/dist/{token-goat-chunk-LYWIRYCF.mjs → token-goat-chunk-LHLQFGWQ.mjs} +1 -1
  12. package/dist/token-goat-chunk-LOCOX2ML.mjs +3564 -0
  13. package/dist/token-goat-chunk-OTW7LC4Y.mjs +5942 -0
  14. package/dist/{token-goat-chunk-FQD3OB5W.mjs → token-goat-chunk-QCQUIPIP.mjs} +6068 -1687
  15. package/dist/{token-goat-chunk-DCYHAUW3.mjs → token-goat-chunk-SBYRP3X4.mjs} +69 -37
  16. package/dist/{token-goat-chunk-JS44SRAX.mjs → token-goat-chunk-SF2CKCFQ.mjs} +3 -2
  17. package/dist/{token-goat-chunk-PXWBHSFB.mjs → token-goat-chunk-T6M7DAW3.mjs} +31 -4
  18. package/dist/{token-goat-chunk-ZQODJQTP.mjs → token-goat-chunk-THLHC6QJ.mjs} +354 -180
  19. package/dist/{token-goat-chunk-VCEK77ED.mjs → token-goat-chunk-TOGYS5A7.mjs} +5307 -6350
  20. package/dist/{token-goat-chunk-2TJBY5RD.mjs → token-goat-chunk-VRWX6QYW.mjs} +6 -5
  21. package/dist/token-goat-hook.mjs +6 -5
  22. package/dist/token-goat.core.mjs +8 -6
  23. package/docs/C4_RUNTIME_ARCHITECTURE.md +1 -1
  24. package/docs/cli.md +4 -1
  25. package/docs/install.md +131 -11
  26. package/docs/security.md +2 -0
  27. package/package.json +8 -3
@@ -1,8 +1,32 @@
1
1
  import { createRequire as __cjsRequire } from 'node:module';
2
2
  const require = __cjsRequire(import.meta.url);
3
3
  import {
4
- DEFAULT_QUERY_LIMIT,
5
4
  IMPORT_RE,
5
+ extractAbap,
6
+ extractAbl,
7
+ extractAsm,
8
+ extractBatch,
9
+ extractCShader,
10
+ extractCmake,
11
+ extractCobol,
12
+ extractErlang,
13
+ extractFortran,
14
+ extractGroovy,
15
+ extractJcl,
16
+ extractMatlab,
17
+ extractNatural,
18
+ extractObjc,
19
+ extractPascal,
20
+ extractPerl,
21
+ extractPli,
22
+ extractRpg,
23
+ extractSas,
24
+ extractSolidity,
25
+ extractThrift,
26
+ stripLeadingAttributes
27
+ } from "./token-goat-chunk-6F5TLJC7.mjs";
28
+ import {
29
+ DEFAULT_QUERY_LIMIT,
6
30
  ImageDecodeError,
7
31
  MAX_OVER_FETCH,
8
32
  MAX_ZIP_INPUT_BYTES,
@@ -11,19 +35,17 @@ import {
11
35
  SKIP_DIRS,
12
36
  ZipInputTooLargeError,
13
37
  ZipOutputTooLargeError,
14
- capJsonRows,
15
38
  countRefs,
16
39
  countSymbols,
17
40
  createLazyModuleLoader,
18
41
  distinctSymbolKinds,
19
- eachUnfencedLine,
20
42
  emptyIndexMessage,
21
43
  enqueueDirtyPathSafe,
22
- estimateTokens,
23
44
  extractPdfMeta,
24
45
  extractPdfOutline,
25
46
  extractPdfText,
26
47
  fenceUntrusted,
48
+ findContainingSection,
27
49
  formatCsvProfile,
28
50
  formatCsvTable,
29
51
  getEmbeddingCoverage,
@@ -35,8 +57,11 @@ import {
35
57
  isImagePath,
36
58
  isIndexEmptyForProject,
37
59
  isOcrEngineAvailable,
60
+ isPrivateIpv4Octets,
61
+ isPrivateIpv6Groups,
38
62
  isTextHeavy,
39
- lineOpenDelimiterAfter,
63
+ isTreeSitterAvailable,
64
+ listSections,
40
65
  loadBlob,
41
66
  locatePdfPages,
42
67
  mergeNearbyHits,
@@ -52,36 +77,62 @@ import {
52
77
  queryRefs,
53
78
  queryRefsByContext,
54
79
  querySymbols,
80
+ readPdfFileWithinBounds,
81
+ readSection,
55
82
  scanAndRecord,
56
83
  searchEvidenceSemantically,
57
84
  searchSemantic,
58
85
  searchSymbolsFts,
59
86
  shrinkImage,
60
87
  storeBlob,
61
- stripLeadingAttributes,
62
- stripTomlComment,
63
- tomlBracketDelta,
64
- trimToBudget,
65
88
  unzipBounded,
66
89
  urlPolicyDenialReason,
67
- walkProject,
68
- yamlLineClosesQuote,
69
- yamlOpenQuoteAfter
70
- } from "./token-goat-chunk-PA5UXNOT.mjs";
90
+ walkProject
91
+ } from "./token-goat-chunk-GM6QZWCF.mjs";
71
92
  import {
72
93
  Database,
94
+ FILENAME_LANGUAGE,
95
+ TREE_SITTER_LANGUAGES,
96
+ basenameImportsExtension,
97
+ capJsonRows,
98
+ colorStdout,
99
+ detectLanguage,
100
+ detectLanguageOfFile,
101
+ estimateTokens,
102
+ fileIsAbsent,
103
+ findProject,
104
+ fingerprintContent,
105
+ fingerprintFile,
106
+ getDb,
107
+ getDisplayRoot,
108
+ languageLabel,
109
+ loadConfig,
110
+ partialRefsReason,
111
+ recordStat,
112
+ redactIfDotenv,
113
+ redactSecrets,
114
+ resolveProjectRoot,
115
+ savedTokensFromBytes,
116
+ shortFingerprint,
117
+ stripAnsi,
118
+ trimToBudget,
119
+ unsupportedLanguageName
120
+ } from "./token-goat-chunk-TOGYS5A7.mjs";
121
+ import {
122
+ registerReset
123
+ } from "./token-goat-chunk-EEIDFMEM.mjs";
124
+ import {
73
125
  PER_FILE_COUNTERFACTUAL_CEILING,
126
+ UNTRUSTED_FILE_TAG,
74
127
  UNTRUSTED_GITHUB_TAG,
75
- _detectOpenQuote,
76
- _lineClosesQuote,
128
+ UNTRUSTED_HTML_TAG,
77
129
  atomicWriteBytes,
78
130
  buildContextWindow,
79
- buildLineIndex,
80
- colorStdout,
81
131
  compileGrepMatcher,
132
+ compileGuardedRegex,
82
133
  countNoun,
83
134
  decodeSource,
84
- detectLanguage,
135
+ displaySafeJson,
85
136
  displaySafeText,
86
137
  ensureNewline,
87
138
  escapeRegExp,
@@ -89,370 +140,39 @@ import {
89
140
  extractErrorMessage,
90
141
  fenceUntrustedContent,
91
142
  fenceUntrustedFileContent,
92
- fileIsAbsent,
93
143
  filtersFilteredToEmptyNotice,
94
- findHtmlHeadingMatches,
95
- findProject,
96
- fingerprintContent,
97
- fingerprintFile,
144
+ foldCaseForContainment,
98
145
  foldPath,
99
- getDb,
100
- getDisplayRoot,
101
146
  globalDbPath,
102
147
  grepFilteredToEmptyNotice,
148
+ isAblSource,
103
149
  isInsideRoot,
150
+ isMatlabSource,
151
+ isObjcHeader,
152
+ isObjcSource,
153
+ isPascalSource,
154
+ isPerlSource,
155
+ isPrologSource,
104
156
  isTestFile,
105
- loadConfig,
106
157
  normalizePath,
107
- offsetToLine,
108
158
  pushAll,
109
- recordStat,
110
- redactIfDotenv,
111
- redactSecrets,
112
159
  redactUrlQuery,
113
160
  renderContextWindow,
114
161
  requireNonNegativeStrictInt,
115
162
  requirePositiveStrictInt,
116
163
  resolveIndexPath,
117
164
  resolveOnPath,
118
- resolveProjectRoot,
119
165
  runGit,
120
- savedTokensFromBytes,
121
- shortFingerprint,
122
- stripAnsi,
123
166
  stripBom,
167
+ supportRequestLine,
124
168
  toDisplayPath,
125
- unsupportedLanguageName,
126
169
  windowsCmdQuoteArg,
127
170
  withExtension
128
- } from "./token-goat-chunk-VCEK77ED.mjs";
129
- import {
130
- registerReset
131
- } from "./token-goat-chunk-EEIDFMEM.mjs";
171
+ } from "./token-goat-chunk-LOCOX2ML.mjs";
132
172
  import {
133
173
  init_define_import_meta_env
134
174
  } from "./token-goat-chunk-A37V4PBF.mjs";
135
175
 
136
- // src/section_reader.ts
137
- init_define_import_meta_env();
138
- import { readFileSync } from "node:fs";
139
- function parseHeadingSpec(spec, headers) {
140
- const m = /^([^#\r\n]+)#(\d+)$/.exec(spec);
141
- if (m !== null && m[1] !== void 0 && m[2] !== void 0) {
142
- const specLower = spec.trim().toLowerCase();
143
- const isLiteralHeading = headers?.some((h) => h.heading.trim().toLowerCase() === specLower) ?? false;
144
- if (isLiteralHeading) {
145
- return { base: spec.trim(), ordinal: null };
146
- }
147
- return { base: m[1].trim(), ordinal: Number.parseInt(m[2], 10) };
148
- }
149
- return { base: spec.trim(), ordinal: null };
150
- }
151
- function normalizeHeading(s) {
152
- let n = s.replace(/[—–]/g, "-");
153
- n = n.replace(/\s*\([^)]+\)\s*$/, "");
154
- n = n.replace(/^\d+\.\s+/, "");
155
- return n.replace(/\s+/g, " ").trim();
156
- }
157
- function normalizeHeadingStrip(s) {
158
- let n = s.replace(/\s*[—–].*$/, "");
159
- n = n.replace(/\s*\([^)]+\)\s*$/, "");
160
- n = n.replace(/^\d+\.\s+/, "");
161
- return n.replace(/\s+/g, " ").trim();
162
- }
163
- var MIN_WIDEN_WORD_LEN = 3;
164
- var MARKDOWN_HEADER_RE = /^(#{1,6})\s+([^\r\n]+?)(?:\s+#+)?\s*$/;
165
- var TABLE_HEADER_RE = /^\s*\[+\s*([^\]]+?)\s*\]+\s*(?:[#;].*)?$/;
166
- var PYTHON_HEADER_RE = /^(\s*)(?:async\s+)?(?:def|class)\s+([A-Za-z_]\w*)/;
167
- var KEYVALUE_HEADER_RE = /^([A-Za-z_][\w.-]*)\s*(?:=|:(?!\/\/))/;
168
- var ENV_KEYVALUE_HEADER_RE = /^(?:export\s+)?([A-Za-z_][\w.-]*)\s*(?:=|:(?!\/\/))/;
169
- function findMarkdownHeaders(lines) {
170
- const headers = [];
171
- for (const [i, line] of eachUnfencedLine(lines)) {
172
- const m = MARKDOWN_HEADER_RE.exec(line);
173
- if (m === null || m[1] === void 0 || m[2] === void 0) continue;
174
- headers.push({ heading: m[2].trim(), level: m[1].length, index: i });
175
- }
176
- return headers;
177
- }
178
- function findTableHeaders(lines, isToml) {
179
- const headers = [];
180
- let openDelim = null;
181
- let arrayDepth = 0;
182
- for (let i = 0; i < lines.length; i++) {
183
- const line = lines[i];
184
- if (line === void 0) continue;
185
- if (openDelim !== null) {
186
- const closeIdx = line.indexOf(openDelim);
187
- if (closeIdx === -1) continue;
188
- const restStart = closeIdx + openDelim.length;
189
- const m2 = TABLE_HEADER_RE.exec(line.slice(restStart));
190
- if (m2 !== null && m2[1] !== void 0) headers.push({ heading: m2[1].trim(), level: 1, index: i });
191
- openDelim = lineOpenDelimiterAfter(stripTomlComment(line.slice(restStart)), 0);
192
- continue;
193
- }
194
- if (isToml && arrayDepth > 0) {
195
- arrayDepth = Math.max(0, arrayDepth + tomlBracketDelta(stripTomlComment(line)));
196
- continue;
197
- }
198
- const m = TABLE_HEADER_RE.exec(line);
199
- if (m !== null && m[1] !== void 0) headers.push({ heading: m[1].trim(), level: 1, index: i });
200
- if (isToml) {
201
- const code = stripTomlComment(line);
202
- openDelim = lineOpenDelimiterAfter(code, 0);
203
- if (openDelim === null) arrayDepth = Math.max(0, tomlBracketDelta(code));
204
- }
205
- }
206
- return headers;
207
- }
208
- function findPythonHeaders(lines) {
209
- const headers = [];
210
- const indentStack = [];
211
- let openDelim = null;
212
- for (let i = 0; i < lines.length; i++) {
213
- const line = lines[i];
214
- if (line === void 0) continue;
215
- if (openDelim !== null) {
216
- const closeIdx = line.indexOf(openDelim);
217
- if (closeIdx === -1) continue;
218
- openDelim = lineOpenDelimiterAfter(line, closeIdx + openDelim.length);
219
- continue;
220
- }
221
- const m = PYTHON_HEADER_RE.exec(line);
222
- if (m === null || m[1] === void 0 || m[2] === void 0) {
223
- openDelim = lineOpenDelimiterAfter(line, 0);
224
- continue;
225
- }
226
- const indent = m[1].replace(/\t/g, " ").length;
227
- while (indentStack.length > 0 && indent <= (indentStack[indentStack.length - 1] ?? -1)) {
228
- indentStack.pop();
229
- }
230
- const level = indentStack.length + 1;
231
- indentStack.push(indent);
232
- headers.push({ heading: m[2], level, index: i });
233
- openDelim = lineOpenDelimiterAfter(line, 0);
234
- }
235
- return headers;
236
- }
237
- function findKeyValueHeaders(lines, language) {
238
- const headers = [];
239
- const isEnv = language === "env_file";
240
- let openQuote = null;
241
- for (let i = 0; i < lines.length; i++) {
242
- const line = lines[i];
243
- if (line === void 0) continue;
244
- if (openQuote !== null) {
245
- const closed = isEnv ? _lineClosesQuote(line, openQuote) : yamlLineClosesQuote(line, openQuote);
246
- if (closed) openQuote = null;
247
- continue;
248
- }
249
- const scanned = isEnv ? line.replace(/^[ \t]+/, "") : line;
250
- const m = (isEnv ? ENV_KEYVALUE_HEADER_RE : KEYVALUE_HEADER_RE).exec(scanned);
251
- if (m === null || m[1] === void 0) continue;
252
- headers.push({ heading: m[1], level: 1, index: i });
253
- openQuote = isEnv ? _detectOpenQuote(scanned.slice(m[0].length)) : yamlOpenQuoteAfter(line, m[0].length);
254
- }
255
- return headers;
256
- }
257
- function findHtmlHeaders(text) {
258
- const lineIndex = buildLineIndex(text);
259
- const headers = [];
260
- for (const hm of findHtmlHeadingMatches(text)) {
261
- if (!hm.heading) continue;
262
- const index = offsetToLine(lineIndex, hm.offset) - 1;
263
- headers.push({ heading: hm.heading, level: hm.level, index });
264
- }
265
- return headers;
266
- }
267
- function findHeaders(text, language) {
268
- const lines = text.split("\n");
269
- if (language === "markdown") return { headers: findMarkdownHeaders(lines), kind: "markdown" };
270
- if (language === "html" || language === "liquid") return { headers: findHtmlHeaders(text), kind: "markdown" };
271
- if (language === "toml") return { headers: findTableHeaders(lines, true), kind: "table-toml" };
272
- if (language === "python") return { headers: findPythonHeaders(lines), kind: "python" };
273
- if (language === "ini") return { headers: findTableHeaders(lines, false), kind: "table-flat" };
274
- if (language === "yaml" || language === "env_file")
275
- return { headers: findKeyValueHeaders(lines, language), kind: "keyvalue" };
276
- const md = findMarkdownHeaders(lines);
277
- if (md.length > 0) return { headers: md, kind: "markdown" };
278
- const tbl = findTableHeaders(lines, false);
279
- if (tbl.length > 0) return { headers: tbl, kind: "table-toml" };
280
- return { headers: findKeyValueHeaders(lines, language), kind: "keyvalue" };
281
- }
282
- function sectionEndIndex(headers, headerPos, totalLines) {
283
- const current = headers[headerPos];
284
- if (current === void 0) return totalLines;
285
- for (let j = headerPos + 1; j < headers.length; j++) {
286
- const next = headers[j];
287
- if (next === void 0) continue;
288
- if (next.level <= current.level) return next.index;
289
- }
290
- return totalLines;
291
- }
292
- function tableSectionEndIndex(headers, headerPos, totalLines) {
293
- const current = headers[headerPos];
294
- if (current === void 0) return totalLines;
295
- const prefix = current.heading + ".";
296
- for (let j = headerPos + 1; j < headers.length; j++) {
297
- const next = headers[j];
298
- if (next === void 0) continue;
299
- if (next.heading.startsWith(prefix)) continue;
300
- return next.index;
301
- }
302
- return totalLines;
303
- }
304
- function resolveHeaderPos(headers, base, ordinal) {
305
- const target = base.toLowerCase();
306
- const normalizedTarget = normalizeHeading(base).toLowerCase();
307
- const strippedTarget = normalizeHeadingStrip(base).toLowerCase();
308
- const exactMatches = [];
309
- const normalizedMatches = [];
310
- const strippedMatches = [];
311
- for (let i = 0; i < headers.length; i++) {
312
- const h = headers[i];
313
- if (h === void 0) continue;
314
- const headingLower = h.heading.toLowerCase();
315
- if (headingLower === target) {
316
- exactMatches.push(i);
317
- } else if (normalizeHeading(h.heading).toLowerCase() === normalizedTarget) {
318
- normalizedMatches.push(i);
319
- } else if (normalizeHeadingStrip(h.heading).toLowerCase() === strippedTarget) {
320
- strippedMatches.push(i);
321
- }
322
- }
323
- const matches = exactMatches.length > 0 ? exactMatches : normalizedMatches.length > 0 ? normalizedMatches : strippedMatches;
324
- if (matches.length > 0) {
325
- const pick2 = ordinal === null ? 0 : ordinal - 1;
326
- const headerPos = matches[pick2];
327
- if (headerPos === void 0) return null;
328
- let occurrences = null;
329
- if (ordinal === null && matches.length > 1) {
330
- occurrences = [];
331
- for (const i of matches) {
332
- const h = headers[i];
333
- if (h !== void 0) occurrences.push(h.index + 1);
334
- }
335
- }
336
- return { headerPos, redirectedFrom: null, occurrences };
337
- }
338
- if (ordinal !== null || normalizedTarget.length === 0) return null;
339
- let prefixPos = -1;
340
- const distinct = /* @__PURE__ */ new Set();
341
- for (let i = 0; i < headers.length; i++) {
342
- const h = headers[i];
343
- if (h === void 0) continue;
344
- const norm = normalizeHeading(h.heading).toLowerCase();
345
- if (norm.startsWith(normalizedTarget)) {
346
- distinct.add(norm);
347
- if (prefixPos === -1) prefixPos = i;
348
- }
349
- }
350
- if (distinct.size === 1 && prefixPos !== -1) {
351
- const chosen = headers[prefixPos];
352
- if (chosen === void 0) return null;
353
- return { headerPos: prefixPos, redirectedFrom: base, occurrences: null };
354
- }
355
- const queryWords = normalizedTarget.split(/\s+/).filter((w) => w.length >= MIN_WIDEN_WORD_LEN);
356
- if (queryWords.length === 0) return null;
357
- const widenedMatches = [];
358
- for (let i = 0; i < headers.length; i++) {
359
- const h = headers[i];
360
- if (h === void 0) continue;
361
- const headingWords = normalizeHeading(h.heading).toLowerCase().split(/\s+/);
362
- const allWordsMatch = queryWords.every((qw) => headingWords.some((hw) => hw.includes(qw)));
363
- if (allWordsMatch) widenedMatches.push(i);
364
- }
365
- if (widenedMatches.length !== 1) return null;
366
- const widenedPos = widenedMatches[0];
367
- if (widenedPos === void 0) return null;
368
- return { headerPos: widenedPos, redirectedFrom: base, occurrences: null };
369
- }
370
- function buildSectionResult(headers, kind, lines, headerPos, redirectedFrom) {
371
- const header = headers[headerPos];
372
- if (header === void 0) return null;
373
- const endIndex = kind === "table-toml" ? tableSectionEndIndex(headers, headerPos, lines.length) : sectionEndIndex(headers, headerPos, lines.length);
374
- let endExclusive = endIndex;
375
- while (endExclusive > header.index + 1 && (lines[endExclusive - 1] === "" || lines[endExclusive - 1] === "\r")) {
376
- endExclusive--;
377
- }
378
- const content = lines.slice(header.index, endExclusive).join("\n");
379
- const result = {
380
- heading: header.heading,
381
- content,
382
- lineStart: header.index + 1,
383
- lineEnd: endExclusive
384
- };
385
- return redirectedFrom === null ? result : { ...result, redirectedFrom };
386
- }
387
- function resolveSectionFromText(text, headingSpec, language) {
388
- const { headers, kind } = findHeaders(text, language);
389
- const { base, ordinal } = parseHeadingSpec(headingSpec, headers);
390
- if (base.length === 0) return null;
391
- const lines = text.split("\n");
392
- const resolved = resolveHeaderPos(headers, base, ordinal);
393
- if (resolved === null) return null;
394
- const built = buildSectionResult(headers, kind, lines, resolved.headerPos, resolved.redirectedFrom);
395
- if (built === null || resolved.occurrences === null) return built;
396
- return { ...built, occurrences: resolved.occurrences };
397
- }
398
- function extractSection(text, headingSpec) {
399
- return resolveSectionFromText(text, headingSpec, "unknown");
400
- }
401
- function readTextForSections(filePath, readFn) {
402
- let text;
403
- if (readFn !== void 0) {
404
- const read = readFn(filePath);
405
- if (read === null) return null;
406
- text = read;
407
- } else {
408
- try {
409
- text = decodeSource(readFileSync(filePath));
410
- } catch {
411
- return null;
412
- }
413
- }
414
- if (text.charCodeAt(0) === 65279) {
415
- text = text.slice(1);
416
- }
417
- return redactIfDotenv(filePath, text);
418
- }
419
- function readSection(filePath, headingSpec, readFn) {
420
- const text = readTextForSections(filePath, readFn);
421
- if (text === null) return null;
422
- return resolveSectionFromText(text, headingSpec, detectLanguage(filePath));
423
- }
424
- function findContainingSection(filePath, lineStart, lineEnd, readFn) {
425
- const text = readTextForSections(filePath, readFn);
426
- if (text === null) return null;
427
- const language = detectLanguage(filePath);
428
- const { headers, kind } = findHeaders(text, language);
429
- if (headers.length === 0) return null;
430
- const lines = text.split("\n");
431
- let bestPos = -1;
432
- let bestHeaderLine = -1;
433
- for (let i = 0; i < headers.length; i++) {
434
- const header = headers[i];
435
- if (header === void 0) continue;
436
- const endIndex = kind === "table-toml" ? tableSectionEndIndex(headers, i, lines.length) : sectionEndIndex(headers, i, lines.length);
437
- const sectionLineStart = header.index + 1;
438
- if (sectionLineStart <= lineStart && lineEnd <= endIndex) {
439
- if (header.index > bestHeaderLine) {
440
- bestHeaderLine = header.index;
441
- bestPos = i;
442
- }
443
- }
444
- }
445
- if (bestPos === -1) return null;
446
- return buildSectionResult(headers, kind, lines, bestPos, null);
447
- }
448
- function listSections(filePath, readFn) {
449
- const text = readTextForSections(filePath, readFn);
450
- if (text === null) return [];
451
- const language = detectLanguage(filePath);
452
- const { headers } = findHeaders(text, language);
453
- return headers.map((h) => h.heading);
454
- }
455
-
456
176
  // src/graph_commands.ts
457
177
  init_define_import_meta_env();
458
178
  import * as fs6 from "node:fs";
@@ -480,35 +200,15 @@ function isRefIndexedLanguage(language) {
480
200
  function isRefIndexedFile(filePath) {
481
201
  return isRefIndexedLanguage(detectLanguage(filePath));
482
202
  }
483
- var LANGUAGE_LABELS = /* @__PURE__ */ new Map([
484
- ["csharp", "C#"],
485
- ["cpp", "C++"],
486
- ["c", "C"],
487
- ["php", "PHP"],
488
- ["powershell", "PowerShell"],
489
- ["sql", "SQL"],
490
- ["graphql", "GraphQL"],
491
- ["proto", "Protocol Buffers"],
492
- ["html", "HTML"],
493
- ["css", "CSS"],
494
- ["toml", "TOML"],
495
- ["json", "JSON"],
496
- ["yaml", "YAML"],
497
- ["ini", "INI"],
498
- ["env_file", "env file"],
499
- ["ipynb", "Jupyter notebook"],
500
- ["salesforce_metadata", "Salesforce metadata"],
501
- ["salesforce_markup", "Salesforce markup"],
502
- ["unknown", "this file type"]
503
- ]);
504
- function languageLabel(language) {
505
- return LANGUAGE_LABELS.get(language) ?? language;
506
- }
507
203
  function refBlindLanguageNotice(symbolName, language, displayPath) {
204
+ const partial = partialRefsReason(language);
205
+ if (partial !== void 0) {
206
+ return `No recorded references to '${symbolName}' (${displayPath}). ${partial}, so an empty result is not evidence the name is unused. Search the source directly as well, e.g. \`rg -n -w ${symbolName}\`.`;
207
+ }
508
208
  return `Cannot determine references for '${symbolName}': ${languageLabel(language)} call sites are not indexed (${displayPath}). This is a gap in token-goat's index, not evidence the symbol is unreferenced. Search the source directly instead, e.g. \`rg -n -w ${symbolName}\`.`;
509
209
  }
510
210
  var REF_BLIND_KIND_REASON = "the index records value-position references only (calls, `new`, macro invocations), never type annotations, so a type declaration has no ref rows however widely it is used";
511
- var REF_BLIND_DEF_PROBE_LIMIT = 50;
211
+ var REF_BLIND_DEF_PROBE_LIMIT = -1;
512
212
  function kindWithArticle(kind) {
513
213
  return `${/^[aeiou]/.test(kind) ? "an" : "a"} ${kind}`;
514
214
  }
@@ -874,16 +574,16 @@ function outlineXml(xmlText, opts = {}) {
874
574
  function formatXmlOutline(summary) {
875
575
  const lines = [];
876
576
  lines.push(
877
- `Root element: <${summary.rootTag}> (${summary.totalElements} total elements, ${summary.uniqueTags.length} unique tags, max depth ${summary.maxDepth})`
577
+ `Root element: <${displaySafeText(summary.rootTag)}> (${summary.totalElements} total elements, ${summary.uniqueTags.length} unique tags, max depth ${summary.maxDepth})`
878
578
  );
879
579
  if (summary.doctype) {
880
- lines.push(`DOCTYPE: ${summary.doctype}`);
580
+ lines.push(`DOCTYPE: ${displaySafeText(summary.doctype)}`);
881
581
  }
882
582
  const nsKeys = Object.keys(summary.namespaces);
883
583
  if (nsKeys.length > 0) {
884
584
  lines.push("Namespaces:");
885
585
  for (const [k, v] of Object.entries(summary.namespaces)) {
886
- lines.push(` ${k}: ${v}`);
586
+ lines.push(` ${displaySafeText(k)}: ${displaySafeText(v)}`);
887
587
  }
888
588
  }
889
589
  lines.push("");
@@ -895,12 +595,12 @@ function formatXmlOutline(summary) {
895
595
  for (const [k, v] of Object.entries(node.attributes)) {
896
596
  if (["id", "name", "type", "key", "class", "code", "status", "value"].includes(k.toLowerCase()) || attrKeys.length <= 3) {
897
597
  const valPreview = v.length > 35 ? `${v.slice(0, 32)}...` : v;
898
- attrParts.push(`${k}="${valPreview}"`);
598
+ attrParts.push(`${displaySafeText(k)}="${displaySafeText(valPreview)}"`);
899
599
  }
900
600
  }
901
601
  const attrStr = attrParts.length > 0 ? ` [${attrParts.join(" ")}]` : "";
902
602
  const countStr = node.childCount > 0 ? ` (${node.childCount} children)` : node.textLength > 0 ? ` (~${node.textLength} chars text)` : " (empty)";
903
- lines.push(`${pad}<${node.tag}>${attrStr}${countStr}`);
603
+ lines.push(`${pad}<${displaySafeText(node.tag)}>${attrStr}${countStr}`);
904
604
  for (const child of node.children) {
905
605
  renderNode(child, indent + 1);
906
606
  }
@@ -1137,6 +837,693 @@ function queryXml(xmlText, pathStr) {
1137
837
  return { items: currentCandidates, fanned: hasFanned || currentCandidates.length > 1 };
1138
838
  }
1139
839
 
840
+ // src/html_query.ts
841
+ init_define_import_meta_env();
842
+ var HTML_VOID_TAGS = /* @__PURE__ */ new Set([
843
+ "area",
844
+ "base",
845
+ "br",
846
+ "col",
847
+ "embed",
848
+ "hr",
849
+ "img",
850
+ "input",
851
+ "link",
852
+ "meta",
853
+ "param",
854
+ "source",
855
+ "track",
856
+ "wbr"
857
+ ]);
858
+ var RAW_TEXT_TAGS = /* @__PURE__ */ new Set(["script", "style"]);
859
+ function findTagEnd(html, startPos) {
860
+ let inQuote = null;
861
+ for (let i = startPos + 1; i < html.length; i++) {
862
+ const ch = html[i];
863
+ if (inQuote) {
864
+ if (ch === inQuote) inQuote = null;
865
+ } else {
866
+ if (ch === '"' || ch === "'") {
867
+ inQuote = ch;
868
+ } else if (ch === ">") {
869
+ return i;
870
+ } else if (ch === "<") {
871
+ return -1;
872
+ }
873
+ }
874
+ }
875
+ return -1;
876
+ }
877
+ function findRawTextEnd(html, startPos, tag) {
878
+ const target = `</${tag}`;
879
+ const targetLen = target.length;
880
+ const limit = html.length - targetLen;
881
+ for (let i = startPos; i <= limit; i++) {
882
+ if (html[i] === "<" && html[i + 1] === "/") {
883
+ if (html.slice(i, i + targetLen).toLowerCase() === target) {
884
+ const nextChar = html[i + targetLen];
885
+ if (!nextChar || /\s|>|\//.test(nextChar)) {
886
+ return i;
887
+ }
888
+ }
889
+ }
890
+ }
891
+ return html.length;
892
+ }
893
+ function parseHtml(html) {
894
+ const issues = [];
895
+ const tagCounts = {};
896
+ const root = {
897
+ tag: "#root",
898
+ attributes: {},
899
+ children: [],
900
+ parent: null,
901
+ text: "",
902
+ rawHtml: "",
903
+ startOffset: 0,
904
+ endOffset: html.length,
905
+ line: 1,
906
+ endLine: 1,
907
+ isVoid: false
908
+ };
909
+ let currentParent = root;
910
+ let currentLine = 1;
911
+ let pos = 0;
912
+ const len = html.length;
913
+ const seenIds = /* @__PURE__ */ new Map();
914
+ while (pos < len) {
915
+ if (html[pos] === "\n") {
916
+ currentLine++;
917
+ pos++;
918
+ continue;
919
+ }
920
+ if (html.startsWith("<!--", pos)) {
921
+ const endComment = html.indexOf("-->", pos + 4);
922
+ const commentEndPos = endComment !== -1 ? endComment + 3 : len;
923
+ const commentLines = (html.slice(pos, commentEndPos).match(/\n/g) || []).length;
924
+ currentLine += commentLines;
925
+ pos = commentEndPos;
926
+ continue;
927
+ }
928
+ if (html.startsWith("<!doctype", pos) || html.startsWith("<!DOCTYPE", pos)) {
929
+ const endDoc = html.indexOf(">", pos);
930
+ const docEndPos = endDoc !== -1 ? endDoc + 1 : len;
931
+ const docLines = (html.slice(pos, docEndPos).match(/\n/g) || []).length;
932
+ currentLine += docLines;
933
+ pos = docEndPos;
934
+ continue;
935
+ }
936
+ if (currentParent && RAW_TEXT_TAGS.has(currentParent.tag.toLowerCase())) {
937
+ const closeTag = `</${currentParent.tag}`;
938
+ if (!html.slice(pos, pos + closeTag.length).toLowerCase().startsWith(closeTag.toLowerCase())) {
939
+ const rawTextEnd = findRawTextEnd(html, pos, currentParent.tag.toLowerCase());
940
+ const textContent = html.slice(pos, rawTextEnd);
941
+ currentParent.text += textContent;
942
+ const textLines = (textContent.match(/\n/g) || []).length;
943
+ currentLine += textLines;
944
+ pos = rawTextEnd;
945
+ continue;
946
+ }
947
+ }
948
+ if (html[pos] === "<") {
949
+ const tagStartLine = currentLine;
950
+ const closeBracket = findTagEnd(html, pos);
951
+ if (closeBracket === -1) {
952
+ issues.push({
953
+ line: tagStartLine,
954
+ type: "warning",
955
+ message: "Unescaped '<' character in text: consider '&lt;'",
956
+ rule: "unescaped-angle-bracket"
957
+ });
958
+ if (currentParent) {
959
+ currentParent.text += "<";
960
+ }
961
+ pos++;
962
+ continue;
963
+ }
964
+ const tagSlice = html.slice(pos, closeBracket + 1);
965
+ const sliceLines = (tagSlice.match(/\n/g) || []).length;
966
+ if (html[pos + 1] === "/") {
967
+ const closeMatch = /^<\/([a-zA-Z][a-zA-Z0-9_-]*)\s*>$/i.exec(tagSlice.trim());
968
+ if (closeMatch) {
969
+ const closeTag = closeMatch[1].toLowerCase();
970
+ if (!tagCounts[closeTag]) tagCounts[closeTag] = { open: 0, close: 0, diff: 0 };
971
+ if (!HTML_VOID_TAGS.has(closeTag)) {
972
+ tagCounts[closeTag].close++;
973
+ }
974
+ if (HTML_VOID_TAGS.has(closeTag)) {
975
+ issues.push({
976
+ line: tagStartLine,
977
+ type: "warning",
978
+ message: `Void tag <${closeTag}> should not have a closing tag </${closeTag}>`,
979
+ rule: "void-tag-closing"
980
+ });
981
+ } else {
982
+ let matchNode = currentParent;
983
+ while (matchNode && matchNode.tag !== "#root" && matchNode.tag.toLowerCase() !== closeTag) {
984
+ matchNode = matchNode.parent;
985
+ }
986
+ if (matchNode && matchNode.tag !== "#root") {
987
+ let unclosed = currentParent;
988
+ while (unclosed && unclosed !== matchNode) {
989
+ if (!HTML_VOID_TAGS.has(unclosed.tag.toLowerCase())) {
990
+ issues.push({
991
+ line: unclosed.line,
992
+ type: "error",
993
+ message: `Unclosed tag <${unclosed.tag}> before closing </${closeTag}> on line ${tagStartLine}`,
994
+ rule: "unclosed-tag"
995
+ });
996
+ }
997
+ unclosed.endLine = tagStartLine;
998
+ unclosed.endOffset = pos;
999
+ unclosed = unclosed.parent;
1000
+ }
1001
+ matchNode.endLine = tagStartLine + sliceLines;
1002
+ matchNode.endOffset = closeBracket + 1;
1003
+ currentParent = matchNode.parent || root;
1004
+ } else {
1005
+ issues.push({
1006
+ line: tagStartLine,
1007
+ type: "error",
1008
+ message: `Unexpected closing tag </${closeTag}> with no matching opening tag`,
1009
+ rule: "stray-closing-tag"
1010
+ });
1011
+ }
1012
+ }
1013
+ } else {
1014
+ issues.push({ line: tagStartLine, type: "error", message: `Malformed closing tag: ${tagSlice}`, rule: "syntax" });
1015
+ }
1016
+ currentLine += sliceLines;
1017
+ pos = closeBracket + 1;
1018
+ continue;
1019
+ }
1020
+ let inner = tagSlice.slice(1, -1).trim();
1021
+ let selfClosing = false;
1022
+ if (inner.endsWith("/")) {
1023
+ selfClosing = true;
1024
+ inner = inner.slice(0, -1).trim();
1025
+ }
1026
+ const spaceIdx = inner.search(/\s/);
1027
+ const rawTagName = spaceIdx === -1 ? inner : inner.slice(0, spaceIdx);
1028
+ const attrStr = spaceIdx === -1 ? "" : inner.slice(spaceIdx + 1);
1029
+ if (/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(rawTagName)) {
1030
+ const tagName = rawTagName.toLowerCase();
1031
+ const isVoid = HTML_VOID_TAGS.has(tagName) || selfClosing;
1032
+ if (!tagCounts[tagName]) tagCounts[tagName] = { open: 0, close: 0, diff: 0 };
1033
+ tagCounts[tagName].open++;
1034
+ if (isVoid) {
1035
+ tagCounts[tagName].close++;
1036
+ }
1037
+ const attributes = {};
1038
+ const attrRegex = /([a-zA-Z0-9_:-]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
1039
+ let attrMatch;
1040
+ while ((attrMatch = attrRegex.exec(attrStr)) !== null) {
1041
+ const attrName = attrMatch[1].toLowerCase();
1042
+ const attrVal = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
1043
+ attributes[attrName] = attrVal;
1044
+ if (attrName === "id" && attrVal) {
1045
+ const prevLine = seenIds.get(attrVal);
1046
+ if (prevLine !== void 0) {
1047
+ issues.push({
1048
+ line: tagStartLine,
1049
+ type: "error",
1050
+ message: `Duplicate ID '#${attrVal}' previously defined on line ${prevLine}`,
1051
+ rule: "duplicate-id"
1052
+ });
1053
+ } else {
1054
+ seenIds.set(attrVal, tagStartLine);
1055
+ }
1056
+ }
1057
+ }
1058
+ const newNode = {
1059
+ tag: tagName,
1060
+ attributes,
1061
+ children: [],
1062
+ parent: currentParent,
1063
+ text: "",
1064
+ rawHtml: tagSlice,
1065
+ startOffset: pos,
1066
+ endOffset: closeBracket + 1,
1067
+ line: tagStartLine,
1068
+ endLine: tagStartLine + sliceLines,
1069
+ isVoid
1070
+ };
1071
+ currentParent.children.push(newNode);
1072
+ if (!isVoid) {
1073
+ currentParent = newNode;
1074
+ }
1075
+ currentLine += sliceLines;
1076
+ pos = closeBracket + 1;
1077
+ continue;
1078
+ }
1079
+ issues.push({
1080
+ line: tagStartLine,
1081
+ type: "warning",
1082
+ message: "Unescaped '<' character in text: consider '&lt;'",
1083
+ rule: "unescaped-angle-bracket"
1084
+ });
1085
+ if (currentParent) {
1086
+ currentParent.text += "<";
1087
+ }
1088
+ pos++;
1089
+ continue;
1090
+ }
1091
+ const nextBracket = html.indexOf("<", pos);
1092
+ const textChunkEnd = nextBracket !== -1 ? nextBracket : len;
1093
+ const textChunk = html.slice(pos, textChunkEnd);
1094
+ if (textChunk) {
1095
+ if (currentParent) {
1096
+ currentParent.text += textChunk;
1097
+ }
1098
+ currentLine += (textChunk.match(/\n/g) || []).length;
1099
+ }
1100
+ pos = textChunkEnd;
1101
+ }
1102
+ let dangling = currentParent;
1103
+ while (dangling && dangling.tag !== "#root") {
1104
+ if (!HTML_VOID_TAGS.has(dangling.tag.toLowerCase())) {
1105
+ issues.push({
1106
+ line: dangling.line,
1107
+ type: "error",
1108
+ message: `Unclosed tag <${dangling.tag}> at end of document`,
1109
+ rule: "unclosed-tag"
1110
+ });
1111
+ }
1112
+ dangling.endOffset = len;
1113
+ dangling = dangling.parent;
1114
+ }
1115
+ for (const tag of Object.keys(tagCounts)) {
1116
+ const entry = tagCounts[tag];
1117
+ entry.diff = entry.open - entry.close;
1118
+ }
1119
+ root.endLine = currentLine;
1120
+ return { root, issues, tagCounts };
1121
+ }
1122
+ function lintHtml(htmlText) {
1123
+ const { issues, tagCounts } = parseHtml(htmlText);
1124
+ const errors = issues.filter((i) => i.type === "error");
1125
+ const warnings = issues.filter((i) => i.type === "warning");
1126
+ return {
1127
+ valid: errors.length === 0,
1128
+ errors,
1129
+ warnings,
1130
+ tagCounts
1131
+ };
1132
+ }
1133
+ function outlineHtml(htmlText) {
1134
+ const { root } = parseHtml(htmlText);
1135
+ let title = null;
1136
+ let doctype = null;
1137
+ const docMatch = /<!doctype\s+(\S[^>]*)>/i.exec(htmlText);
1138
+ if (docMatch) doctype = docMatch[1].trim();
1139
+ let totalElements = 0;
1140
+ const uniqueTags = /* @__PURE__ */ new Set();
1141
+ let maxDepth = 0;
1142
+ const headings = [];
1143
+ const landmarks = [];
1144
+ const tables = [];
1145
+ const forms = [];
1146
+ let scripts = 0;
1147
+ let styles = 0;
1148
+ const landmarkTags = /* @__PURE__ */ new Set(["header", "nav", "main", "article", "section", "footer", "aside"]);
1149
+ function walk(node, depth) {
1150
+ if (node.tag !== "#root") {
1151
+ totalElements++;
1152
+ uniqueTags.add(node.tag);
1153
+ if (depth > maxDepth) maxDepth = depth;
1154
+ const tag = node.tag.toLowerCase();
1155
+ if (tag === "title" && !title) {
1156
+ title = node.text.trim();
1157
+ } else if (/^h[1-6]$/.test(tag)) {
1158
+ const level = parseInt(tag[1], 10);
1159
+ headings.push({ level, text: node.text.replace(/\s+/g, " ").trim().slice(0, 80), line: node.line });
1160
+ } else if (landmarkTags.has(tag)) {
1161
+ landmarks.push({
1162
+ tag,
1163
+ id: node.attributes["id"],
1164
+ class: node.attributes["class"],
1165
+ line: node.line
1166
+ });
1167
+ } else if (tag === "table") {
1168
+ const rows = node.children.flatMap((c) => c.tag === "tbody" || c.tag === "thead" ? c.children : [c]).filter((c) => c.tag === "tr").length;
1169
+ tables.push({ id: node.attributes["id"], class: node.attributes["class"], rows, line: node.line });
1170
+ } else if (tag === "form") {
1171
+ forms.push({ id: node.attributes["id"], action: node.attributes["action"], method: node.attributes["method"], line: node.line });
1172
+ } else if (tag === "script") {
1173
+ scripts++;
1174
+ } else if (tag === "style") {
1175
+ styles++;
1176
+ }
1177
+ }
1178
+ for (const child of node.children) {
1179
+ walk(child, depth + 1);
1180
+ }
1181
+ }
1182
+ walk(root, 0);
1183
+ return {
1184
+ title,
1185
+ doctype,
1186
+ totalElements,
1187
+ uniqueTags: Array.from(uniqueTags).sort(),
1188
+ maxDepth,
1189
+ headings,
1190
+ landmarks,
1191
+ tables,
1192
+ forms,
1193
+ scripts,
1194
+ styles
1195
+ };
1196
+ }
1197
+ function formatHtmlOutline(summary) {
1198
+ const lines = [];
1199
+ lines.push(`HTML Document (${summary.totalElements} elements, ${summary.uniqueTags.length} unique tags, max depth ${summary.maxDepth})`);
1200
+ if (summary.title) lines.push(`Title: "${displaySafeText(summary.title)}"`);
1201
+ if (summary.doctype) lines.push(`DOCTYPE: ${displaySafeText(summary.doctype)}`);
1202
+ lines.push(`Assets: ${summary.scripts} scripts, ${summary.styles} stylesheets`);
1203
+ if (summary.headings.length > 0) {
1204
+ lines.push("", "Headings:");
1205
+ for (const h of summary.headings.slice(0, 30)) {
1206
+ lines.push(` ${" ".repeat(h.level - 1)}[h${h.level}] ${displaySafeText(h.text)} (line ${h.line})`);
1207
+ }
1208
+ if (summary.headings.length > 30) {
1209
+ lines.push(` ... (${summary.headings.length - 30} more headings elided)`);
1210
+ }
1211
+ }
1212
+ if (summary.landmarks.length > 0) {
1213
+ lines.push("", "Landmarks:");
1214
+ for (const l of summary.landmarks.slice(0, 25)) {
1215
+ const idStr = l.id ? `#${displaySafeText(l.id)}` : "";
1216
+ const classStr = l.class ? `.${displaySafeText(l.class.trim().split(/\s+/).join("."))}` : "";
1217
+ lines.push(` <${displaySafeText(l.tag)}${idStr}${classStr}> (line ${l.line})`);
1218
+ }
1219
+ }
1220
+ if (summary.tables.length > 0) {
1221
+ lines.push("", "Tables:");
1222
+ for (const t of summary.tables) {
1223
+ const idStr = t.id ? `#${displaySafeText(t.id)}` : "";
1224
+ lines.push(` <table${idStr}> with ~${t.rows} rows (line ${t.line})`);
1225
+ }
1226
+ }
1227
+ if (summary.forms.length > 0) {
1228
+ lines.push("", "Forms:");
1229
+ for (const f of summary.forms) {
1230
+ const idStr = f.id ? `#${displaySafeText(f.id)}` : "";
1231
+ lines.push(` <form${idStr} method="${displaySafeText(f.method || "GET")}" action="${displaySafeText(f.action || "")}"> (line ${f.line})`);
1232
+ }
1233
+ }
1234
+ return lines.join("\n");
1235
+ }
1236
+ function splitTopLevel(str, delimiter) {
1237
+ const results = [];
1238
+ let start = 0;
1239
+ let inQuote = null;
1240
+ let bracketDepth = 0;
1241
+ for (let i = 0; i < str.length; i++) {
1242
+ const ch = str[i];
1243
+ if (inQuote) {
1244
+ if (ch === inQuote) inQuote = null;
1245
+ } else {
1246
+ if (ch === '"' || ch === "'") {
1247
+ inQuote = ch;
1248
+ } else if (ch === "[") {
1249
+ bracketDepth++;
1250
+ } else if (ch === "]") {
1251
+ if (bracketDepth > 0) bracketDepth--;
1252
+ } else if (bracketDepth === 0 && ch === delimiter) {
1253
+ results.push(str.slice(start, i));
1254
+ start = i + 1;
1255
+ }
1256
+ }
1257
+ }
1258
+ results.push(str.slice(start));
1259
+ return results;
1260
+ }
1261
+ function tokenizeSelector(sel) {
1262
+ const tokens = [];
1263
+ let i = 0;
1264
+ const n = sel.length;
1265
+ while (i < n) {
1266
+ while (i < n && /\s/.test(sel[i])) i++;
1267
+ if (i >= n) break;
1268
+ if (sel[i] === ">") {
1269
+ tokens.push(">");
1270
+ i++;
1271
+ continue;
1272
+ }
1273
+ const start = i;
1274
+ let inQuote = null;
1275
+ let bracketDepth = 0;
1276
+ while (i < n) {
1277
+ const ch = sel[i];
1278
+ if (inQuote) {
1279
+ if (ch === inQuote) inQuote = null;
1280
+ } else {
1281
+ if (ch === '"' || ch === "'") {
1282
+ inQuote = ch;
1283
+ } else if (ch === "[") {
1284
+ bracketDepth++;
1285
+ } else if (ch === "]") {
1286
+ if (bracketDepth > 0) bracketDepth--;
1287
+ } else if (bracketDepth === 0) {
1288
+ if (/\s/.test(ch) || ch === ">") {
1289
+ break;
1290
+ }
1291
+ }
1292
+ }
1293
+ i++;
1294
+ }
1295
+ if (i > start) {
1296
+ tokens.push(sel.slice(start, i));
1297
+ }
1298
+ }
1299
+ return tokens;
1300
+ }
1301
+ function parseCompoundSelector(sel) {
1302
+ const result = {
1303
+ ids: [],
1304
+ classes: [],
1305
+ attributes: []
1306
+ };
1307
+ let i = 0;
1308
+ const n = sel.length;
1309
+ if (sel[0] !== "#" && sel[0] !== "." && sel[0] !== "[") {
1310
+ const tagMatch = /^([a-zA-Z*][a-zA-Z0-9_-]*)/.exec(sel);
1311
+ if (tagMatch) {
1312
+ result.tag = tagMatch[1];
1313
+ i = tagMatch[0].length;
1314
+ }
1315
+ }
1316
+ while (i < n) {
1317
+ const ch = sel[i];
1318
+ if (ch === "#") {
1319
+ i++;
1320
+ const idMatch = /^([a-zA-Z0-9_-]+)/.exec(sel.slice(i));
1321
+ if (idMatch) {
1322
+ result.ids.push(idMatch[1]);
1323
+ i += idMatch[0].length;
1324
+ }
1325
+ } else if (ch === ".") {
1326
+ i++;
1327
+ const classMatch = /^([a-zA-Z0-9_-]+)/.exec(sel.slice(i));
1328
+ if (classMatch) {
1329
+ result.classes.push(classMatch[1]);
1330
+ i += classMatch[0].length;
1331
+ }
1332
+ } else if (ch === "[") {
1333
+ let endBracket = -1;
1334
+ let inQuote = null;
1335
+ for (let j = i + 1; j < n; j++) {
1336
+ const c = sel[j];
1337
+ if (inQuote) {
1338
+ if (c === inQuote) inQuote = null;
1339
+ } else {
1340
+ if (c === '"' || c === "'") inQuote = c;
1341
+ else if (c === "]") {
1342
+ endBracket = j;
1343
+ break;
1344
+ }
1345
+ }
1346
+ }
1347
+ if (endBracket === -1) {
1348
+ break;
1349
+ }
1350
+ const inner = sel.slice(i + 1, endBracket).trim();
1351
+ i = endBracket + 1;
1352
+ const attrMatch = /^([a-zA-Z0-9_:-]+)(?:\s*([*^$!~]?=)\s*(?:"([^"]*)"|'([^']*)'|([^\s"'\]]+)))?$/.exec(inner);
1353
+ if (attrMatch) {
1354
+ const attrName = attrMatch[1].toLowerCase();
1355
+ const op = attrMatch[2];
1356
+ const val = attrMatch[3] ?? attrMatch[4] ?? attrMatch[5];
1357
+ result.attributes.push({ name: attrName, op, val });
1358
+ }
1359
+ } else {
1360
+ i++;
1361
+ }
1362
+ }
1363
+ return result;
1364
+ }
1365
+ function matchesCompoundSelector(node, sel) {
1366
+ if (!sel || node.tag === "#root") return false;
1367
+ if (sel === "*") return true;
1368
+ const parsed = parseCompoundSelector(sel);
1369
+ if (parsed.tag && parsed.tag !== "*") {
1370
+ if (node.tag.toLowerCase() !== parsed.tag.toLowerCase()) return false;
1371
+ }
1372
+ for (const id of parsed.ids) {
1373
+ if (node.attributes["id"] !== id) return false;
1374
+ }
1375
+ if (parsed.classes.length > 0) {
1376
+ const nodeClasses = (node.attributes["class"] || "").split(/\s+/).filter(Boolean);
1377
+ for (const c of parsed.classes) {
1378
+ if (!nodeClasses.includes(c)) return false;
1379
+ }
1380
+ }
1381
+ for (const attr of parsed.attributes) {
1382
+ const hasAttr = Object.prototype.hasOwnProperty.call(node.attributes, attr.name);
1383
+ if (!hasAttr) {
1384
+ if (attr.op === "!=") continue;
1385
+ return false;
1386
+ }
1387
+ if (!attr.op) continue;
1388
+ const actual = node.attributes[attr.name] || "";
1389
+ const val = attr.val ?? "";
1390
+ if (attr.op === "=") {
1391
+ if (actual !== val) return false;
1392
+ } else if (attr.op === "^=") {
1393
+ if (!actual.startsWith(val)) return false;
1394
+ } else if (attr.op === "$=") {
1395
+ if (!actual.endsWith(val)) return false;
1396
+ } else if (attr.op === "*=") {
1397
+ if (!actual.includes(val)) return false;
1398
+ } else if (attr.op === "~=") {
1399
+ if (!actual.split(/\s+/).includes(val)) return false;
1400
+ } else if (attr.op === "!=") {
1401
+ if (actual === val) return false;
1402
+ }
1403
+ }
1404
+ return true;
1405
+ }
1406
+ function queryHtml(htmlText, selectorStr) {
1407
+ const { root } = parseHtml(htmlText);
1408
+ let selector = selectorStr.trim();
1409
+ let targetAttr;
1410
+ const attrAtMatch = /@([a-zA-Z0-9_:-]+)$/.exec(selector);
1411
+ if (attrAtMatch) {
1412
+ targetAttr = attrAtMatch[1].toLowerCase();
1413
+ selector = selector.slice(0, selector.length - attrAtMatch[0].length).trim();
1414
+ if (!selector) selector = "*";
1415
+ }
1416
+ const rawSubSelectors = splitTopLevel(selector, ",").map((s) => s.trim()).filter(Boolean);
1417
+ const matchedSet = /* @__PURE__ */ new Set();
1418
+ for (const subSel of rawSubSelectors) {
1419
+ let collectAll2 = function(node) {
1420
+ if (node.tag !== "#root") currentCandidates.push(node);
1421
+ for (const c of node.children) collectAll2(c);
1422
+ };
1423
+ var collectAll = collectAll2;
1424
+ const tokens = tokenizeSelector(subSel);
1425
+ let currentCandidates = [];
1426
+ collectAll2(root);
1427
+ let i = 0;
1428
+ while (i < tokens.length) {
1429
+ const token = tokens[i];
1430
+ if (token === ">") {
1431
+ const nextTarget = tokens[i + 1];
1432
+ if (!nextTarget) break;
1433
+ const nextCandidates = [];
1434
+ for (const parent of currentCandidates) {
1435
+ for (const child of parent.children) {
1436
+ if (nextTarget === "*" || matchesCompoundSelector(child, nextTarget)) {
1437
+ nextCandidates.push(child);
1438
+ }
1439
+ }
1440
+ }
1441
+ currentCandidates = nextCandidates;
1442
+ i += 2;
1443
+ } else {
1444
+ if (i === 0) {
1445
+ currentCandidates = currentCandidates.filter((node) => token === "*" || matchesCompoundSelector(node, token));
1446
+ } else {
1447
+ const nextSet = /* @__PURE__ */ new Set();
1448
+ for (const ancestor of currentCandidates) {
1449
+ let findDescendants2 = function(node) {
1450
+ for (const child of node.children) {
1451
+ if (token === "*" || matchesCompoundSelector(child, token)) {
1452
+ nextSet.add(child);
1453
+ }
1454
+ findDescendants2(child);
1455
+ }
1456
+ };
1457
+ var findDescendants = findDescendants2;
1458
+ findDescendants2(ancestor);
1459
+ }
1460
+ currentCandidates = Array.from(nextSet);
1461
+ }
1462
+ i++;
1463
+ }
1464
+ }
1465
+ for (const match of currentCandidates) {
1466
+ matchedSet.add(match);
1467
+ }
1468
+ }
1469
+ const elements = Array.from(matchedSet);
1470
+ if (targetAttr !== void 0) {
1471
+ const attributeValues = [];
1472
+ for (const el of elements) {
1473
+ if (Object.prototype.hasOwnProperty.call(el.attributes, targetAttr)) {
1474
+ attributeValues.push(el.attributes[targetAttr]);
1475
+ }
1476
+ }
1477
+ return { elements, attributeValues, fanned: attributeValues.length > 1, sourceHtml: htmlText };
1478
+ }
1479
+ return { elements, fanned: elements.length > 1, sourceHtml: htmlText };
1480
+ }
1481
+ function extractNodeText(node, sourceHtml) {
1482
+ if (RAW_TEXT_TAGS.has(node.tag.toLowerCase())) return "";
1483
+ if (sourceHtml && node.startOffset !== void 0 && node.endOffset !== void 0) {
1484
+ const raw = sourceHtml.slice(node.startOffset, node.endOffset);
1485
+ const clean = raw.replace(/<!--[\s\S]*?-->/g, "").replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
1486
+ return clean.replace(/\s+/g, " ").trim();
1487
+ }
1488
+ const parts = [];
1489
+ if (node.text.trim()) parts.push(node.text.trim());
1490
+ for (const child of node.children) {
1491
+ const t = extractNodeText(child);
1492
+ if (t) parts.push(t);
1493
+ }
1494
+ return parts.join(" ");
1495
+ }
1496
+ function serializeHtmlNode(node, indent = 0, sourceHtml) {
1497
+ if (node.tag === "#root") {
1498
+ return node.children.map((c) => serializeHtmlNode(c, indent, sourceHtml)).join("\n");
1499
+ }
1500
+ if (sourceHtml && node.startOffset !== void 0 && node.endOffset !== void 0) {
1501
+ return sourceHtml.slice(node.startOffset, node.endOffset);
1502
+ }
1503
+ const pad = " ".repeat(indent);
1504
+ const attrs = Object.entries(node.attributes).map(([k, v]) => v !== "" ? `${k}="${v.replace(/"/g, "&quot;")}"` : k).join(" ");
1505
+ const attrStr = attrs ? ` ${attrs}` : "";
1506
+ if (node.isVoid) {
1507
+ return `${pad}<${node.tag}${attrStr}>`;
1508
+ }
1509
+ if (node.children.length === 0) {
1510
+ const textTrimmed = node.text.trim();
1511
+ if (!textTrimmed) {
1512
+ return `${pad}<${node.tag}${attrStr}></${node.tag}>`;
1513
+ }
1514
+ if (!textTrimmed.includes("\n") && textTrimmed.length < 80) {
1515
+ return `${pad}<${node.tag}${attrStr}>${textTrimmed}</${node.tag}>`;
1516
+ }
1517
+ return `${pad}<${node.tag}${attrStr}>
1518
+ ${pad} ${textTrimmed}
1519
+ ${pad}</${node.tag}>`;
1520
+ }
1521
+ const inner = node.children.map((c) => serializeHtmlNode(c, indent + 1, sourceHtml)).join("\n");
1522
+ return `${pad}<${node.tag}${attrStr}>
1523
+ ${inner}
1524
+ ${pad}</${node.tag}>`;
1525
+ }
1526
+
1140
1527
  // node_modules/js-yaml/dist/js-yaml.mjs
1141
1528
  init_define_import_meta_env();
1142
1529
  var NOT_RESOLVED = /* @__PURE__ */ Symbol("NOT_RESOLVED");
@@ -3712,17 +4099,17 @@ function findOperation(operations, key) {
3712
4099
  return operations.find((op) => op.method === method && op.path === opPath);
3713
4100
  }
3714
4101
  function operationLabel(op) {
3715
- return op.operationId !== void 0 ? `${op.operationId} (${op.method} ${op.path})` : `${op.method} ${op.path}`;
4102
+ return op.operationId !== void 0 ? `${displaySafeText(op.operationId)} (${displaySafeText(op.method)} ${displaySafeText(op.path)})` : `${displaySafeText(op.method)} ${displaySafeText(op.path)}`;
3716
4103
  }
3717
4104
  function indentedJson(value, indent) {
3718
4105
  return JSON.stringify(value, null, 2).split("\n").map((line) => indent + line).join("\n");
3719
4106
  }
3720
4107
  function formatOperationDetail(op) {
3721
- const lines = [`${op.method} ${op.path}`];
3722
- if (op.operationId !== void 0) lines.push(`operationId: ${op.operationId}`);
3723
- if (op.summary !== void 0) lines.push(`summary: ${op.summary}`);
3724
- if (op.description !== void 0) lines.push(`description: ${op.description}`);
3725
- if (op.tags !== void 0 && op.tags.length > 0) lines.push(`tags: ${op.tags.join(", ")}`);
4108
+ const lines = [`${displaySafeText(op.method)} ${displaySafeText(op.path)}`];
4109
+ if (op.operationId !== void 0) lines.push(`operationId: ${displaySafeText(op.operationId)}`);
4110
+ if (op.summary !== void 0) lines.push(`summary: ${displaySafeText(op.summary)}`);
4111
+ if (op.description !== void 0) lines.push(`description: ${displaySafeText(op.description)}`);
4112
+ if (op.tags !== void 0 && op.tags.length > 0) lines.push(`tags: ${op.tags.map(displaySafeText).join(", ")}`);
3726
4113
  if (op.parameters.length > 0) {
3727
4114
  lines.push("", "parameters:");
3728
4115
  for (const paramRaw of op.parameters) {
@@ -3732,7 +4119,7 @@ function formatOperationDetail(op) {
3732
4119
  const location = typeof param["in"] === "string" ? param["in"] : "?";
3733
4120
  const required = param["required"] === true ? " required" : "";
3734
4121
  const description = typeof param["description"] === "string" ? ` - ${param["description"]}` : "";
3735
- lines.push(` ${name} (${location})${required}${description}`);
4122
+ lines.push(` ${displaySafeText(name)} (${displaySafeText(location)})${required}${displaySafeText(description)}`);
3736
4123
  if (param["schema"] !== void 0) lines.push(indentedJson(param["schema"], " "));
3737
4124
  }
3738
4125
  }
@@ -4001,10 +4388,10 @@ function isSqliteFile(filePath) {
4001
4388
  }
4002
4389
  function openReadonlySqlite(filePath) {
4003
4390
  if (!fs.existsSync(filePath)) {
4004
- throw new Error(`file not found: ${filePath}`);
4391
+ throw new Error(`file not found: ${displaySafeText(filePath)}`);
4005
4392
  }
4006
4393
  if (!isSqliteFile(filePath)) {
4007
- throw new Error(`not a valid SQLite database (bad file header): ${filePath}`);
4394
+ throw new Error(`not a valid SQLite database (bad file header): ${displaySafeText(filePath)}`);
4008
4395
  }
4009
4396
  try {
4010
4397
  return new Database(filePath, { readonly: true, fileMustExist: true });
@@ -4057,12 +4444,12 @@ function formatSqliteSchema(result) {
4057
4444
  const lines = [`${t.name} (${t.kind}${t.rowCount !== null ? `, ${t.rowCount} row${t.rowCount === 1 ? "" : "s"}` : ""})`];
4058
4445
  for (const c of t.columns) {
4059
4446
  const flags = [c.primaryKey ? "PK" : null, c.notNull ? "NOT NULL" : null, c.defaultValue !== null ? `DEFAULT ${c.defaultValue}` : null].filter((f) => f !== null).join(" ");
4060
- lines.push(` ${c.name} ${c.type || "(untyped)"}${flags ? ` ${flags}` : ""}`);
4447
+ lines.push(` ${displaySafeText(c.name)} ${displaySafeText(c.type || "(untyped)")}${flags ? ` ${flags}` : ""}`);
4061
4448
  }
4062
4449
  if (t.indexes.length > 0) {
4063
4450
  lines.push(" indexes:");
4064
4451
  for (const idx of t.indexes) {
4065
- lines.push(` ${idx.name}${idx.unique ? " (unique)" : ""}: ${idx.columns.join(", ")}`);
4452
+ lines.push(` ${displaySafeText(idx.name)}${idx.unique ? " (unique)" : ""}: ${displaySafeText(idx.columns.join(", "))}`);
4066
4453
  }
4067
4454
  }
4068
4455
  if (t.foreignKeys.length > 0) {
@@ -4514,7 +4901,7 @@ function formatRanges(ranges) {
4514
4901
  return ranges.map((r) => r.start === r.end ? String(r.start) : `${r.start}-${r.end}`).join(", ");
4515
4902
  }
4516
4903
  function formatFileGaps(f) {
4517
- const lines = [f.filePath];
4904
+ const lines = [displaySafeText(f.filePath)];
4518
4905
  lines.push(` lines: ${f.linesHit}/${f.linesTotal} functions: ${f.functionsHit}/${f.functionsTotal} branches: ${f.branchesHit}/${f.branchesTotal}`);
4519
4906
  if (f.summaryOnly) {
4520
4907
  lines.push(" (summary-only report -- no per-line/function/branch detail available)");
@@ -4525,7 +4912,7 @@ function formatFileGaps(f) {
4525
4912
  }
4526
4913
  if (f.uncoveredFunctions.length > 0) {
4527
4914
  lines.push(" uncovered functions:");
4528
- for (const fn of f.uncoveredFunctions) lines.push(` ${fn.name} (line ${fn.line})`);
4915
+ for (const fn of f.uncoveredFunctions) lines.push(` ${displaySafeText(fn.name)} (line ${fn.line})`);
4529
4916
  }
4530
4917
  if (f.uncoveredBranches.length > 0) {
4531
4918
  const branchLines = [...new Set(f.uncoveredBranches.map((br) => br.line))].sort((a, b) => a - b);
@@ -4653,25 +5040,25 @@ function indentBlock(content) {
4653
5040
  function formatSingleFileConflicts(result) {
4654
5041
  const lines = [];
4655
5042
  if (result.regions.length > 0) {
4656
- lines.push(`${result.filePath} -- ${result.regions.length} conflict${result.regions.length === 1 ? "" : "s"}`);
5043
+ lines.push(`${displaySafeText(result.filePath)} -- ${result.regions.length} conflict${result.regions.length === 1 ? "" : "s"}`);
4657
5044
  for (const r of result.regions) {
4658
5045
  lines.push(` lines ${r.lineStart}-${r.lineEnd}`);
4659
- lines.push(` <<<<<<< ${r.ours.label || "(ours)"}`);
5046
+ lines.push(` <<<<<<< ${displaySafeText(r.ours.label || "(ours)")}`);
4660
5047
  lines.push(indentBlock(r.ours.content));
4661
5048
  if (r.base !== null) {
4662
- lines.push(` ||||||| ${r.base.label || "(base)"}`);
5049
+ lines.push(` ||||||| ${displaySafeText(r.base.label || "(base)")}`);
4663
5050
  lines.push(indentBlock(r.base.content));
4664
5051
  }
4665
5052
  lines.push(" =======");
4666
5053
  lines.push(indentBlock(r.theirs.content));
4667
- lines.push(` >>>>>>> ${r.theirs.label || "(theirs)"}`);
5054
+ lines.push(` >>>>>>> ${displaySafeText(r.theirs.label || "(theirs)")}`);
4668
5055
  }
4669
5056
  } else {
4670
- lines.push(`${result.filePath} -- no conflicts`);
5057
+ lines.push(`${displaySafeText(result.filePath)} -- no conflicts`);
4671
5058
  }
4672
5059
  if (result.warnings.length > 0) {
4673
5060
  lines.push(" warnings:");
4674
- for (const w of result.warnings) lines.push(` line ${w.line}: ${w.message}`);
5061
+ for (const w of result.warnings) lines.push(` line ${w.line}: ${displaySafeText(w.message)}`);
4675
5062
  }
4676
5063
  return lines.join("\n");
4677
5064
  }
@@ -4683,17 +5070,17 @@ function formatConflicts(results) {
4683
5070
  function formatSingleFileSummary(summary) {
4684
5071
  const lines = [];
4685
5072
  if (summary.conflictCount > 0) {
4686
- lines.push(`${summary.filePath} -- ${summary.conflictCount} conflict${summary.conflictCount === 1 ? "" : "s"}`);
5073
+ lines.push(`${displaySafeText(summary.filePath)} -- ${summary.conflictCount} conflict${summary.conflictCount === 1 ? "" : "s"}`);
4687
5074
  for (const r of summary.regions) {
4688
- const baseText = r.baseLabel !== null ? ` base=${r.baseLabel || "(unlabeled)"}` : "";
4689
- lines.push(` lines ${r.lineStart}-${r.lineEnd} ours=${r.oursLabel || "(unlabeled)"}${baseText} theirs=${r.theirsLabel || "(unlabeled)"}`);
5075
+ const baseText = r.baseLabel !== null ? ` base=${displaySafeText(r.baseLabel || "(unlabeled)")}` : "";
5076
+ lines.push(` lines ${r.lineStart}-${r.lineEnd} ours=${displaySafeText(r.oursLabel || "(unlabeled)")}${baseText} theirs=${displaySafeText(r.theirsLabel || "(unlabeled)")}`);
4690
5077
  }
4691
5078
  } else {
4692
- lines.push(`${summary.filePath} -- no conflicts`);
5079
+ lines.push(`${displaySafeText(summary.filePath)} -- no conflicts`);
4693
5080
  }
4694
5081
  if (summary.warnings.length > 0) {
4695
5082
  lines.push(" warnings:");
4696
- for (const w of summary.warnings) lines.push(` line ${w.line}: ${w.message}`);
5083
+ for (const w of summary.warnings) lines.push(` line ${w.line}: ${displaySafeText(w.message)}`);
4697
5084
  }
4698
5085
  return lines.join("\n");
4699
5086
  }
@@ -4765,36 +5152,12 @@ function resolveBrowserExecutablePath(explicit) {
4765
5152
  }
4766
5153
  return null;
4767
5154
  }
4768
- function isBlockedIpv4Octets(a, b) {
4769
- if (a === 127) return true;
4770
- if (a === 169 && b === 254) return true;
4771
- if (a === 10) return true;
4772
- if (a === 172 && b >= 16 && b <= 31) return true;
4773
- if (a === 192 && b === 168) return true;
4774
- if (a === 0) return true;
4775
- return false;
4776
- }
4777
- function isBlockedIpv6(groups) {
4778
- if (groups.every((g) => g === 0)) return true;
4779
- if (groups.slice(0, 7).every((g) => g === 0) && groups[7] === 1) return true;
4780
- if (groups.slice(0, 5).every((g) => g === 0) && (groups[5] === 65535 || groups[5] === 0)) {
4781
- const embedded = groups[6];
4782
- return isBlockedIpv4Octets(embedded >> 8, embedded & 255);
4783
- }
4784
- if (groups[0] === 100 && groups[1] === 65435 && groups.slice(2, 6).every((g) => g === 0)) {
4785
- const embedded = groups[6];
4786
- return isBlockedIpv4Octets(embedded >> 8, embedded & 255);
4787
- }
4788
- if ((groups[0] & 65472) === 65152) return true;
4789
- if ((groups[0] & 65024) === 64512) return true;
4790
- return false;
4791
- }
4792
5155
  function isBlockedIpAddress(addr) {
4793
5156
  const bare = addr.replace(/^\[/, "").replace(/\]$/, "");
4794
5157
  const v4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(bare);
4795
- if (v4) return isBlockedIpv4Octets(Number(v4[1]), Number(v4[2]));
5158
+ if (v4) return isPrivateIpv4Octets(Number(v4[1]), Number(v4[2]), Number(v4[3]));
4796
5159
  const groups = parseIpv6Groups(bare);
4797
- return groups !== null && isBlockedIpv6(groups);
5160
+ return groups !== null && isPrivateIpv6Groups(groups);
4798
5161
  }
4799
5162
  function blockedAddressAmong(addresses) {
4800
5163
  return addresses.find((addr) => isBlockedIpAddress(addr)) ?? null;
@@ -5063,11 +5426,11 @@ function bodyFromSource(entry) {
5063
5426
  }
5064
5427
  }
5065
5428
  function symbolNamesInFile(filePath, dbPath = globalDbPath()) {
5066
- const symbols = querySymbols({ filePath, limit: 1e5 }, dbPath);
5429
+ const symbols = querySymbols({ filePath, limit: -1 }, dbPath);
5067
5430
  return [...new Set(symbols.map((s) => s.name))].sort();
5068
5431
  }
5069
5432
  function computeFileFingerprint(filePath, dbPath = globalDbPath()) {
5070
- const symbols = querySymbols({ filePath, limit: 1e6 }, dbPath);
5433
+ const symbols = querySymbols({ filePath, limit: -1 }, dbPath);
5071
5434
  const manifest = symbols.map((s) => `${s.name}:${s.kind}:${s.lineStart}-${s.lineEnd}`).sort().join("\n");
5072
5435
  return fingerprintContent(manifest);
5073
5436
  }
@@ -5291,6 +5654,39 @@ function findIdentifierNearPosition(ts, sourceFile, line0, position, name) {
5291
5654
  }
5292
5655
 
5293
5656
  // src/read_commands.ts
5657
+ var STATEMENT_ADAPTER_IMPORTS = new Map([
5658
+ [".abap", extractAbap],
5659
+ [".sas", extractSas],
5660
+ [".pli", extractPli],
5661
+ [".pl1", extractPli],
5662
+ [".rpgle", extractRpg],
5663
+ [".sqlrpgle", extractRpg],
5664
+ [".jcl", extractJcl],
5665
+ ...[".f", ".for", ".f77", ".f90", ".f95", ".f03", ".f08"].map((e) => [e, extractFortran]),
5666
+ ...[".pas", ".dpr", ".dpk", ".lpr"].map((e) => [e, extractPascal]),
5667
+ [".cmake", extractCmake],
5668
+ ...[".s", ".asm", ".nasm"].map((e) => [e, extractAsm]),
5669
+ ...[".bat", ".cmd"].map((e) => [e, extractBatch]),
5670
+ ...[".erl", ".hrl"].map((e) => [e, extractErlang])
5671
+ ]);
5672
+ var BRACE_ADAPTER_IMPORTS = new Map([
5673
+ [".mm", extractObjc],
5674
+ [".groovy", extractGroovy],
5675
+ [".gvy", extractGroovy],
5676
+ [".gradle", extractGroovy],
5677
+ [".pm", extractPerl],
5678
+ [".sol", extractSolidity],
5679
+ [".thrift", extractThrift],
5680
+ ...[".glsl", ".vert", ".frag", ".comp", ".geom", ".tesc", ".tese", ".hlsl", ".hlsli", ".metal"].map((e) => [e, extractCShader])
5681
+ ]);
5682
+ function braceAdapterImportsFor(e, text) {
5683
+ if (e === ".m") return isObjcSource(text) ? extractObjc : isMatlabSource(text) ? extractMatlab : void 0;
5684
+ if (e === ".pp") return isPascalSource(text) ? extractPascal : void 0;
5685
+ if (e === ".h") return isObjcHeader(text) ? extractObjc : void 0;
5686
+ if (e === ".pl") return isPrologSource(text) ? void 0 : extractPerl;
5687
+ if (e === ".t") return isPerlSource(text) ? extractPerl : void 0;
5688
+ return BRACE_ADAPTER_IMPORTS.get(e);
5689
+ }
5294
5690
  var DIDYOUMEAN_LIMIT = 5;
5295
5691
  var SYMBOL_PREVIEW_LINES = 5;
5296
5692
  var AMBIGUOUS_HEADING_LIMIT = 10;
@@ -5482,6 +5878,24 @@ function healStaleIndex(resolvedPath) {
5482
5878
  if (err instanceof ConfinementIdentityError) throw err;
5483
5879
  }
5484
5880
  }
5881
+ var STALE_CHECK_FILE_CAP = 25;
5882
+ function warnIfFilesStale(filePaths) {
5883
+ const checked = /* @__PURE__ */ new Set();
5884
+ let staleCount = 0;
5885
+ for (const raw of filePaths) {
5886
+ if (checked.size >= STALE_CHECK_FILE_CAP) break;
5887
+ if (checked.has(raw)) continue;
5888
+ checked.add(raw);
5889
+ if (staleWarning(raw) === "") continue;
5890
+ staleCount++;
5891
+ healStaleIndex(raw);
5892
+ }
5893
+ if (staleCount > 0) {
5894
+ console.warn(
5895
+ `token-goat: ${countNoun(staleCount, "file")} behind these results changed on disk since the index last saw ${staleCount === 1 ? "it" : "them"} -- a reindex just ran, so a repeat of this command will reflect the current version.`
5896
+ );
5897
+ }
5898
+ }
5485
5899
  function emit(text) {
5486
5900
  const out = colorStdout() ? text : stripAnsi(text);
5487
5901
  process.stdout.write(ensureNewline(out));
@@ -5499,6 +5913,9 @@ function guardText(text, command) {
5499
5913
  function fenceGithubText(text) {
5500
5914
  return fenceUntrusted(text, UNTRUSTED_GITHUB_TAG);
5501
5915
  }
5916
+ function fenceHtmlText(text) {
5917
+ return fenceUntrusted(text, UNTRUSTED_HTML_TAG);
5918
+ }
5502
5919
  function fenceGithubFieldIfMatched(text) {
5503
5920
  const matches = scanAndRecord(text);
5504
5921
  if (matches.length === 0) return text;
@@ -5642,7 +6059,7 @@ function formatBareNameSpecError(command, name, projectRoot) {
5642
6059
  if (specs.length === 0) {
5643
6060
  return `Invalid spec - expected "file::symbol", got: ${name}`;
5644
6061
  }
5645
- const lines = [`Not a file: '${name}'. Did you mean:`];
6062
+ const lines = [`Not a file: '${displaySafeText(name)}'. Did you mean:`];
5646
6063
  for (const spec of specs.slice(0, DIDYOUMEAN_LIMIT)) {
5647
6064
  lines.push(` - token-goat ${command} "${spec}"`);
5648
6065
  }
@@ -5676,7 +6093,7 @@ function formatCrossFileLead(command, name, excludeFilePath, projectRoot) {
5676
6093
  return lines.join("\n");
5677
6094
  }
5678
6095
  function resolveEnclosingSymbol(filePath, chunkStartLine) {
5679
- const symbols = querySymbols({ filePath, limit: 1e5 }, globalDbPath());
6096
+ const symbols = querySymbols({ filePath, limit: -1 }, globalDbPath());
5680
6097
  let best = null;
5681
6098
  for (const s of symbols) {
5682
6099
  if (s.lineStart <= chunkStartLine && chunkStartLine <= s.lineEnd) {
@@ -5685,7 +6102,7 @@ function resolveEnclosingSymbol(filePath, chunkStartLine) {
5685
6102
  }
5686
6103
  }
5687
6104
  }
5688
- return best === null ? null : { name: best.name, kind: best.kind };
6105
+ return best === null ? null : { name: best.name, kind: best.kind, lineStart: best.lineStart };
5689
6106
  }
5690
6107
  function trimBlankLines(lines) {
5691
6108
  let start = 0;
@@ -5802,13 +6219,13 @@ function runSymbol(opts) {
5802
6219
  const label = opts.name ?? opts.grep ?? "*";
5803
6220
  const notice = `no non-test matches for '${label}' (${excludeTestsHiddenNote(hiddenByExcludeTests)})`;
5804
6221
  if (opts.json === true) {
5805
- return { text: JSON.stringify({ items: [], truncated: false, totalCount: 0 }, null, 2), code: 0 };
6222
+ return { text: displaySafeJson({ items: [], truncated: false, totalCount: 0 }), code: 0 };
5806
6223
  }
5807
6224
  return { text: `token-goat: ${notice}`, code: 0 };
5808
6225
  }
5809
6226
  if (matchesGrep !== void 0 && filtered.length === 0 && preFilterCount > 0) {
5810
6227
  if (opts.json === true) {
5811
- const text2 = JSON.stringify({ items: [], truncated: false, totalCount: 0 }, null, 2);
6228
+ const text2 = displaySafeJson({ items: [], truncated: false, totalCount: 0 });
5812
6229
  return { text: text2, code: 0 };
5813
6230
  }
5814
6231
  return { text: grepFilteredToEmptyNotice(preFilterCount, opts.grep ?? "", "symbol", "symbols"), code: 0 };
@@ -5876,7 +6293,7 @@ ${emptyIndexMessage(emptyIndexRoot)}`;
5876
6293
  ...refCounts !== void 0 ? { refCount: refCounts.get(s.name) ?? 0, hasDoc: hasRealDocstring(s.docstring) } : {}
5877
6294
  }));
5878
6295
  const payload = { items, truncated: truncatedFlag, totalCount: trueTotal };
5879
- const text2 = JSON.stringify(payload, null, 2);
6296
+ const text2 = displaySafeJson(payload);
5880
6297
  recordReadStat("symbol_lookup", fullSourceBytes, text2, opts.name ?? opts.file ?? opts.grep);
5881
6298
  return { text: text2, code: 0 };
5882
6299
  }
@@ -5990,7 +6407,7 @@ function runLineRange(range, opts) {
5990
6407
  const clampedEnd = Math.min(end, allLines.length);
5991
6408
  const slice = allLines.slice(start - 1, clampedEnd);
5992
6409
  if (opts.json === true) {
5993
- return { text: JSON.stringify({ file, start, end: clampedEnd, lines: slice }, null, 2), code: 0 };
6410
+ return { text: displaySafeJson({ file, start, end: clampedEnd, lines: slice }), code: 0 };
5994
6411
  }
5995
6412
  const tok = Math.ceil(slice.join("\n").length / 4);
5996
6413
  return {
@@ -6022,7 +6439,8 @@ function formatAmbiguity(symbol, file, candidates, explicitRoot, commandName = "
6022
6439
  const multiFile = new Set(candidates.map((c) => c.filePath)).size > 1;
6023
6440
  const displayRoot = getDisplayRoot(explicitRoot);
6024
6441
  const lines = [
6025
- `Ambiguous symbol '${symbol}' in '${file}': ${countNoun(candidates.length, "definition")} match. Retry with one of the qualified commands below to pick one:`
6442
+ // A symbol name and a file name are chosen by the repository, and this sentence is token-goat instructing the reader what to do next, so a name shaped like a marker would read as part of that instruction.
6443
+ `Ambiguous symbol '${displaySafeText(symbol)}' in '${displaySafeText(file)}': ${countNoun(candidates.length, "definition")} match. Retry with one of the qualified commands below to pick one:`
6026
6444
  ];
6027
6445
  const fileSymCache = /* @__PURE__ */ new Map();
6028
6446
  const getFileSyms = (filePath) => {
@@ -6133,7 +6551,16 @@ function resolveSymbolSpec(spec, forceRefresh, projectRoot) {
6133
6551
  if (cParent.toLowerCase() === symBaseLower) return true;
6134
6552
  if (cParent === "" && c.docstring.toLowerCase() === symBaseLower) return true;
6135
6553
  return containers.some(
6136
- (cls) => cls.filePath === c.filePath && c.lineStart >= cls.lineStart && c.lineEnd <= cls.lineEnd
6554
+ (cls) => cls.filePath === c.filePath && // A container is never its own containment match (mirrors findParentName's `sameSpan`
6555
+ // exclusion above): without this, a same-named non-nesting pair on adjacent spans --
6556
+ // e.g. an HTML `heading` symbol and an unrelated `html_id` symbol that both happen to
6557
+ // be named "Overview" -- lets the OUTER one satisfy containment against ITSELF (its
6558
+ // own span trivially contains itself), so `Overview.Overview` (the exact qualifier
6559
+ // `formatAmbiguity` printed as the retry hint for the html_id candidate, since
6560
+ // findParentName resolved its enclosing heading's name to the same string) kept both
6561
+ // candidates in scope and reported ambiguous again -- a disambiguation hint that could
6562
+ // never resolve the ambiguity it was emitted for.
6563
+ !(cls.lineStart === c.lineStart && cls.lineEnd === c.lineEnd) && c.lineStart >= cls.lineStart && c.lineEnd <= cls.lineEnd
6137
6564
  );
6138
6565
  });
6139
6566
  if (scoped.length > 0) candidates = scoped;
@@ -6186,13 +6613,17 @@ function runRead(opts) {
6186
6613
  const closes = rankSimilarNames(scanned, symbol);
6187
6614
  if (closes.length > 0) messages.push(didYouMean(closes));
6188
6615
  else if (scanned.length > 0) messages.push(`Try: token-goat outline ${file}`);
6616
+ else if (fs4.existsSync(resolved)) {
6617
+ const gap = symbolExtractorGap(file, resolved);
6618
+ if (gap !== void 0) messages.push(gap);
6619
+ }
6189
6620
  return { text: messages.join("\n"), code: 1 };
6190
6621
  }
6191
6622
  const match = resolution.entry;
6192
6623
  const fullSourceBytes = sumFileSizes([match.filePath]);
6193
6624
  const refCounts = opts.stats === true ? queryRefCounts([match.name], globalDbPath(), resolveProjectRoot({ project: opts.projectRoot ?? process.cwd() })) : void 0;
6194
6625
  if (opts.json === true) {
6195
- const text2 = JSON.stringify(
6626
+ const text2 = displaySafeJson(
6196
6627
  {
6197
6628
  ...match,
6198
6629
  body: resolveBody(match),
@@ -6200,9 +6631,7 @@ function runRead(opts) {
6200
6631
  // would be the one surface that still passes a deleted file's body off as a live read.
6201
6632
  ...fileIsGone(match.filePath) ? { deleted: true } : {},
6202
6633
  ...refCounts !== void 0 ? { refCount: refCounts.get(match.name) ?? 0 } : {}
6203
- },
6204
- null,
6205
- 2
6634
+ }
6206
6635
  );
6207
6636
  if (opts.suppressStat !== true) recordReadStat("read_replacement", fullSourceBytes, text2, opts.spec);
6208
6637
  return { text: text2, code: 0 };
@@ -6238,11 +6667,11 @@ ${sub.text}`);
6238
6667
  }
6239
6668
  if (anyFound) {
6240
6669
  const fullSourceBytes = sumFileSizes(Array.from(distinctFiles, (f) => resolveIndexPath(f, opts.projectRoot ?? process.cwd())));
6241
- const text2 = opts.json === true ? JSON.stringify(jsonOut, null, 2) : textBlocks.join("\n\n");
6670
+ const text2 = opts.json === true ? displaySafeJson(jsonOut) : textBlocks.join("\n\n");
6242
6671
  recordReadStat("read_replacement", fullSourceBytes, text2, opts.spec);
6243
6672
  return { text: text2, code: 0 };
6244
6673
  }
6245
- const text = opts.json === true ? JSON.stringify(jsonOut, null, 2) : textBlocks.join("\n\n");
6674
+ const text = opts.json === true ? displaySafeJson(jsonOut) : textBlocks.join("\n\n");
6246
6675
  return { text, code: 1 };
6247
6676
  }
6248
6677
  function resolveAgainstProjectRoot(file, projectRoot) {
@@ -6308,7 +6737,7 @@ function runSection(opts) {
6308
6737
  const kind = result.redirectedFrom !== void 0 ? "section_replacement" : "section_read";
6309
6738
  const fullSourceBytes = sumFileSizes([filePath]);
6310
6739
  if (opts.json === true) {
6311
- const text2 = JSON.stringify(result, null, 2);
6740
+ const text2 = displaySafeJson(result);
6312
6741
  if (opts.suppressStat !== true) recordReadStat(kind, fullSourceBytes, text2, heading);
6313
6742
  return { text: text2, code: 0 };
6314
6743
  }
@@ -6336,7 +6765,7 @@ function runSectionMulti(specFilePath, resolvedFilePath, headings, opts) {
6336
6765
  ${sub.text}`);
6337
6766
  }
6338
6767
  const fullSourceBytes = sumFileSizes([resolvedFilePath]);
6339
- const text = opts.json === true ? JSON.stringify(jsonOut, null, 2) : textBlocks.join("\n\n");
6768
+ const text = opts.json === true ? displaySafeJson(jsonOut) : textBlocks.join("\n\n");
6340
6769
  if (anyFound) recordReadStat("section_read", fullSourceBytes, text, opts.spec);
6341
6770
  return { text, code: anyFound ? 0 : 1 };
6342
6771
  }
@@ -6358,7 +6787,7 @@ function runSectionCrossFile(pairs, opts) {
6358
6787
  ${sub.text}`);
6359
6788
  }
6360
6789
  const resolvePath = (f) => opts.projectRoot !== void 0 && !path4.isAbsolute(f) ? path4.resolve(opts.projectRoot, f) : f;
6361
- const text = opts.json === true ? JSON.stringify(jsonOut, null, 2) : textBlocks.join("\n\n");
6790
+ const text = opts.json === true ? displaySafeJson(jsonOut) : textBlocks.join("\n\n");
6362
6791
  if (anyFound) {
6363
6792
  const fullSourceBytes = sumFileSizes(Array.from(distinctFiles, resolvePath));
6364
6793
  recordReadStat("section_read", fullSourceBytes, text, opts.spec);
@@ -6378,7 +6807,7 @@ function refsJsonItems(items, contextLines) {
6378
6807
  }
6379
6808
  function renderRefLines(ref, contextLines, indent = " ") {
6380
6809
  const displayPath = refsDisplayPath(ref.filePath);
6381
- const base = `${indent}${displayPath}:${ref.line}: ${ref.context}`;
6810
+ const base = `${indent}${displaySafeText(displayPath)}:${ref.line}: ${displaySafeText(ref.context)}`;
6382
6811
  const window = buildContextWindow(ref.filePath, ref.line, contextLines);
6383
6812
  if (window === null) return [base];
6384
6813
  return [base, ...renderContextWindow(displayPath, ref.line, window, "", `${indent} `)];
@@ -6501,9 +6930,10 @@ function renderRefsTargets(targets, opts, {
6501
6930
  if (notice !== null) lines.push(` token-goat: ${notice}`);
6502
6931
  }
6503
6932
  }
6933
+ warnIfFilesStale(refRows.map((r) => r.filePath));
6504
6934
  const fullSourceBytes = refsSearchBaselineBytes(refRows);
6505
6935
  if (opts.json === true) {
6506
- const text2 = JSON.stringify(jsonOut, null, 2);
6936
+ const text2 = displaySafeJson(jsonOut);
6507
6937
  emit(text2);
6508
6938
  if (anyFound) recordReadStat("symbol_read", fullSourceBytes, text2, opts.spec);
6509
6939
  return anyFound ? 0 : 1;
@@ -6591,7 +7021,7 @@ function runRefsSingle(opts) {
6591
7021
  if (results.length === 0) {
6592
7022
  if (matchesGrep !== void 0 && preGrepCount > 0) {
6593
7023
  if (opts.json === true) {
6594
- emit(JSON.stringify({ items: [], truncated: false, totalCount: 0, hiddenByGrep: preGrepCount }, null, 2));
7024
+ emit(displaySafeJson({ items: [], truncated: false, totalCount: 0, hiddenByGrep: preGrepCount }));
6595
7025
  return 0;
6596
7026
  }
6597
7027
  emit(grepFilteredToEmptyNotice(preGrepCount, opts.grep ?? "", "reference", "references"));
@@ -6611,7 +7041,7 @@ function runRefsSingle(opts) {
6611
7041
  const defPaths = defFileHint !== void 0 ? [defFileHint] : defRows.map((r) => r.filePath);
6612
7042
  const firstDefPath = defPaths[0];
6613
7043
  if (firstDefPath !== void 0 && defPaths.every((fp) => !isRefIndexedFile(fp))) {
6614
- emitErr(refBlindLanguageNotice(symName, detectLanguage(firstDefPath), refsDisplayPath(firstDefPath)));
7044
+ emitErr(refBlindLanguageNotice(symName, detectLanguageOfFile(firstDefPath), refsDisplayPath(firstDefPath)));
6615
7045
  return 1;
6616
7046
  }
6617
7047
  const kindRows = defFileHint !== void 0 ? querySymbols({ name: symName, filePath: defFileHint, rootDir: rootDir2, limit: REF_BLIND_DEF_PROBE_LIMIT }) : defRows;
@@ -6627,6 +7057,7 @@ function runRefsSingle(opts) {
6627
7057
  }
6628
7058
  return 1;
6629
7059
  }
7060
+ warnIfFilesStale(results.map((r) => r.filePath));
6630
7061
  const fullSourceBytes = refsSearchBaselineBytes(results);
6631
7062
  if (opts.json === true) {
6632
7063
  let payload;
@@ -6638,7 +7069,7 @@ function runRefsSingle(opts) {
6638
7069
  payload = { items: refsJsonItems(capped.items, opts.context ?? 0), truncated: capped.truncated || trueTotal > results.length, totalCount: trueTotal };
6639
7070
  }
6640
7071
  const hiddenByGrep = matchesGrep !== void 0 ? preGrepCount - (filteredTotal ?? results.length) : 0;
6641
- const text2 = JSON.stringify({ ...payload, ...hiddenByGrep > 0 ? { hiddenByGrep } : {} }, null, 2);
7072
+ const text2 = displaySafeJson({ ...payload, ...hiddenByGrep > 0 ? { hiddenByGrep } : {} });
6642
7073
  emit(text2);
6643
7074
  recordReadStat("symbol_read", fullSourceBytes, text2, symName);
6644
7075
  return 0;
@@ -6690,24 +7121,34 @@ function renderCallerGroups(refs, contextLines = 0) {
6690
7121
  const lines = [];
6691
7122
  for (const [file, fileRefs] of byFile) {
6692
7123
  const displayPath = refsDisplayPath(file);
6693
- lines.push(`${displayPath}:`);
7124
+ lines.push(`${displaySafeText(displayPath)}:`);
6694
7125
  for (const ref of fileRefs) {
6695
- lines.push(` :${ref.line} ${ref.context !== "" ? ref.context : "(module scope)"}`);
7126
+ lines.push(` :${ref.line} ${ref.context !== "" ? displaySafeText(ref.context) : "(module scope)"}`);
6696
7127
  const window = buildContextWindow(file, ref.line, contextLines);
6697
7128
  if (window !== null) lines.push(...renderContextWindow(displayPath, ref.line, window, "", " "));
6698
7129
  }
6699
7130
  }
6700
7131
  return lines;
6701
7132
  }
7133
+ function symbolExtractorGap(displayPath, resolvedPath) {
7134
+ const ext = path4.extname(resolvedPath).toLowerCase();
7135
+ const named = unsupportedLanguageName(resolvedPath);
7136
+ const language = detectLanguageOfFile(resolvedPath);
7137
+ if (named !== void 0 || language === "unknown") {
7138
+ const what = named !== void 0 ? `${named}, ${ext}` : ext !== "" ? ext : "no extension";
7139
+ return `'${displayPath}': token-goat has no symbol extractor for this file type (${what}), so there are no symbols to list; grep, plain reads and \`token-goat tokens\` still work on it.
7140
+ ` + supportRequestLine(named ?? (ext !== "" ? `${ext} file` : "this file type"));
7141
+ }
7142
+ if (TREE_SITTER_LANGUAGES.includes(language) && !isTreeSitterAvailable(language)) {
7143
+ return `No symbols found in '${displayPath}', but tree-sitter parsing for this file type (${ext}) is unavailable, so only a coarse regex fallback ran. Run \`token-goat doctor\` for the cause and the fix.`;
7144
+ }
7145
+ return void 0;
7146
+ }
6702
7147
  function noSymbolsMessage(displayPath, resolvedPath) {
6703
7148
  if (!fs4.existsSync(resolvedPath)) {
6704
7149
  return `Could not read: ${displayPath}`;
6705
7150
  }
6706
- const lang = unsupportedLanguageName(resolvedPath);
6707
- if (lang !== void 0) {
6708
- return `No indexed symbols found in '${displayPath}' -- ${lang} has no symbol extractor yet, so this file always indexes to 0 symbols regardless of its contents`;
6709
- }
6710
- return `No indexed symbols found in '${displayPath}'`;
7151
+ return symbolExtractorGap(displayPath, resolvedPath) ?? `No indexed symbols found in '${displayPath}'`;
6711
7152
  }
6712
7153
  var SKELETON_SYMBOL_CAP = 5e3;
6713
7154
  function hasRealDocstring(docstring) {
@@ -6746,7 +7187,8 @@ function prepareSymbolListing(file, opts) {
6746
7187
  );
6747
7188
  const refCounts = opts.stats === true ? queryRefCounts(filtered.map((s) => s.name), globalDbPath(), resolveProjectRoot({ project: opts.projectRoot ?? process.cwd() })) : void 0;
6748
7189
  const fullSourceBytes = sumFileSizes([resolved]);
6749
- return { kind: "ok", resolved, displayRoot: getDisplayRoot(opts.projectRoot), filtered, preFilterCount: symbols.length, refCounts, fullSourceBytes, symbolsTruncated, trueSymbolCount };
7190
+ const totalLines = symbols.length > 0 ? Math.max(...symbols.map((s) => s.lineEnd)) : 0;
7191
+ return { kind: "ok", resolved, displayRoot: getDisplayRoot(opts.projectRoot), filtered, preFilterCount: symbols.length, refCounts, fullSourceBytes, symbolsTruncated, trueSymbolCount, totalLines };
6750
7192
  }
6751
7193
  function runPerFileListing(files, run, json = false) {
6752
7194
  const blocks = [];
@@ -6781,7 +7223,7 @@ function mergeListingJson(files, blocks, anyOk) {
6781
7223
  totalCount += parsed.totalCount ?? parsed.items.length;
6782
7224
  }
6783
7225
  const payload = { items, truncated, totalCount, ...errors.length > 0 ? { errors } : {} };
6784
- return { text: JSON.stringify(payload, null, 2), code: anyOk ? 0 : 1 };
7226
+ return { text: displaySafeJson(payload), code: anyOk ? 0 : 1 };
6785
7227
  }
6786
7228
  function symbolCountLabel(shown, truncated, trueCount) {
6787
7229
  if (!truncated || trueCount === void 0 || trueCount <= shown) return countNoun(shown, "symbol");
@@ -6794,7 +7236,7 @@ function runSkeleton(opts) {
6794
7236
  if (prep.kind === "confined" || prep.kind === "empty") {
6795
7237
  return { text: prep.text, code: 1 };
6796
7238
  }
6797
- const { resolved, displayRoot, filtered, preFilterCount, refCounts, fullSourceBytes, symbolsTruncated, trueSymbolCount } = prep;
7239
+ const { resolved, displayRoot, filtered, preFilterCount, refCounts, fullSourceBytes, symbolsTruncated, trueSymbolCount, totalLines } = prep;
6798
7240
  if (opts.json === true) {
6799
7241
  const rows = filtered.map((s) => ({
6800
7242
  ...opts.includeFilePath === true ? { filePath: toDisplayPath(displayRoot, s.filePath) } : {},
@@ -6814,11 +7256,10 @@ function runSkeleton(opts) {
6814
7256
  // tests/json_envelope_shape.test.ts pins.
6815
7257
  ...fileIsGone(resolved) ? { deleted: true } : {}
6816
7258
  };
6817
- const text2 = JSON.stringify(payload, null, 2);
7259
+ const text2 = displaySafeJson(payload);
6818
7260
  recordReadStat("stub_view", fullSourceBytes, text2, opts.file);
6819
7261
  return { text: text2, code: 0 };
6820
7262
  }
6821
- const totalLines = filtered.length > 0 ? Math.max(...filtered.map((s) => s.lineEnd)) : 0;
6822
7263
  const lines = [`# Skeleton: ${opts.file} (${symbolCountLabel(filtered.length, symbolsTruncated, trueSymbolCount)}, ${countNoun(totalLines, "line")})`];
6823
7264
  if (filtered.length === 0 && preFilterCount > 0) lines.push(filteredToEmptyNotice(preFilterCount, opts.minLines, opts.grep));
6824
7265
  for (const sym of filtered) {
@@ -6879,7 +7320,7 @@ function runOutline(opts) {
6879
7320
  // Same envelope-level flag, and same reason, as runSkeleton's payload above.
6880
7321
  ...fileIsGone(resolved) ? { deleted: true } : {}
6881
7322
  };
6882
- const text2 = JSON.stringify(payload, null, 2);
7323
+ const text2 = displaySafeJson(payload);
6883
7324
  recordReadStat("outline", fullSourceBytes, text2, opts.file);
6884
7325
  return { text: text2, code: 0 };
6885
7326
  }
@@ -6921,7 +7362,7 @@ function runCsvQuery(opts) {
6921
7362
  ...opts.noHeader === true ? { noHeader: true } : {}
6922
7363
  });
6923
7364
  if (result.header.length === 0) {
6924
- emit(`No data rows found in ${opts.file}`);
7365
+ emit(`No data rows found in ${displaySafeText(opts.file)}`);
6925
7366
  return 0;
6926
7367
  }
6927
7368
  const fullSourceBytes = sumFileSizes([opts.file]);
@@ -6929,7 +7370,7 @@ function runCsvQuery(opts) {
6929
7370
  const rowsJson = result.rows.map((r) => Object.fromEntries(result.header.map((h, i) => [h, r[i]])));
6930
7371
  const headTruncated = result.rows.length < result.totalRows;
6931
7372
  const capped = guardJsonRows(rowsJson);
6932
- const jsonText = JSON.stringify({ items: capped.items, truncated: capped.truncated || headTruncated, totalCount: result.totalRows, ...result.totalRows === 0 && result.preFilterRows > 0 ? { filteredFromRows: result.preFilterRows } : {} });
7373
+ const jsonText = displaySafeJson({ items: capped.items, truncated: capped.truncated || headTruncated, totalCount: result.totalRows, ...result.totalRows === 0 && result.preFilterRows > 0 ? { filteredFromRows: result.preFilterRows } : {} }, 0);
6933
7374
  emit(jsonText);
6934
7375
  recordReadStat("csv_query", fullSourceBytes, jsonText, opts.file);
6935
7376
  } else {
@@ -6955,7 +7396,7 @@ function runCsvProfile(opts) {
6955
7396
  ...opts.noHeader === true ? { noHeader: true } : {}
6956
7397
  });
6957
7398
  if (profiles.length === 0) {
6958
- emit(`No data rows found in ${opts.file}`);
7399
+ emit(`No data rows found in ${displaySafeText(opts.file)}`);
6959
7400
  return 0;
6960
7401
  }
6961
7402
  const fullSourceBytes = sumFileSizes([opts.file]);
@@ -6984,7 +7425,7 @@ function runOutlineCommand(opts, parse, formatLabel, kind) {
6984
7425
  const outline = outlineJson(data);
6985
7426
  const fullSourceBytes = sumFileSizes([opts.file]);
6986
7427
  if (opts.json === true) {
6987
- const jsonText = JSON.stringify(outline);
7428
+ const jsonText = displaySafeJson(outline, 0);
6988
7429
  emit(jsonText);
6989
7430
  recordReadStat(kind, fullSourceBytes, jsonText, opts.file);
6990
7431
  } else {
@@ -7022,7 +7463,7 @@ function runQueryCommand(opts, parse, formatLabel, guardTag, kind) {
7022
7463
  const fullSourceBytes = sumFileSizes([opts.file]);
7023
7464
  if (!result.fanned) {
7024
7465
  const value = result.items[0];
7025
- const valueText = opts.json === true ? JSON.stringify(value) : JSON.stringify(value, null, 2);
7466
+ const valueText = opts.json === true ? displaySafeJson(value, 0) : displaySafeJson(value);
7026
7467
  emit(valueText);
7027
7468
  recordReadStat(kind, fullSourceBytes, valueText, opts.file);
7028
7469
  return 0;
@@ -7032,11 +7473,11 @@ function runQueryCommand(opts, parse, formatLabel, guardTag, kind) {
7032
7473
  const headTruncated = limited.length < totalCount;
7033
7474
  if (opts.json === true) {
7034
7475
  const capped = guardJsonRows(limited);
7035
- const jsonText = JSON.stringify({ items: capped.items, truncated: capped.truncated || headTruncated, totalCount });
7476
+ const jsonText = displaySafeJson({ items: capped.items, truncated: capped.truncated || headTruncated, totalCount }, 0);
7036
7477
  emit(jsonText);
7037
7478
  recordReadStat(kind, fullSourceBytes, jsonText, opts.file);
7038
7479
  } else {
7039
- const lines = limited.map((item) => JSON.stringify(item));
7480
+ const lines = limited.map((item) => displaySafeJson(item, 0));
7040
7481
  if (headTruncated) {
7041
7482
  lines.push(`...(${totalCount - limited.length} more items elided; use --head to see more)`);
7042
7483
  }
@@ -7101,7 +7542,7 @@ ${extractErrorMessage(e)}`);
7101
7542
  }
7102
7543
  const fullSourceBytes = sumFileSizes([opts.file]);
7103
7544
  if (opts.json === true) {
7104
- const jsonText = JSON.stringify(summary, null, 2);
7545
+ const jsonText = displaySafeJson(summary);
7105
7546
  emit(jsonText);
7106
7547
  recordReadStat("xml_outline", fullSourceBytes, jsonText, opts.file);
7107
7548
  } else {
@@ -7130,17 +7571,17 @@ function runXmlQuery(opts) {
7130
7571
  if (result.attributeValues !== void 0) {
7131
7572
  if (result.attributeValues.length === 0) {
7132
7573
  if (opts.json === true) {
7133
- const jsonText = JSON.stringify({ items: [], truncated: false, totalCount: 0 });
7574
+ const jsonText = displaySafeJson({ items: [], truncated: false, totalCount: 0 }, 0);
7134
7575
  emit(jsonText);
7135
7576
  recordReadStat("xml_query", fullSourceBytes, jsonText, opts.file);
7136
7577
  } else {
7137
- emit(`No attributes matched path: '${opts.path}'`);
7578
+ emit(`No attributes matched path: '${displaySafeText(opts.path)}'`);
7138
7579
  }
7139
7580
  return 0;
7140
7581
  }
7141
7582
  if (!result.fanned) {
7142
7583
  const val = result.attributeValues[0] ?? "";
7143
- const outText = opts.json === true ? JSON.stringify(val) : val;
7584
+ const outText = opts.json === true ? displaySafeJson(val, 0) : val;
7144
7585
  emit(outText);
7145
7586
  recordReadStat("xml_query", fullSourceBytes, outText, opts.file);
7146
7587
  return 0;
@@ -7150,7 +7591,7 @@ function runXmlQuery(opts) {
7150
7591
  const headTruncated2 = limited2.length < totalCount2;
7151
7592
  if (opts.json === true) {
7152
7593
  const capped = guardJsonRows(limited2);
7153
- const jsonText = JSON.stringify({ items: capped.items, truncated: capped.truncated || headTruncated2, totalCount: totalCount2 });
7594
+ const jsonText = displaySafeJson({ items: capped.items, truncated: capped.truncated || headTruncated2, totalCount: totalCount2 }, 0);
7154
7595
  emit(jsonText);
7155
7596
  recordReadStat("xml_query", fullSourceBytes, jsonText, opts.file);
7156
7597
  } else {
@@ -7166,11 +7607,11 @@ function runXmlQuery(opts) {
7166
7607
  }
7167
7608
  if (result.items.length === 0) {
7168
7609
  if (opts.json === true) {
7169
- const jsonText = JSON.stringify({ items: [], truncated: false, totalCount: 0 });
7610
+ const jsonText = displaySafeJson({ items: [], truncated: false, totalCount: 0 }, 0);
7170
7611
  emit(jsonText);
7171
7612
  recordReadStat("xml_query", fullSourceBytes, jsonText, opts.file);
7172
7613
  } else {
7173
- emit(`No elements matched path: '${opts.path}'`);
7614
+ emit(`No elements matched path: '${displaySafeText(opts.path)}'`);
7174
7615
  }
7175
7616
  return 0;
7176
7617
  }
@@ -7178,7 +7619,7 @@ function runXmlQuery(opts) {
7178
7619
  const node = result.items[0];
7179
7620
  if (opts.json === true) {
7180
7621
  const jsonVal = xmlNodeToJson(node);
7181
- const jsonText = JSON.stringify(jsonVal, null, 2);
7622
+ const jsonText = displaySafeJson(jsonVal);
7182
7623
  emit(jsonText);
7183
7624
  recordReadStat("xml_query", fullSourceBytes, jsonText, opts.file);
7184
7625
  } else {
@@ -7194,7 +7635,7 @@ function runXmlQuery(opts) {
7194
7635
  if (opts.json === true) {
7195
7636
  const jsonItems = limited.map(xmlNodeToJson);
7196
7637
  const capped = guardJsonRows(jsonItems);
7197
- const jsonText = JSON.stringify({ items: capped.items, truncated: capped.truncated || headTruncated, totalCount });
7638
+ const jsonText = displaySafeJson({ items: capped.items, truncated: capped.truncated || headTruncated, totalCount }, 0);
7198
7639
  emit(jsonText);
7199
7640
  recordReadStat("xml_query", fullSourceBytes, jsonText, opts.file);
7200
7641
  } else {
@@ -7212,6 +7653,160 @@ function runXmlQuery(opts) {
7212
7653
  return 1;
7213
7654
  }
7214
7655
  }
7656
+ function runHtmlOutline(opts) {
7657
+ const text = readFileText(opts.file);
7658
+ if (text === null) {
7659
+ emitErr(`Could not read: ${opts.file}`);
7660
+ return 1;
7661
+ }
7662
+ const summary = outlineHtml(text);
7663
+ const fullSourceBytes = sumFileSizes([opts.file]);
7664
+ if (opts.json === true) {
7665
+ const jsonText = displaySafeJson(summary);
7666
+ emit(jsonText);
7667
+ recordReadStat("html_outline", fullSourceBytes, jsonText, opts.file);
7668
+ } else {
7669
+ const outlineText = formatHtmlOutline(summary);
7670
+ emitGuarded(outlineText, "html-outline");
7671
+ recordReadStat("html_outline", fullSourceBytes, outlineText, opts.file);
7672
+ }
7673
+ return 0;
7674
+ }
7675
+ function runHtmlQuery(opts) {
7676
+ const text = readFileText(opts.file);
7677
+ if (text === null) {
7678
+ emitErr(`Could not read: ${opts.file}`);
7679
+ return 1;
7680
+ }
7681
+ let head;
7682
+ try {
7683
+ head = opts.head !== void 0 ? requireNonNegativeStrictInt("--head", opts.head) : void 0;
7684
+ } catch (e) {
7685
+ emitErr(extractErrorMessage(e));
7686
+ return 1;
7687
+ }
7688
+ try {
7689
+ const querySelector = opts.attr ? `${opts.selector}@${opts.attr}` : opts.selector;
7690
+ const result = queryHtml(text, querySelector);
7691
+ const fullSourceBytes = sumFileSizes([opts.file]);
7692
+ if (result.attributeValues !== void 0) {
7693
+ if (result.attributeValues.length === 0) {
7694
+ if (opts.json === true) {
7695
+ const jsonText = displaySafeJson({ items: [], truncated: false, totalCount: 0 }, 0);
7696
+ emit(jsonText);
7697
+ recordReadStat("html_query", fullSourceBytes, jsonText, opts.file);
7698
+ } else {
7699
+ emit(`No attributes matched selector: '${displaySafeText(opts.selector)}'`);
7700
+ }
7701
+ return 0;
7702
+ }
7703
+ const totalCount2 = result.attributeValues.length;
7704
+ const limited2 = head !== void 0 ? result.attributeValues.slice(0, head) : result.attributeValues;
7705
+ const headTruncated2 = limited2.length < totalCount2;
7706
+ if (opts.json === true) {
7707
+ const capped = guardJsonRows(limited2);
7708
+ const jsonText = displaySafeJson({ items: capped.items, truncated: capped.truncated || headTruncated2, totalCount: totalCount2 }, 0);
7709
+ emit(jsonText);
7710
+ recordReadStat("html_query", fullSourceBytes, jsonText, opts.file);
7711
+ } else {
7712
+ const lines = limited2.map((item) => item);
7713
+ if (headTruncated2) {
7714
+ lines.push(`...(${totalCount2 - limited2.length} more items elided; use --head to see more)`);
7715
+ }
7716
+ const plainText = lines.join("\n");
7717
+ emitGuarded(fenceHtmlText(plainText), "html-query");
7718
+ recordReadStat("html_query", fullSourceBytes, plainText, opts.file);
7719
+ }
7720
+ return 0;
7721
+ }
7722
+ if (result.elements.length === 0) {
7723
+ if (opts.json === true) {
7724
+ const jsonText = displaySafeJson({ items: [], truncated: false, totalCount: 0 }, 0);
7725
+ emit(jsonText);
7726
+ recordReadStat("html_query", fullSourceBytes, jsonText, opts.file);
7727
+ } else {
7728
+ emit(`No elements matched selector: '${displaySafeText(opts.selector)}'`);
7729
+ }
7730
+ return 0;
7731
+ }
7732
+ const totalCount = result.elements.length;
7733
+ const limited = head !== void 0 ? result.elements.slice(0, head) : result.elements;
7734
+ const headTruncated = limited.length < totalCount;
7735
+ if (opts.json === true) {
7736
+ const jsonItems = limited.map((n) => ({
7737
+ tag: n.tag,
7738
+ attributes: n.attributes,
7739
+ text: extractNodeText(n, text),
7740
+ line: n.line,
7741
+ endLine: n.endLine
7742
+ }));
7743
+ const capped = guardJsonRows(jsonItems);
7744
+ const jsonText = displaySafeJson({ items: capped.items, truncated: capped.truncated || headTruncated, totalCount });
7745
+ emit(jsonText);
7746
+ recordReadStat("html_query", fullSourceBytes, jsonText, opts.file);
7747
+ } else if (opts.text === true) {
7748
+ const textLines = limited.map((n) => extractNodeText(n, text)).filter(Boolean);
7749
+ if (headTruncated) {
7750
+ textLines.push(`...(${totalCount - limited.length} more elements elided; use --head to see more)`);
7751
+ }
7752
+ const plainText = textLines.join("\n\n");
7753
+ emitGuarded(fenceHtmlText(plainText), "html-query");
7754
+ recordReadStat("html_query", fullSourceBytes, plainText, opts.file);
7755
+ } else {
7756
+ const blocks = limited.map((node) => serializeHtmlNode(node, 0, text));
7757
+ if (headTruncated) {
7758
+ blocks.push(`...(${totalCount - limited.length} more elements elided; use --head to see more)`);
7759
+ }
7760
+ const plainText = blocks.join("\n\n");
7761
+ emitGuarded(fenceHtmlText(plainText), "html-query");
7762
+ recordReadStat("html_query", fullSourceBytes, plainText, opts.file);
7763
+ }
7764
+ return 0;
7765
+ } catch (e) {
7766
+ emitErr(extractErrorMessage(e));
7767
+ return 1;
7768
+ }
7769
+ }
7770
+ function runHtmlLint(opts) {
7771
+ const text = readFileText(opts.file);
7772
+ if (text === null) {
7773
+ emitErr(`Could not read: ${opts.file}`);
7774
+ return 1;
7775
+ }
7776
+ const report = lintHtml(text);
7777
+ const isClean = opts.strict ? report.errors.length === 0 && report.warnings.length === 0 : report.errors.length === 0;
7778
+ if (opts.json === true) {
7779
+ emit(displaySafeJson(report));
7780
+ return isClean ? 0 : 1;
7781
+ }
7782
+ if (report.errors.length === 0 && report.warnings.length === 0) {
7783
+ emit(`\u2713 ${displaySafeText(opts.file)}: HTML structure is valid (all tags balanced, no duplicate IDs, void elements respected).`);
7784
+ return 0;
7785
+ }
7786
+ const lines = [];
7787
+ if (report.errors.length > 0) {
7788
+ lines.push(`Errors found in ${displaySafeText(opts.file)} (${report.errors.length}):`);
7789
+ for (const err of report.errors) {
7790
+ lines.push(` line ${err.line}: [${err.rule}] ${displaySafeText(err.message)}`);
7791
+ }
7792
+ }
7793
+ if (report.warnings.length > 0) {
7794
+ if (lines.length > 0) lines.push("");
7795
+ lines.push(`Warnings in ${displaySafeText(opts.file)} (${report.warnings.length}):`);
7796
+ for (const warn of report.warnings) {
7797
+ lines.push(` line ${warn.line}: [${warn.rule}] ${displaySafeText(warn.message)}`);
7798
+ }
7799
+ }
7800
+ const unbalanced = Object.entries(report.tagCounts).filter(([, c]) => c.diff !== 0);
7801
+ if (unbalanced.length > 0) {
7802
+ lines.push("", "Unbalanced tags:");
7803
+ for (const [tag, counts] of unbalanced) {
7804
+ lines.push(` <${tag}>: open=${counts.open}, close=${counts.close}, diff=${counts.diff}`);
7805
+ }
7806
+ }
7807
+ emit(lines.join("\n"));
7808
+ return isClean ? 0 : 1;
7809
+ }
7215
7810
  function loadOpenApiOperations(file) {
7216
7811
  const text = readFileText(file);
7217
7812
  if (text === null) {
@@ -7232,7 +7827,7 @@ function runOpenApiOutline(opts) {
7232
7827
  if (operations === null) return 1;
7233
7828
  const fullSourceBytes = sumFileSizes([opts.file]);
7234
7829
  if (opts.json === true) {
7235
- const jsonText = JSON.stringify(operations);
7830
+ const jsonText = displaySafeJson(operations, 0);
7236
7831
  emit(jsonText);
7237
7832
  recordReadStat("openapi_outline", fullSourceBytes, jsonText, opts.file);
7238
7833
  } else {
@@ -7256,7 +7851,7 @@ function runOpenApiOp(opts) {
7256
7851
  }
7257
7852
  const fullSourceBytes = sumFileSizes([opts.file]);
7258
7853
  if (opts.json === true) {
7259
- const jsonText = JSON.stringify(match);
7854
+ const jsonText = displaySafeJson(match, 0);
7260
7855
  emit(jsonText);
7261
7856
  recordReadStat("openapi_op", fullSourceBytes, jsonText, opts.operation);
7262
7857
  } else {
@@ -7294,7 +7889,7 @@ async function runZipList(opts) {
7294
7889
  }
7295
7890
  const fullSourceBytes = sumFileSizes([opts.file]);
7296
7891
  if (opts.json === true) {
7297
- const jsonText = JSON.stringify(entries);
7892
+ const jsonText = displaySafeJson(entries, 0);
7298
7893
  emit(jsonText);
7299
7894
  recordReadStat("zip_list", fullSourceBytes, jsonText, opts.file);
7300
7895
  } else {
@@ -7345,7 +7940,7 @@ async function runZipRead(opts) {
7345
7940
  const text = isValidUtf8(buf) ? buf.toString("utf-8") : "[binary content elided by token-goat]";
7346
7941
  const fullSourceBytes = sumFileSizes([opts.file]);
7347
7942
  if (opts.json === true) {
7348
- const jsonText = JSON.stringify({ path: opts.entry, text });
7943
+ const jsonText = displaySafeJson({ path: opts.entry, text }, 0);
7349
7944
  emit(jsonText);
7350
7945
  recordReadStat("zip_read", fullSourceBytes, jsonText, opts.entry);
7351
7946
  } else {
@@ -7399,7 +7994,7 @@ function runPrSlice(opts) {
7399
7994
  const fullSourceBytes = Buffer.byteLength(JSON.stringify(files), "utf8");
7400
7995
  if (opts.json === true) {
7401
7996
  const capped = guardJsonRows(files);
7402
- const jsonText = JSON.stringify({ items: capped.items, truncated: capped.truncated, totalCount: capped.totalCount });
7997
+ const jsonText = displaySafeJson({ items: capped.items, truncated: capped.truncated, totalCount: capped.totalCount }, 0);
7403
7998
  emit(jsonText);
7404
7999
  recordReadStat("pr_slice", fullSourceBytes, jsonText, `${repo}#${opts.pr} files`);
7405
8000
  } else {
@@ -7411,14 +8006,15 @@ function runPrSlice(opts) {
7411
8006
  }
7412
8007
  case "diff": {
7413
8008
  const diffText = fetchPrDiff(opts.pr, repo);
7414
- const fileDiff = extractFileDiff(diffText, parsed.path);
7415
- if (fileDiff === null) {
8009
+ const rawFileDiff = extractFileDiff(diffText, parsed.path);
8010
+ if (rawFileDiff === null) {
7416
8011
  emitErr(`No diff found for '${parsed.path}' in PR #${opts.pr}`);
7417
8012
  return 1;
7418
8013
  }
8014
+ const fileDiff = redactSecrets(rawFileDiff).text;
7419
8015
  const fullSourceBytes = Buffer.byteLength(diffText, "utf8");
7420
8016
  if (opts.json === true) {
7421
- const jsonText = JSON.stringify({ path: parsed.path, diff: fenceGithubFieldIfMatched(fileDiff) });
8017
+ const jsonText = displaySafeJson({ path: parsed.path, diff: fenceGithubFieldIfMatched(fileDiff) }, 0);
7422
8018
  emit(jsonText);
7423
8019
  recordReadStat("pr_slice", fullSourceBytes, jsonText, `${repo}#${opts.pr} diff:${parsed.path}`);
7424
8020
  } else {
@@ -7428,12 +8024,17 @@ function runPrSlice(opts) {
7428
8024
  return 0;
7429
8025
  }
7430
8026
  case "comments": {
7431
- const comments = fetchPrComments(opts.pr, repo);
7432
- const fullSourceBytes = Buffer.byteLength(JSON.stringify(comments), "utf8");
8027
+ const rawComments = fetchPrComments(opts.pr, repo);
8028
+ const fullSourceBytes = Buffer.byteLength(JSON.stringify(rawComments), "utf8");
8029
+ const comments = rawComments.map((c) => ({
8030
+ ...c,
8031
+ body: redactSecrets(c.body).text,
8032
+ ...c.diffHunk !== void 0 ? { diffHunk: redactSecrets(c.diffHunk).text } : {}
8033
+ }));
7433
8034
  if (opts.json === true) {
7434
8035
  const fencedComments = comments.map((c) => ({ ...c, body: fenceGithubFieldIfMatched(c.body) }));
7435
8036
  const capped = guardJsonRows(fencedComments);
7436
- const jsonText = JSON.stringify({ items: capped.items, truncated: capped.truncated, totalCount: capped.totalCount });
8037
+ const jsonText = displaySafeJson({ items: capped.items, truncated: capped.truncated, totalCount: capped.totalCount }, 0);
7437
8038
  emit(jsonText);
7438
8039
  recordReadStat("pr_slice", fullSourceBytes, jsonText, `${repo}#${opts.pr} comments`);
7439
8040
  } else {
@@ -7444,15 +8045,20 @@ function runPrSlice(opts) {
7444
8045
  return 0;
7445
8046
  }
7446
8047
  case "description": {
7447
- const desc = fetchPrDescription(opts.pr, repo);
7448
- const fullSourceBytes = Buffer.byteLength(JSON.stringify(desc), "utf8");
8048
+ const rawDesc = fetchPrDescription(opts.pr, repo);
8049
+ const fullSourceBytes = Buffer.byteLength(JSON.stringify(rawDesc), "utf8");
8050
+ const desc = {
8051
+ ...rawDesc,
8052
+ title: redactSecrets(rawDesc.title).text,
8053
+ body: rawDesc.body !== null ? redactSecrets(rawDesc.body).text : null
8054
+ };
7449
8055
  if (opts.json === true) {
7450
8056
  const fencedDesc = {
7451
8057
  ...desc,
7452
8058
  title: fenceGithubFieldIfMatched(desc.title),
7453
8059
  body: desc.body !== null ? fenceGithubFieldIfMatched(desc.body) : null
7454
8060
  };
7455
- const jsonText = JSON.stringify(fencedDesc);
8061
+ const jsonText = displaySafeJson(fencedDesc, 0);
7456
8062
  emit(jsonText);
7457
8063
  recordReadStat("pr_slice", fullSourceBytes, jsonText, `${repo}#${opts.pr} description`);
7458
8064
  } else {
@@ -7473,7 +8079,7 @@ function runSqliteSchema(opts) {
7473
8079
  const schema = getSqliteSchema(opts.file);
7474
8080
  const fullSourceBytes = sumFileSizes([opts.file]);
7475
8081
  if (opts.json === true) {
7476
- const jsonText = JSON.stringify(schema);
8082
+ const jsonText = displaySafeJson(schema, 0);
7477
8083
  emit(jsonText);
7478
8084
  recordReadStat("sqlite_schema", fullSourceBytes, jsonText, opts.file);
7479
8085
  } else {
@@ -7502,22 +8108,22 @@ function runSqliteQuery(opts) {
7502
8108
  const rows = head !== void 0 ? result.rows.slice(0, head) : result.rows;
7503
8109
  if (opts.json === true) {
7504
8110
  const capped = guardJsonRows(rows);
7505
- const jsonText = JSON.stringify({
8111
+ const jsonText = displaySafeJson({
7506
8112
  columns: result.columns,
7507
8113
  items: capped.items,
7508
8114
  truncated: capped.truncated || headTruncated || result.rowCapped,
7509
8115
  totalCount,
7510
8116
  rowCapped: result.rowCapped
7511
- });
8117
+ }, 0);
7512
8118
  emit(jsonText);
7513
8119
  const uncappedFull = guardJsonRows(result.rows);
7514
- const baselineJsonText = JSON.stringify({
8120
+ const baselineJsonText = displaySafeJson({
7515
8121
  columns: result.columns,
7516
8122
  items: uncappedFull.items,
7517
8123
  truncated: uncappedFull.truncated || result.rowCapped,
7518
8124
  totalCount,
7519
8125
  rowCapped: result.rowCapped
7520
- });
8126
+ }, 0);
7521
8127
  recordReadStat("sqlite_query", Buffer.byteLength(baselineJsonText, "utf8"), jsonText, opts.file);
7522
8128
  } else {
7523
8129
  const text = formatSqliteQueryTable({ ...result, rows }, { headTruncated });
@@ -7548,7 +8154,7 @@ ${extractErrorMessage(e)}`);
7548
8154
  const scoped = opts.fileFilter !== void 0 ? filterCoverageGapsByFile(report, opts.fileFilter) : report;
7549
8155
  const fullSourceBytes = sumFileSizes([opts.file]);
7550
8156
  if (opts.json === true) {
7551
- const jsonText = JSON.stringify(scoped);
8157
+ const jsonText = displaySafeJson(scoped, 0);
7552
8158
  emit(jsonText);
7553
8159
  recordReadStat("coverage_report_gaps", fullSourceBytes, jsonText, opts.file);
7554
8160
  } else {
@@ -7583,7 +8189,7 @@ function runConflicts(opts) {
7583
8189
  const fullSourceBytes = sumFileSizes(files);
7584
8190
  const detail = opts.path ?? ".";
7585
8191
  if (opts.json === true) {
7586
- const jsonText = JSON.stringify(opts.summary === true ? results.map(summarizeFileConflicts) : results);
8192
+ const jsonText = displaySafeJson(opts.summary === true ? results.map(summarizeFileConflicts) : results, 0);
7587
8193
  emit(jsonText);
7588
8194
  recordReadStat("conflicts", fullSourceBytes, jsonText, detail);
7589
8195
  } else if (opts.summary === true) {
@@ -7597,34 +8203,24 @@ function runConflicts(opts) {
7597
8203
  }
7598
8204
  return 0;
7599
8205
  }
7600
- async function runPdfExtractText(file, pagesSpec, layout = false) {
8206
+ async function readPdfBytes(file) {
7601
8207
  if (!fileExists(file)) {
7602
8208
  throw new Error(`Could not read: ${file}`);
7603
8209
  }
7604
- const data = fs4.readFileSync(file);
7605
- const result = await extractPdfText(new Uint8Array(data), pagesSpec, layout);
8210
+ return readPdfFileWithinBounds(file);
8211
+ }
8212
+ async function runPdfExtractText(file, pagesSpec, layout = false) {
8213
+ const result = await extractPdfText(await readPdfBytes(file), pagesSpec, layout);
7606
8214
  return result.text;
7607
8215
  }
7608
8216
  async function runPdfOutline(file) {
7609
- if (!fileExists(file)) {
7610
- throw new Error(`Could not read: ${file}`);
7611
- }
7612
- const data = fs4.readFileSync(file);
7613
- return extractPdfOutline(new Uint8Array(data));
8217
+ return extractPdfOutline(await readPdfBytes(file));
7614
8218
  }
7615
8219
  async function runPdfMeta(file) {
7616
- if (!fileExists(file)) {
7617
- throw new Error(`Could not read: ${file}`);
7618
- }
7619
- const data = fs4.readFileSync(file);
7620
- return extractPdfMeta(new Uint8Array(data));
8220
+ return extractPdfMeta(await readPdfBytes(file));
7621
8221
  }
7622
8222
  async function runPdfLocate(file, pattern, opts) {
7623
- if (!fileExists(file)) {
7624
- throw new Error(`Could not read: ${file}`);
7625
- }
7626
- const data = fs4.readFileSync(file);
7627
- return locatePdfPages(new Uint8Array(data), pattern, opts);
8223
+ return locatePdfPages(await readPdfBytes(file), pattern, opts);
7628
8224
  }
7629
8225
  async function runImageMeta(file) {
7630
8226
  if (!fileExists(file)) {
@@ -7745,14 +8341,15 @@ ${emptyIndexMessage(rootDir2)}`, code: 1 };
7745
8341
  ...hiddenByGrep > 0 ? { hiddenByGrep } : {},
7746
8342
  section
7747
8343
  };
7748
- const jsonText = JSON.stringify(result, null, 2);
8344
+ const jsonText = displaySafeJson(result);
7749
8345
  if (opts.suppressStat !== true) recordReadStat("brief_view", fullSourceBytes, jsonText, opts.spec);
7750
8346
  return { text: jsonText, code: 0 };
7751
8347
  }
7752
8348
  const body = resolveBody(match);
7753
8349
  const bodyLen = match.lineEnd - match.lineStart + 1;
7754
8350
  const lines = [
7755
- `# ${match.name} ${match.kind} ${toDisplayPath(rootDir, match.filePath)}:${match.lineStart}-${match.lineEnd}`,
8351
+ // This header is token-goat's own line quoting a repo-chosen name, kind and path, so it is escaped. `body` below it is the source the reader asked for and stays byte-for-byte.
8352
+ `# ${displaySafeText(match.name)} ${displaySafeText(match.kind)} ${displaySafeText(toDisplayPath(rootDir, match.filePath))}:${match.lineStart}-${match.lineEnd}`,
7756
8353
  `# ${countNoun(bodyLen, "line")} (~${Math.ceil(body.length / 4)} tok)`,
7757
8354
  body,
7758
8355
  ""
@@ -7795,7 +8392,7 @@ function runBriefMulti(file, symbols, opts) {
7795
8392
  ${sub.text}`);
7796
8393
  }
7797
8394
  const fullSourceBytes = sumFileSizes([resolveIndexPath(file, opts.projectRoot ?? process.cwd())]);
7798
- const text = opts.json === true ? JSON.stringify(jsonOut, null, 2) : textBlocks.join("\n\n");
8395
+ const text = opts.json === true ? displaySafeJson(jsonOut) : textBlocks.join("\n\n");
7799
8396
  if (anyFound) recordReadStat("brief_view", fullSourceBytes, text, opts.spec);
7800
8397
  return { text, code: anyFound ? 0 : 1 };
7801
8398
  }
@@ -7817,7 +8414,7 @@ function runBriefCrossFile(pairs, opts) {
7817
8414
  ${sub.text}`);
7818
8415
  }
7819
8416
  const fullSourceBytes = sumFileSizes([...distinctFiles].map((f) => resolveIndexPath(f, opts.projectRoot ?? process.cwd())));
7820
- const text = opts.json === true ? JSON.stringify(jsonOut, null, 2) : textBlocks.join("\n\n");
8417
+ const text = opts.json === true ? displaySafeJson(jsonOut) : textBlocks.join("\n\n");
7821
8418
  if (anyFound) recordReadStat("brief_view", fullSourceBytes, text, opts.spec);
7822
8419
  return { text, code: anyFound ? 0 : 1 };
7823
8420
  }
@@ -7874,7 +8471,7 @@ function runFind(opts) {
7874
8471
  }
7875
8472
  if (opts.json === true) {
7876
8473
  const fuzzyPayload = fuzzyNames.length > 0 ? { fuzzy: true, matchedNames: fuzzyNames } : {};
7877
- emit(JSON.stringify({ files, truncated, totalCount: allFiles.length, ...fuzzyPayload }, null, 2));
8474
+ emit(displaySafeJson({ files, truncated, totalCount: allFiles.length, ...fuzzyPayload }));
7878
8475
  return 0;
7879
8476
  }
7880
8477
  if (fuzzyNames.length > 0) {
@@ -7914,7 +8511,7 @@ function runListSections(opts) {
7914
8511
  const filtered = matchesGrep !== void 0 ? labelled.filter((_, i) => matchesGrep(sections[i] ?? "")) : labelled;
7915
8512
  if (filtered.length === 0) {
7916
8513
  if (opts.json === true) {
7917
- emit(JSON.stringify({ items: [], truncated: false, totalCount: 0 }, null, 2));
8514
+ emit(displaySafeJson({ items: [], truncated: false, totalCount: 0 }));
7918
8515
  return 0;
7919
8516
  }
7920
8517
  emit(grepFilteredToEmptyNotice(preFilterCount, opts.grep ?? "", "section", "sections"));
@@ -7922,7 +8519,7 @@ function runListSections(opts) {
7922
8519
  }
7923
8520
  if (opts.json === true) {
7924
8521
  const capped = guardJsonRows(filtered);
7925
- emit(JSON.stringify({ items: capped.items, truncated: capped.truncated, totalCount: capped.totalCount }, null, 2));
8522
+ emit(displaySafeJson({ items: capped.items, truncated: capped.truncated, totalCount: capped.totalCount }));
7926
8523
  return 0;
7927
8524
  }
7928
8525
  for (const heading of filtered) {
@@ -8022,7 +8619,7 @@ function runChanged(opts = {}) {
8022
8619
  return 1;
8023
8620
  }
8024
8621
  const emptyEnvelope = () => {
8025
- emit(JSON.stringify({ items: [], truncated: false, totalCount: 0 }, null, 2));
8622
+ emit(displaySafeJson({ items: [], truncated: false, totalCount: 0 }));
8026
8623
  return 0;
8027
8624
  };
8028
8625
  if (changedFiles.length === 0) {
@@ -8083,14 +8680,14 @@ function runChanged(opts = {}) {
8083
8680
  const symbolFullBytes = symbolDiffBaselineBytes;
8084
8681
  if (opts.json === true) {
8085
8682
  const capped = guardJsonRows(allSymbols);
8086
- const text = JSON.stringify({ items: capped.items, truncated: capped.truncated, totalCount: capped.totalCount }, null, 2);
8683
+ const text = displaySafeJson({ items: capped.items, truncated: capped.truncated, totalCount: capped.totalCount });
8087
8684
  emit(text);
8088
8685
  recordReadStat("changed_lookup", symbolFullBytes, text, ref);
8089
8686
  return 0;
8090
8687
  }
8091
8688
  const symbolText = allSymbols.map((s) => `${s.name} (${s.kind}) \u2014 ${toDisplayPath(projectRoot, s.filePath)}:${s.lineStart}`).join("\n");
8092
8689
  for (const s of allSymbols) {
8093
- emit(`${s.name} (${s.kind}) \u2014 ${toDisplayPath(projectRoot, s.filePath)}:${s.lineStart}`);
8690
+ emit(`${displaySafeText(s.name)} (${displaySafeText(s.kind)}) \u2014 ${displaySafeText(toDisplayPath(projectRoot, s.filePath))}:${s.lineStart}`);
8094
8691
  }
8095
8692
  recordReadStat("changed_lookup", symbolFullBytes, symbolText, ref);
8096
8693
  return 0;
@@ -8098,7 +8695,7 @@ function runChanged(opts = {}) {
8098
8695
  const fullBytes = changedDiffBaselineBytes(cwd, ref, changedFiles);
8099
8696
  if (opts.json === true) {
8100
8697
  const capped = guardJsonRows(changedFiles);
8101
- const text = JSON.stringify({ items: capped.items, truncated: capped.truncated, totalCount: capped.totalCount }, null, 2);
8698
+ const text = displaySafeJson({ items: capped.items, truncated: capped.truncated, totalCount: capped.totalCount });
8102
8699
  emit(text);
8103
8700
  recordReadStat("changed_lookup", fullBytes, text, ref);
8104
8701
  return 0;
@@ -8199,19 +8796,19 @@ function runDiff(opts) {
8199
8796
  return 1;
8200
8797
  }
8201
8798
  if (diffResult.stdout.trim() === "") {
8202
- emit(`No changes to '${match.name}' in '${toDisplayPath(getDisplayRoot(opts.projectRoot), match.filePath)}'.`);
8799
+ emit(`No changes to '${displaySafeText(match.name)}' in '${displaySafeText(toDisplayPath(getDisplayRoot(opts.projectRoot), match.filePath))}'.`);
8203
8800
  return 0;
8204
8801
  }
8205
8802
  const { hunks } = splitDiffHunks(diffResult.stdout);
8206
8803
  const overlapping = hunks.filter((h) => h.start <= match.lineEnd && h.end >= match.lineStart);
8207
8804
  if (overlapping.length === 0) {
8208
- emit(`No changes to '${match.name}' (lines ${match.lineStart}-${match.lineEnd}) in '${toDisplayPath(getDisplayRoot(opts.projectRoot), match.filePath)}'.`);
8805
+ emit(`No changes to '${displaySafeText(match.name)}' (lines ${match.lineStart}-${match.lineEnd}) in '${displaySafeText(toDisplayPath(getDisplayRoot(opts.projectRoot), match.filePath))}'.`);
8209
8806
  return 0;
8210
8807
  }
8211
8808
  if (opts.json === true) {
8212
8809
  const capped = guardJsonRows(overlapping.map((h) => ({ start: h.start, end: h.end, text: h.text })));
8213
8810
  emit(
8214
- JSON.stringify(
8811
+ displaySafeJson(
8215
8812
  {
8216
8813
  symbol: match.name,
8217
8814
  file: match.filePath,
@@ -8220,14 +8817,12 @@ function runDiff(opts) {
8220
8817
  hunks: capped.items,
8221
8818
  truncated: capped.truncated,
8222
8819
  totalCount: capped.totalCount
8223
- },
8224
- null,
8225
- 2
8820
+ }
8226
8821
  )
8227
8822
  );
8228
8823
  return 0;
8229
8824
  }
8230
- const header = `# ${match.name} (${match.kind}) \u2014 ${toDisplayPath(getDisplayRoot(opts.projectRoot), match.filePath)}:${match.lineStart}-${match.lineEnd}`;
8825
+ const header = `# ${displaySafeText(match.name)} (${displaySafeText(match.kind)}) \u2014 ${displaySafeText(toDisplayPath(getDisplayRoot(opts.projectRoot), match.filePath))}:${match.lineStart}-${match.lineEnd}`;
8231
8826
  emit(guardText([header, ...overlapping.map((h) => h.text)].join("\n"), "diff"));
8232
8827
  return 0;
8233
8828
  }
@@ -8292,17 +8887,17 @@ function runLog(opts) {
8292
8887
  return 1;
8293
8888
  }
8294
8889
  if (logResult.exitCode !== 0) {
8295
- emitErr(`git log failed: ${logResult.stderr}`);
8890
+ emitErr(`git log failed: ${displaySafeText(logResult.stderr)}`);
8296
8891
  return 1;
8297
8892
  }
8298
8893
  if (logResult.stdout.trim() === "") {
8299
- emit(`No history for '${match.name}' (lines ${match.lineStart}-${match.lineEnd}) in '${toDisplayPath(getDisplayRoot(opts.projectRoot), match.filePath)}'.`);
8894
+ emit(`No history for '${displaySafeText(match.name)}' (lines ${match.lineStart}-${match.lineEnd}) in '${displaySafeText(toDisplayPath(getDisplayRoot(opts.projectRoot), match.filePath))}'.`);
8300
8895
  return 0;
8301
8896
  }
8302
8897
  if (opts.json === true) {
8303
8898
  const capped = guardJsonRows(parseLogDashLOutput(logResult.stdout));
8304
8899
  emit(
8305
- JSON.stringify(
8900
+ displaySafeJson(
8306
8901
  {
8307
8902
  symbol: match.name,
8308
8903
  file: match.filePath,
@@ -8311,32 +8906,30 @@ function runLog(opts) {
8311
8906
  commits: capped.items,
8312
8907
  truncated: capped.truncated,
8313
8908
  totalCount: capped.totalCount
8314
- },
8315
- null,
8316
- 2
8909
+ }
8317
8910
  )
8318
8911
  );
8319
8912
  return 0;
8320
8913
  }
8321
- const header = `# ${match.name} (${match.kind}) \u2014 ${toDisplayPath(getDisplayRoot(opts.projectRoot), match.filePath)}:${match.lineStart}-${match.lineEnd}`;
8914
+ const header = `# ${displaySafeText(match.name)} (${displaySafeText(match.kind)}) \u2014 ${displaySafeText(toDisplayPath(getDisplayRoot(opts.projectRoot), match.filePath))}:${match.lineStart}-${match.lineEnd}`;
8322
8915
  emit(guardText([header, logResult.stdout].join("\n"), "diff"));
8323
8916
  return 0;
8324
8917
  }
8325
8918
  function foldRealpath(p) {
8326
8919
  const n = normalizePath(p);
8327
- return process.platform === "win32" ? n.toLowerCase() : n;
8920
+ return process.platform === "win32" ? foldCaseForContainment(n) : n;
8328
8921
  }
8329
8922
  function runGrep(opts) {
8330
- const searchPaths = opts.path === void 0 ? [opts.projectRoot ?? process.cwd()] : Array.isArray(opts.path) ? opts.path : [opts.path];
8923
+ const anchorSearchPath = (p) => opts.projectRoot === void 0 || path4.isAbsolute(p) ? p : path4.resolve(opts.projectRoot, p);
8924
+ const searchPaths = opts.path === void 0 ? [opts.projectRoot ?? process.cwd()] : (Array.isArray(opts.path) ? opts.path : [opts.path]).map(anchorSearchPath);
8331
8925
  const maxLines = opts.maxLines ?? GREP_MAX_LINES;
8332
8926
  const contextLines = opts.context ?? 0;
8333
- let regex;
8334
- try {
8335
- regex = new RegExp(opts.pattern);
8336
- } catch {
8337
- emitErr(`Invalid regex: ${opts.pattern}`);
8927
+ const guarded = compileGuardedRegex(opts.pattern);
8928
+ if (!guarded.ok) {
8929
+ emitErr(`Invalid regex: ${opts.pattern} -- ${guarded.reason}`);
8338
8930
  return 1;
8339
8931
  }
8932
+ const regex = guarded.re;
8340
8933
  const hits = [];
8341
8934
  function searchFile(filePath) {
8342
8935
  try {
@@ -8464,7 +9057,7 @@ function runGrep(opts) {
8464
9057
  }
8465
9058
  if (opts.json === true) {
8466
9059
  const payload = { items: truncated, truncated: hits.length > maxLines, totalCount: hits.length };
8467
- emit(JSON.stringify(payload, null, 2));
9060
+ emit(displaySafeJson(payload));
8468
9061
  return 0;
8469
9062
  }
8470
9063
  for (const hit of truncated) {
@@ -8472,7 +9065,7 @@ function runGrep(opts) {
8472
9065
  if (hit.context !== void 0) {
8473
9066
  for (const line of renderContextWindow(hit.file, hit.line, hit.context, symbolTag)) emit(line);
8474
9067
  } else {
8475
- emit(`${hit.file}:${hit.line}: ${hit.text}${symbolTag}`);
9068
+ emit(`${displaySafeText(hit.file)}:${hit.line}: ${hit.text}${symbolTag}`);
8476
9069
  }
8477
9070
  }
8478
9071
  if (hits.length > maxLines) {
@@ -8577,7 +9170,7 @@ function runConfigGet(opts) {
8577
9170
  return 1;
8578
9171
  }
8579
9172
  }
8580
- emit(JSON.stringify(obj));
9173
+ emit(displaySafeJson(obj, 0));
8581
9174
  return 0;
8582
9175
  } catch {
8583
9176
  emitErr(`Failed to parse JSON: ${opts.file}`);
@@ -8673,7 +9266,7 @@ function runExports(opts) {
8673
9266
  return 1;
8674
9267
  }
8675
9268
  const diskPath = resolveAgainstProjectRoot(opts.file, opts.projectRoot);
8676
- const symbols = querySymbols({ filePath: resolveIndexPath(diskPath), limit: 500 });
9269
+ const symbols = querySymbols({ filePath: resolveIndexPath(diskPath), limit: -1 });
8677
9270
  const kindOf = (name) => symbols.find((s) => s.name === name)?.kind ?? "export";
8678
9271
  const locOf = (name) => {
8679
9272
  const s = symbols.find((sym) => sym.name === name);
@@ -8698,7 +9291,7 @@ function runExports(opts) {
8698
9291
  for (const n of extractExportNames(text, ext)) if (!names.includes(n)) names.push(n);
8699
9292
  }
8700
9293
  if (names.length === 0) {
8701
- emit(`No exported symbols found in '${opts.file}'`);
9294
+ emit(`No exported symbols found in '${displaySafeText(opts.file)}'`);
8702
9295
  return 0;
8703
9296
  }
8704
9297
  const preFilterCount = names.length;
@@ -8707,7 +9300,7 @@ function runExports(opts) {
8707
9300
  const fullSourceBytes = sumFileSizes([diskPath]);
8708
9301
  if (filteredNames.length === 0) {
8709
9302
  if (opts.json === true) {
8710
- const jsonText = JSON.stringify([], null, 2);
9303
+ const jsonText = displaySafeJson([]);
8711
9304
  emit(jsonText);
8712
9305
  recordReadStat("exports", fullSourceBytes, jsonText, opts.file);
8713
9306
  return 0;
@@ -8718,13 +9311,11 @@ function runExports(opts) {
8718
9311
  return 0;
8719
9312
  }
8720
9313
  if (opts.json === true) {
8721
- const jsonText = JSON.stringify(
9314
+ const jsonText = displaySafeJson(
8722
9315
  filteredNames.map((n) => {
8723
9316
  const loc = locOf(n);
8724
9317
  return { name: n, kind: kindOf(n), lineStart: loc?.lineStart ?? null, lineEnd: loc?.lineEnd ?? null };
8725
- }),
8726
- null,
8727
- 2
9318
+ })
8728
9319
  );
8729
9320
  emit(jsonText);
8730
9321
  recordReadStat("exports", fullSourceBytes, jsonText, opts.file);
@@ -8929,6 +9520,27 @@ function extractImports(text, ext) {
8929
9520
  let m;
8930
9521
  while ((m = re.exec(line)) !== null) push(m[1]);
8931
9522
  }
9523
+ } else if (e === ".vb" || e === ".bas" || e === ".vbs") {
9524
+ for (const line of lines) {
9525
+ const m = /^\s*Imports\s+([^'<][^']*)/i.exec(line);
9526
+ if (!m) continue;
9527
+ for (const clause of m[1].split(",")) {
9528
+ const c = clause.trim();
9529
+ if (c === "" || c.startsWith("<")) continue;
9530
+ const alias = /^[A-Za-z_]\w*\s*=\s*([\w.]+(?:\(Of[^)]*\))?)/i.exec(c);
9531
+ push(alias ? alias[1] : (/^[\w.]+/.exec(c) ?? [void 0])[0]);
9532
+ }
9533
+ }
9534
+ } else if (/^\.(?:cbl|cob|cpy|cobol)$/i.test(e)) {
9535
+ for (const imp of extractCobol(text, `imports${e}`).imports) push(imp.target);
9536
+ } else if (/^\.ns[pnsalgch]$/i.test(e)) {
9537
+ for (const imp of extractNatural(text, `imports${e}`).imports) push(imp.target);
9538
+ } else if (STATEMENT_ADAPTER_IMPORTS.has(e)) {
9539
+ for (const imp of STATEMENT_ADAPTER_IMPORTS.get(e)(text, `imports${e}`).imports) push(imp.target);
9540
+ } else if (braceAdapterImportsFor(e, text) !== void 0) {
9541
+ for (const imp of braceAdapterImportsFor(e, text)(text, `imports${e}`).imports) push(imp.target);
9542
+ } else if ((e === ".p" || e === ".w" || e === ".cls") && isAblSource(text)) {
9543
+ for (const imp of extractAbl(text, `imports${e}`).imports) push(imp.target);
8932
9544
  } else if (e === ".lua") {
8933
9545
  for (const line of lines) {
8934
9546
  const re = /\brequire\s*\(?\s*["']([^"']+)["']/g;
@@ -9022,9 +9634,9 @@ function extractImports(text, ext) {
9022
9634
  return found;
9023
9635
  }
9024
9636
  function importsExtensionFor(filePath) {
9025
- const base = path4.basename(filePath).toLowerCase();
9026
- if (base === "makefile" || base === "gnumakefile" || base === "bsdmakefile") return ".mk";
9027
- return path4.extname(filePath);
9637
+ const ext = path4.extname(filePath);
9638
+ if (ext === "" || FILENAME_LANGUAGE.has(path4.basename(filePath).toLowerCase())) return basenameImportsExtension(detectLanguage(filePath)) ?? ext;
9639
+ return ext;
9028
9640
  }
9029
9641
  function runImports(opts) {
9030
9642
  const multiFiles = parseMultiFileSpec(opts.file);
@@ -9037,7 +9649,7 @@ function runImports(opts) {
9037
9649
  }
9038
9650
  const imports = extractImports(text, importsExtensionFor(opts.file));
9039
9651
  if (imports.length === 0) {
9040
- emit(`No imports found in '${opts.file}'`);
9652
+ emit(`No imports found in '${displaySafeText(opts.file)}'`);
9041
9653
  return 0;
9042
9654
  }
9043
9655
  const preFilterCount = imports.length;
@@ -9046,7 +9658,7 @@ function runImports(opts) {
9046
9658
  const fullSourceBytes = sumFileSizes([diskPath]);
9047
9659
  if (filteredImports.length === 0) {
9048
9660
  if (opts.json === true) {
9049
- const jsonText = JSON.stringify({ items: [], truncated: false, totalCount: 0 }, null, 2);
9661
+ const jsonText = displaySafeJson({ items: [], truncated: false, totalCount: 0 });
9050
9662
  emit(jsonText);
9051
9663
  recordReadStat("imports", fullSourceBytes, jsonText, opts.file);
9052
9664
  return 0;
@@ -9058,7 +9670,7 @@ function runImports(opts) {
9058
9670
  }
9059
9671
  if (opts.json === true) {
9060
9672
  const capped = guardJsonRows(filteredImports);
9061
- const jsonText = JSON.stringify({ items: capped.items, truncated: capped.truncated, totalCount: capped.totalCount }, null, 2);
9673
+ const jsonText = displaySafeJson({ items: capped.items, truncated: capped.truncated, totalCount: capped.totalCount });
9062
9674
  emit(jsonText);
9063
9675
  recordReadStat("imports", fullSourceBytes, jsonText, opts.file);
9064
9676
  return 0;
@@ -9110,7 +9722,7 @@ async function runSemantic(query, opts) {
9110
9722
  if (opts.limit !== void 0 && opts.limit <= 0) {
9111
9723
  const message = `--limit must be a positive number, got: ${opts.limit}`;
9112
9724
  if (opts.json === true) {
9113
- return { text: JSON.stringify({ error: message }, null, 2), code: 1 };
9725
+ return { text: displaySafeJson({ error: message }), code: 1 };
9114
9726
  }
9115
9727
  return { text: message, code: 1 };
9116
9728
  }
@@ -9119,32 +9731,39 @@ async function runSemantic(query, opts) {
9119
9731
  if (!path4.isAbsolute(opts.projectRoot) || !fs4.existsSync(opts.projectRoot) || !fs4.statSync(opts.projectRoot).isDirectory()) {
9120
9732
  const message = `token-goat: projectRoot must be an absolute, existing directory, got '${opts.projectRoot}'`;
9121
9733
  if (opts.json === true) {
9122
- return { text: JSON.stringify({ error: message }, null, 2), code: 1 };
9734
+ return { text: displaySafeJson({ error: message }), code: 1 };
9123
9735
  }
9124
9736
  return { text: message, code: 1 };
9125
9737
  }
9126
9738
  }
9127
9739
  const rootDir = opts.projectRoot ?? resolveProjectRoot({ project: process.cwd() });
9128
- if (!isAvailable()) {
9740
+ const embeddingsEnabled = loadConfig().indexing?.embeddings_enabled ?? true;
9741
+ if (embeddingsEnabled && !isAvailable()) {
9129
9742
  console.warn(
9130
9743
  "Matching on meaning is off (onnxruntime-node is not installed); these results come from keyword search alone. Install it with: npm install -g onnxruntime-node (drop -g if token-goat is a project dependency)"
9131
9744
  );
9745
+ } else if (!embeddingsEnabled) {
9746
+ console.warn(
9747
+ "Matching on meaning is off (indexing.embeddings_enabled / TOKEN_GOAT_EMBEDDINGS_ENABLED is disabled); these results come from keyword search alone."
9748
+ );
9132
9749
  }
9133
9750
  const overFetchForMerge = Math.min(MAX_OVER_FETCH, n * OVER_FETCH_FACTOR);
9134
9751
  let rawHits = [];
9135
- try {
9136
- rawHits = await searchSemantic(
9137
- getDb(globalDbPath()),
9138
- query,
9139
- overFetchForMerge,
9140
- void 0,
9141
- void 0,
9142
- rootDir
9143
- );
9144
- } catch (e) {
9145
- console.warn(
9146
- `Matching on meaning is off (${extractErrorMessage(e)}); these results come from keyword search alone.`
9147
- );
9752
+ if (embeddingsEnabled) {
9753
+ try {
9754
+ rawHits = await searchSemantic(
9755
+ getDb(globalDbPath()),
9756
+ query,
9757
+ overFetchForMerge,
9758
+ void 0,
9759
+ void 0,
9760
+ rootDir
9761
+ );
9762
+ } catch (e) {
9763
+ console.warn(
9764
+ `Matching on meaning is off (${extractErrorMessage(e)}); these results come from keyword search alone.`
9765
+ );
9766
+ }
9148
9767
  }
9149
9768
  if (rawHits.length === 0 && isAvailable()) {
9150
9769
  try {
@@ -9164,7 +9783,7 @@ async function runSemantic(query, opts) {
9164
9783
  const fused = /* @__PURE__ */ new Map();
9165
9784
  mergedHits.forEach((h, denseRank) => {
9166
9785
  const enclosing = resolveEnclosingSymbol(h.filePath, h.startLine);
9167
- const key = enclosing !== null ? `${h.filePath}::${enclosing.name}` : `${h.filePath}::L${h.startLine}`;
9786
+ const key = enclosing !== null ? `${h.filePath}::${enclosing.name}@${enclosing.lineStart}` : `${h.filePath}::L${h.startLine}`;
9168
9787
  fused.set(key, {
9169
9788
  filePath: h.filePath,
9170
9789
  startLine: h.startLine,
@@ -9177,7 +9796,7 @@ async function runSemantic(query, opts) {
9177
9796
  });
9178
9797
  });
9179
9798
  ftsRows.forEach((s, ftsRank) => {
9180
- const key = `${s.filePath}::${s.name}`;
9799
+ const key = `${s.filePath}::${s.name}@${s.lineStart}`;
9181
9800
  const existing = fused.get(key);
9182
9801
  if (existing !== void 0) {
9183
9802
  existing.rrf += 1 / (RRF_K + ftsRank);
@@ -9210,6 +9829,7 @@ async function runSemantic(query, opts) {
9210
9829
  const hadDense = mergedHits.length > 0;
9211
9830
  const hadFts = ftsRows.length > 0;
9212
9831
  const source = hadDense && hadFts ? "hybrid" : hadDense ? "embeddings" : "fts";
9832
+ warnIfFilesStale(hits.map((h) => h.filePath));
9213
9833
  if (hits.length > 0) {
9214
9834
  if (opts.json === true) {
9215
9835
  const items = hits.map((h) => ({
@@ -9223,7 +9843,7 @@ async function runSemantic(query, opts) {
9223
9843
  }));
9224
9844
  const capped = guardJsonRows(items);
9225
9845
  const limitTruncated = hits.length < eligibleCount;
9226
- const text3 = JSON.stringify({ source, ...capped, truncated: capped.truncated || limitTruncated, totalCount: eligibleCount, ...candidatesClipped ? { totalCountIsFloor: true } : {} }, null, 2);
9846
+ const text3 = displaySafeJson({ source, ...capped, truncated: capped.truncated || limitTruncated, totalCount: eligibleCount, ...candidatesClipped ? { totalCountIsFloor: true } : {} });
9227
9847
  recordReadStat("semantic_search", sumFileSizes(hits.map((h) => h.filePath)), text3, query);
9228
9848
  return { text: text3, code: 0 };
9229
9849
  }
@@ -9247,7 +9867,7 @@ ${previewLines(h.previewText, 3)}`;
9247
9867
  const notice = grepFilteredToEmptyNotice(preFilterCount, opts.grep ?? "", "match", "matches");
9248
9868
  if (opts.json === true) {
9249
9869
  const payload = { source: "fts", items: [], truncated: false, totalCount: 0, grepFilteredToEmpty: true, hint: notice.trim() };
9250
- return { text: JSON.stringify(payload, null, 2), code: 0 };
9870
+ return { text: displaySafeJson(payload), code: 0 };
9251
9871
  }
9252
9872
  return { text: `token-goat: ${notice.trim()}`, code: 0 };
9253
9873
  }
@@ -9255,7 +9875,7 @@ ${previewLines(h.previewText, 3)}`;
9255
9875
  const notice = `no non-test matches for '${query}' (${excludeTestsHiddenNote(suppressedTotal)})`;
9256
9876
  if (opts.json === true) {
9257
9877
  const payload = { source: "fts", items: [], truncated: false, totalCount: 0, excludeTestsFilteredToEmpty: true, hint: notice };
9258
- return { text: JSON.stringify(payload, null, 2), code: 0 };
9878
+ return { text: displaySafeJson(payload), code: 0 };
9259
9879
  }
9260
9880
  return { text: `token-goat: ${notice}`, code: 0 };
9261
9881
  }
@@ -9271,7 +9891,7 @@ ${previewLines(h.previewText, 3)}`;
9271
9891
  cachedAt: entry.createdAt
9272
9892
  }));
9273
9893
  const capped = guardJsonRows(items);
9274
- const text3 = JSON.stringify({ source: "workspace-evidence", ...capped }, null, 2);
9894
+ const text3 = displaySafeJson({ source: "workspace-evidence", ...capped });
9275
9895
  recordReadStat("semantic_search", evidenceFullBytes, text3, query);
9276
9896
  return { text: text3, code: 0 };
9277
9897
  }
@@ -9287,7 +9907,7 @@ ${previewLines(entry.text, 3)}`).join("\n\n"),
9287
9907
  const indexEmpty = isIndexEmptyForProject(globalDbPath(), rootDir);
9288
9908
  if (opts.json === true) {
9289
9909
  const payload = indexEmpty ? { source: "fts", items: [], truncated: false, totalCount: 0, indexEmpty: true, hint: emptyIndexMessage(rootDir) } : { source: "fts", items: [], truncated: false, totalCount: 0 };
9290
- const text2 = JSON.stringify(payload, null, 2);
9910
+ const text2 = displaySafeJson(payload);
9291
9911
  return { text: text2, code: 1 };
9292
9912
  }
9293
9913
  const text = indexEmpty ? `token-goat: no matches for '${query}'
@@ -9313,7 +9933,7 @@ function runNoteGet(opts) {
9313
9933
  createdAt: note.createdAt,
9314
9934
  updatedAt: note.updatedAt
9315
9935
  };
9316
- const text2 = JSON.stringify(payload, null, 2);
9936
+ const text2 = displaySafeJson(payload);
9317
9937
  recordStat("note_read");
9318
9938
  return { text: text2, code: 0 };
9319
9939
  }
@@ -9335,7 +9955,7 @@ function runNoteList(opts = {}) {
9335
9955
  updatedAt: note.updatedAt
9336
9956
  }));
9337
9957
  recordStat("note_list");
9338
- return { text: JSON.stringify(items, null, 2), code: 0 };
9958
+ return { text: displaySafeJson(items), code: 0 };
9339
9959
  }
9340
9960
  recordStat("note_list");
9341
9961
  if (filtered.length === 0) {
@@ -9670,7 +10290,7 @@ function renderModules(result, top) {
9670
10290
 
9671
10291
  // src/graph_commands.ts
9672
10292
  var DEFAULT_REF_QUERY_LIMIT = 500;
9673
- var ALL_SYMBOLS_IN_FILE_LIMIT = 1e4;
10293
+ var ALL_SYMBOLS_IN_FILE_LIMIT = -1;
9674
10294
  var UNBOUNDED_REF_LIMIT = -1;
9675
10295
  function emit2(text) {
9676
10296
  const payload = colorStdout() ? text : stripAnsi(text);
@@ -9818,7 +10438,7 @@ function runCallers(opts) {
9818
10438
  if (entries.length === 0) {
9819
10439
  if (matchesGrep !== void 0 && preGrepCount > 0) {
9820
10440
  if (opts.json === true) {
9821
- emit2(JSON.stringify({ items: [], truncated: false, totalCount: 0, hiddenByGrep: preGrepCount }, null, 2));
10441
+ emit2(displaySafeJson({ items: [], truncated: false, totalCount: 0, hiddenByGrep: preGrepCount }));
9822
10442
  return 0;
9823
10443
  }
9824
10444
  emit2(grepFilteredToEmptyNotice(preGrepCount, opts.grep ?? "", "caller", "callers"));
@@ -9826,7 +10446,7 @@ function runCallers(opts) {
9826
10446
  }
9827
10447
  if (opts.excludeTests === true && suppressed > 0) {
9828
10448
  if (opts.json === true) {
9829
- emit2(JSON.stringify({ items: [], truncated: false, totalCount: 0, hiddenByExcludeTests: suppressed }, null, 2));
10449
+ emit2(displaySafeJson({ items: [], truncated: false, totalCount: 0, hiddenByExcludeTests: suppressed }));
9830
10450
  return 1;
9831
10451
  }
9832
10452
  emitErr2(`No non-test references found for '${opts.symbol}' (${excludeTestsHiddenNote(suppressed)})`);
@@ -9841,7 +10461,7 @@ function runCallers(opts) {
9841
10461
  const defPaths = fileHint !== void 0 ? [fileHint] : defRows.map((r) => r.filePath);
9842
10462
  const firstDefPath = defPaths[0];
9843
10463
  if (firstDefPath !== void 0 && defPaths.every((fp) => !isRefIndexedFile(fp))) {
9844
- emitErr2(refBlindLanguageNotice(name, detectLanguage(firstDefPath), toDisplayPath(rootDir, firstDefPath)));
10464
+ emitErr2(refBlindLanguageNotice(name, detectLanguageOfFile(firstDefPath), toDisplayPath(rootDir, firstDefPath)));
9845
10465
  return 1;
9846
10466
  }
9847
10467
  const kindRows = fileHint !== void 0 ? querySymbols({ name, filePath: fileHint, limit: REF_BLIND_DEF_PROBE_LIMIT }) : defRows;
@@ -9865,7 +10485,7 @@ function runCallers(opts) {
9865
10485
  const capped = guardJsonRows(rows);
9866
10486
  const limitTruncated = entries.length < filtered.length || sqlTruncated;
9867
10487
  const hiddenByGrep = preGrepCount - filtered.length;
9868
- emit2(JSON.stringify({ items: capped.items, truncated: capped.truncated || limitTruncated, totalCount: filtered.length, ...hiddenByGrep > 0 ? { hiddenByGrep } : {}, ...opts.excludeTests === true && suppressed > 0 ? { hiddenByExcludeTests: suppressed } : {} }, null, 2));
10488
+ emit2(displaySafeJson({ items: capped.items, truncated: capped.truncated || limitTruncated, totalCount: filtered.length, ...hiddenByGrep > 0 ? { hiddenByGrep } : {}, ...opts.excludeTests === true && suppressed > 0 ? { hiddenByExcludeTests: suppressed } : {} }));
9869
10489
  return 0;
9870
10490
  }
9871
10491
  if (opts.excludeTests === true && suppressed > 0) {
@@ -9933,8 +10553,8 @@ function runCallChain(opts) {
9933
10553
  const hiddenByExcludeTests = noCallers && opts.excludeTests === true ? suppressedCount : 0;
9934
10554
  const refBlindKinds = noCallers ? refBlindRootKinds().blindKinds : [];
9935
10555
  const blindRootPath = noCallers ? refBlindRootPath() : void 0;
9936
- const refBlindLanguage = blindRootPath !== void 0 ? { language: detectLanguage(blindRootPath), definedIn: toDisplayPath(rootDir, blindRootPath) } : void 0;
9937
- emit2(JSON.stringify({ chains: filteredChains, ...hiddenByGrep > 0 ? { hiddenByGrep } : {}, ...hiddenByExcludeTests > 0 ? { hiddenByExcludeTests } : {}, ...refBlindKinds.length > 0 ? { refBlindKinds } : {}, ...refBlindLanguage !== void 0 ? { refBlindLanguage } : {} }, null, 2));
10556
+ const refBlindLanguage = blindRootPath !== void 0 ? { language: detectLanguageOfFile(blindRootPath), definedIn: toDisplayPath(rootDir, blindRootPath) } : void 0;
10557
+ emit2(displaySafeJson({ chains: filteredChains, ...hiddenByGrep > 0 ? { hiddenByGrep } : {}, ...hiddenByExcludeTests > 0 ? { hiddenByExcludeTests } : {}, ...refBlindKinds.length > 0 ? { refBlindKinds } : {}, ...refBlindLanguage !== void 0 ? { refBlindLanguage } : {} }));
9938
10558
  return 0;
9939
10559
  }
9940
10560
  if (noCallers) {
@@ -9944,7 +10564,7 @@ function runCallChain(opts) {
9944
10564
  }
9945
10565
  const blindRoot = refBlindRootPath();
9946
10566
  if (blindRoot !== void 0) {
9947
- emitErr2(refBlindLanguageNotice(name, detectLanguage(blindRoot), toDisplayPath(rootDir, blindRoot)));
10567
+ emitErr2(refBlindLanguageNotice(name, detectLanguageOfFile(blindRoot), toDisplayPath(rootDir, blindRoot)));
9948
10568
  emit2(`${name} (no callers recorded)`);
9949
10569
  return 0;
9950
10570
  }
@@ -10035,7 +10655,7 @@ function runImpact(opts) {
10035
10655
  if (matchesGrep !== void 0 && sorted.length === 0 && allSorted.length > 0) {
10036
10656
  if (opts.json === true) {
10037
10657
  emitErr2(grepFilteredToEmptyNotice(allSorted.length, opts.grep, "impacted symbol", "impacted symbols"));
10038
- emit2(JSON.stringify([], null, 2));
10658
+ emit2(displaySafeJson([]));
10039
10659
  return 0;
10040
10660
  }
10041
10661
  emit2(grepFilteredToEmptyNotice(allSorted.length, opts.grep, "impacted symbol", "impacted symbols"));
@@ -10044,7 +10664,7 @@ function runImpact(opts) {
10044
10664
  if (sorted.length === 0) {
10045
10665
  if (opts.excludeTests === true && suppressedCount > 0) {
10046
10666
  if (opts.json === true) {
10047
- emit2(JSON.stringify([], null, 2));
10667
+ emit2(displaySafeJson([]));
10048
10668
  return 1;
10049
10669
  }
10050
10670
  emitErr2(`No non-test impact found for '${opts.symbol}' (${excludeTestsHiddenNote(suppressedCount)})`);
@@ -10058,7 +10678,7 @@ function runImpact(opts) {
10058
10678
  const defPaths = fileHint !== void 0 ? [fileHint] : defRows.map((r) => r.filePath);
10059
10679
  const firstDefPath = defPaths[0];
10060
10680
  if (firstDefPath !== void 0 && defPaths.every((fp) => !isRefIndexedFile(fp))) {
10061
- emitErr2(refBlindLanguageNotice(rootName, detectLanguage(firstDefPath), toDisplayPath(rootDir, firstDefPath)));
10681
+ emitErr2(refBlindLanguageNotice(rootName, detectLanguageOfFile(firstDefPath), toDisplayPath(rootDir, firstDefPath)));
10062
10682
  return 1;
10063
10683
  }
10064
10684
  const kindRows = fileHint !== void 0 ? querySymbols({ name: rootName, filePath: fileHint, limit: REF_BLIND_DEF_PROBE_LIMIT }) : defRows;
@@ -10072,7 +10692,7 @@ function runImpact(opts) {
10072
10692
  return 1;
10073
10693
  }
10074
10694
  if (opts.json === true) {
10075
- emit2(JSON.stringify(sorted.map(([symbol, h]) => ({ symbol, hops: h })), null, 2));
10695
+ emit2(displaySafeJson(sorted.map(([symbol, h]) => ({ symbol, hops: h }))));
10076
10696
  return 0;
10077
10697
  }
10078
10698
  const MODULE_SCOPE_PREFIX = "(module scope) ";
@@ -10229,7 +10849,7 @@ function runDead(opts) {
10229
10849
  }));
10230
10850
  const topTruncated = sliced.length < grepped.length;
10231
10851
  const hiddenByGrep = preGrepCount - grepped.length;
10232
- emit2(JSON.stringify({ items: capped.items, truncated: capped.truncated || topTruncated, totalCount: grepped.length, ...hiddenByGrep > 0 ? { hiddenByGrep } : {}, ...opts.excludeTests === true && suppressed > 0 ? { hiddenByExcludeTests: suppressed } : {}, ...blindKinds.length > 0 ? { excludedKinds: blindKinds, excludedKindsReason: REF_BLIND_KIND_REASON } : {}, ...refBlindByLanguage > 0 ? { unassessableByLanguage: refBlindByLanguage } : {} }, null, 2));
10852
+ emit2(displaySafeJson({ items: capped.items, truncated: capped.truncated || topTruncated, totalCount: grepped.length, ...hiddenByGrep > 0 ? { hiddenByGrep } : {}, ...opts.excludeTests === true && suppressed > 0 ? { hiddenByExcludeTests: suppressed } : {}, ...blindKinds.length > 0 ? { excludedKinds: blindKinds, excludedKindsReason: REF_BLIND_KIND_REASON } : {}, ...refBlindByLanguage > 0 ? { unassessableByLanguage: refBlindByLanguage } : {} }));
10233
10853
  return 0;
10234
10854
  }
10235
10855
  if (sliced.length === 0) {
@@ -10249,7 +10869,7 @@ function runDead(opts) {
10249
10869
  emit2(`${countNoun(sliced.length, "dead symbol")} (${excludeTestsHiddenNote(suppressed)})`);
10250
10870
  }
10251
10871
  for (const r of sliced) {
10252
- emit2(`${r.name} ${toDisplayPath(rootDir, r.file)}:${r.line}`);
10872
+ emit2(`${displaySafeText(r.name)} ${displaySafeText(toDisplayPath(rootDir, r.file))}:${r.line}`);
10253
10873
  }
10254
10874
  return 0;
10255
10875
  }
@@ -10326,7 +10946,7 @@ function runDeps(opts) {
10326
10946
  const filteredExternal = matchesGrep !== void 0 ? external.filter((e) => matchesGrep(e)) : external;
10327
10947
  if (opts.json === true) {
10328
10948
  const hiddenByGrep = preFilterCount - (filteredInternal.length + filteredExternal.length);
10329
- emit2(JSON.stringify({ file: toDisplayPath(rootDir, normalizePath(opts.file)), internal: filteredInternal, external: filteredExternal, ...hiddenByGrep > 0 ? { hiddenByGrep } : {} }, null, 2));
10949
+ emit2(displaySafeJson({ file: toDisplayPath(rootDir, normalizePath(opts.file)), internal: filteredInternal, external: filteredExternal, ...hiddenByGrep > 0 ? { hiddenByGrep } : {} }));
10330
10950
  return 0;
10331
10951
  }
10332
10952
  if (matchesGrep !== void 0 && preFilterCount > 0 && filteredInternal.length === 0 && filteredExternal.length === 0) {
@@ -10436,7 +11056,7 @@ function refBlindKindVerdict(rows) {
10436
11056
  var CORE_SYMBOL_KINDS = [
10437
11057
  .../* @__PURE__ */ new Set([...GENERIC_SYMBOL_KINDS, ...TYPE_KINDS, ...OTHER_ADAPTER_SYMBOL_KINDS])
10438
11058
  ];
10439
- var TYPES_SCAN_LIMIT = 5e3;
11059
+ var TYPES_SCAN_LIMIT = -1;
10440
11060
  function runTypes(opts) {
10441
11061
  if (opts.limit !== void 0 && opts.limit <= 0) {
10442
11062
  emitErr2(`--limit must be a positive number, got: ${opts.limit}`);
@@ -10482,7 +11102,7 @@ function runTypes(opts) {
10482
11102
  const ctx = opts.file !== void 0 ? ` in '${opts.file}'` : "";
10483
11103
  if (excludeTests && suppressed > 0) {
10484
11104
  if (opts.json === true) {
10485
- emit2(JSON.stringify({ items: [], truncated: false, totalCount: 0, hiddenByExcludeTests: suppressed }, null, 2));
11105
+ emit2(displaySafeJson({ items: [], truncated: false, totalCount: 0, hiddenByExcludeTests: suppressed }));
10486
11106
  return 0;
10487
11107
  }
10488
11108
  emit2(`No non-test type declarations found${ctx} (${excludeTestsHiddenNote(suppressed)})`);
@@ -10506,7 +11126,7 @@ function runTypes(opts) {
10506
11126
  const cappedOut = eligibleCount - filtered.length;
10507
11127
  if (filtered.length === 0) {
10508
11128
  if (opts.json === true) {
10509
- emit2(JSON.stringify({ items: [], truncated: false, totalCount: 0, hiddenByGrep: preFilterCount }, null, 2));
11129
+ emit2(displaySafeJson({ items: [], truncated: false, totalCount: 0, hiddenByGrep: preFilterCount }));
10510
11130
  return 0;
10511
11131
  }
10512
11132
  emit2(grepFilteredToEmptyNotice(preFilterCount, opts.grep ?? "", "type declaration", "type declarations"));
@@ -10515,14 +11135,14 @@ function runTypes(opts) {
10515
11135
  if (opts.json === true) {
10516
11136
  const capped = guardJsonRows(filtered.map((r) => ({ ...r, filePath: toDisplayPath(rootDir, r.filePath) })));
10517
11137
  const hiddenByGrep = preFilterCount - eligibleCount;
10518
- emit2(JSON.stringify({ items: capped.items, truncated: capped.truncated || cappedOut > 0, totalCount: eligibleCount, ...hiddenByGrep > 0 ? { hiddenByGrep } : {}, ...excludeTests && suppressed > 0 ? { hiddenByExcludeTests: suppressed } : {} }, null, 2));
11138
+ emit2(displaySafeJson({ items: capped.items, truncated: capped.truncated || cappedOut > 0, totalCount: eligibleCount, ...hiddenByGrep > 0 ? { hiddenByGrep } : {}, ...excludeTests && suppressed > 0 ? { hiddenByExcludeTests: suppressed } : {} }));
10519
11139
  return 0;
10520
11140
  }
10521
11141
  if (excludeTests && suppressed > 0) {
10522
11142
  emit2(`${countNoun(filtered.length, "type declaration")} (${excludeTestsHiddenNote(suppressed)})`);
10523
11143
  }
10524
11144
  for (const r of filtered) {
10525
- emit2(`${r.name} ${r.kind} ${toDisplayPath(rootDir, r.filePath)}:${r.lineStart}`);
11145
+ emit2(`${displaySafeText(r.name)} ${displaySafeText(r.kind)} ${displaySafeText(toDisplayPath(rootDir, r.filePath))}:${r.lineStart}`);
10526
11146
  }
10527
11147
  if (cappedOut > 0) {
10528
11148
  emitErr2(`Showing ${filtered.length} of ${eligibleCount} type declarations (--limit is per kind; raise it to see the rest).`);
@@ -10560,7 +11180,7 @@ function runScope(opts) {
10560
11180
  return 1;
10561
11181
  }
10562
11182
  emitErr2(
10563
- `No indexed symbols in '${file}' \u2014 the file exists but nothing is indexed for it, so every line looks empty`
11183
+ symbolExtractorGap(file, filePath) ?? `No indexed symbols in '${file}' \u2014 the file exists but nothing is indexed for it, so every line looks empty`
10564
11184
  );
10565
11185
  return 1;
10566
11186
  }
@@ -10570,16 +11190,14 @@ function runScope(opts) {
10570
11190
  const scopeDisplayRoot = getDisplayRoot();
10571
11191
  if (opts.json === true) {
10572
11192
  emit2(
10573
- JSON.stringify(
10574
- enclosing.map((s) => ({ ...s, filePath: toDisplayPath(scopeDisplayRoot, s.filePath) })),
10575
- null,
10576
- 2
11193
+ displaySafeJson(
11194
+ enclosing.map((s) => ({ ...s, filePath: toDisplayPath(scopeDisplayRoot, s.filePath) }))
10577
11195
  )
10578
11196
  );
10579
11197
  return 0;
10580
11198
  }
10581
11199
  for (const s of enclosing) {
10582
- emit2(`${s.name} ${s.kind} ${toDisplayPath(scopeDisplayRoot, s.filePath)}:${s.lineStart}-${s.lineEnd}`);
11200
+ emit2(`${displaySafeText(s.name)} ${displaySafeText(s.kind)} ${displaySafeText(toDisplayPath(scopeDisplayRoot, s.filePath))}:${s.lineStart}-${s.lineEnd}`);
10583
11201
  }
10584
11202
  return 0;
10585
11203
  }
@@ -10709,10 +11327,10 @@ function runSimilar(opts) {
10709
11327
  emitErr2(`Showing top ${results.length} of ${matches.length} similar symbols (raise --top to see the rest).`);
10710
11328
  }
10711
11329
  if (opts.json === true) {
10712
- emit2(JSON.stringify(results.map((h) => ({ name: h.name, kind: h.kind, file: h.filePath, line: h.lineStart })), null, 2));
11330
+ emit2(displaySafeJson(results.map((h) => ({ name: h.name, kind: h.kind, file: h.filePath, line: h.lineStart }))));
10713
11331
  return 0;
10714
11332
  }
10715
- for (const h of results) emit2(`${h.name} ${h.kind} ${toDisplayPath(rootDir, h.filePath)}:${h.lineStart}`);
11333
+ for (const h of results) emit2(`${displaySafeText(h.name)} ${displaySafeText(h.kind)} ${displaySafeText(toDisplayPath(rootDir, h.filePath))}:${h.lineStart}`);
10716
11334
  return 0;
10717
11335
  }
10718
11336
  function runContextFor(opts) {
@@ -10748,10 +11366,10 @@ function runContextFor(opts) {
10748
11366
  if (moreBeyondTop) notices.push(`More matches exist beyond --top ${top} (raise it to see them).`);
10749
11367
  for (const n of notices) emitErr2(n);
10750
11368
  if (opts.json === true) {
10751
- emit2(JSON.stringify(entries, null, 2));
11369
+ emit2(displaySafeJson(entries));
10752
11370
  return 0;
10753
11371
  }
10754
- for (const e of entries) emit2(`token-goat read "${toDisplayPath(rootDir, e.file)}::${e.symbol}@${e.line}"`);
11372
+ for (const e of entries) emit2(`token-goat read "${displaySafeText(toDisplayPath(rootDir, e.file))}::${displaySafeText(e.symbol)}@${e.line}"`);
10755
11373
  return 0;
10756
11374
  }
10757
11375
  function runTestFor(opts) {
@@ -10783,11 +11401,11 @@ function runTestFor(opts) {
10783
11401
  }
10784
11402
  if (opts.json === true) {
10785
11403
  const capped = guardJsonRows(results.map((r) => ({ ...r, testFile: toDisplayPath(rootDir, r.testFile) })));
10786
- emit2(JSON.stringify({ items: capped.items, truncated: capped.truncated, totalCount: capped.totalCount }, null, 2));
11404
+ emit2(displaySafeJson({ items: capped.items, truncated: capped.truncated, totalCount: capped.totalCount }));
10787
11405
  return 0;
10788
11406
  }
10789
11407
  if (results.length === 0) {
10790
- emit2(`No test files found referencing symbols in '${opts.file}'`);
11408
+ emit2(`No test files found referencing symbols in '${displaySafeText(opts.file)}'`);
10791
11409
  return 0;
10792
11410
  }
10793
11411
  for (const r of results) {
@@ -10819,7 +11437,7 @@ function runCoverageGaps(opts) {
10819
11437
  emitErr2(`Showing top ${sliced.length} of ${gaps.length} coverage gaps (raise --top to see the rest).`);
10820
11438
  }
10821
11439
  if (opts.json === true) {
10822
- emit2(JSON.stringify(sliced, null, 2));
11440
+ emit2(displaySafeJson(sliced));
10823
11441
  return 0;
10824
11442
  }
10825
11443
  if (sliced.length === 0) {
@@ -10827,7 +11445,7 @@ function runCoverageGaps(opts) {
10827
11445
  if (isIndexEmptyForProject(globalDbPath(), rootDir)) emit2(emptyIndexMessage(rootDir));
10828
11446
  return 0;
10829
11447
  }
10830
- for (const g of sliced) emit2(`${g.name} ${g.kind} ${toDisplayPath(rootDir, g.file)}:${g.line}`);
11448
+ for (const g of sliced) emit2(`${displaySafeText(g.name)} ${displaySafeText(g.kind)} ${displaySafeText(toDisplayPath(rootDir, g.file))}:${g.line}`);
10831
11449
  return 0;
10832
11450
  }
10833
11451
  function runArch(opts) {
@@ -10852,13 +11470,13 @@ function runArch(opts) {
10852
11470
  const moduleResult = opts.modules === true ? detectModules({ files, graph, importedBy, resolve: resolve5 }, cwd, top) : null;
10853
11471
  if (opts.json === true) {
10854
11472
  const modulePayload = moduleResult === null ? {} : { modules: moduleResult.modules, modulesTotal: moduleResult.modulesTotal, modulesTruncated: moduleResult.modules.length < moduleResult.modulesTotal, modularity: moduleResult.modularity, isolatedCount: moduleResult.isolatedCount, crossImports: moduleResult.crossImports, crossImportsTotal: moduleResult.crossImportsTotal, crossImportsTruncated: moduleResult.crossImports.length < moduleResult.crossImportsTotal, noImportEdges: moduleResult.noEdges };
10855
- emit2(JSON.stringify({ hubs, hubsTotal, hubsTruncated: hubs.length < hubsTotal, entryPoints, entryPointsTotal, entryPointsTruncated: entryPoints.length < entryPointsTotal, cycles, ...cyclesTruncated ? { cyclesTruncated: true } : {}, ...modulePayload }, null, 2));
11473
+ emit2(displaySafeJson({ hubs, hubsTotal, hubsTruncated: hubs.length < hubsTotal, entryPoints, entryPointsTotal, entryPointsTruncated: entryPoints.length < entryPointsTotal, cycles, ...cyclesTruncated ? { cyclesTruncated: true } : {}, ...modulePayload }));
10856
11474
  return 0;
10857
11475
  }
10858
11476
  emit2(hubs.length < hubsTotal ? `hubs (top ${hubs.length} of ${hubsTotal} most-imported):` : `hubs (${hubsTotal} most-imported):`);
10859
- for (const h of hubs) emit2(` ${h.importedBy} importers ${toDisplayPath(getDisplayRoot(opts.cwd), h.file)}`);
11477
+ for (const h of hubs) emit2(` ${h.importedBy} importers ${displaySafeText(toDisplayPath(getDisplayRoot(opts.cwd), h.file))}`);
10860
11478
  emit2(entryPoints.length < entryPointsTotal ? `entry points (imported by nobody, top ${entryPoints.length} of ${entryPointsTotal}):` : `entry points (imported by nobody, ${entryPointsTotal} found):`);
10861
- for (const e of entryPoints) emit2(` ${toDisplayPath(getDisplayRoot(opts.cwd), e.file)}`);
11479
+ for (const e of entryPoints) emit2(` ${displaySafeText(toDisplayPath(getDisplayRoot(opts.cwd), e.file))}`);
10862
11480
  emit2(cyclesTruncated ? `cycles (first ${cycles.length}, truncated at the ${MAX_CYCLES}-cycle enumeration limit: more cycles exist):` : `cycles (${cycles.length} found):`);
10863
11481
  for (const c of cycles) emit2(` ${c.map((f) => toDisplayPath(getDisplayRoot(opts.cwd), f)).join(" -> ")}`);
10864
11482
  if (moduleResult !== null) for (const line of renderModules(moduleResult, top)) emit2(line);
@@ -10867,7 +11485,7 @@ function runArch(opts) {
10867
11485
  function runBlame(opts) {
10868
11486
  const sepIdx = opts.spec.lastIndexOf("::");
10869
11487
  if (sepIdx < 0) {
10870
- emitErr2(`Invalid spec - expected "file::symbol", got: ${opts.spec}`);
11488
+ emitErr2(`Invalid spec - expected "file::symbol", got: ${displaySafeText(opts.spec)}`);
10871
11489
  return 1;
10872
11490
  }
10873
11491
  const cwd = opts.cwd ?? process.cwd();
@@ -10880,7 +11498,7 @@ function runBlame(opts) {
10880
11498
  try {
10881
11499
  const result = runGit(["blame", "-L", `${start},${end}`, "--", filePath], { cwd });
10882
11500
  if (result.exitCode !== 0) {
10883
- emitErr2(`git blame failed: ${result.stderr}`);
11501
+ emitErr2(`git blame failed: ${displaySafeText(result.stderr)}`);
10884
11502
  return 1;
10885
11503
  }
10886
11504
  raw = result.stdout;
@@ -10894,11 +11512,11 @@ function runBlame(opts) {
10894
11512
  if (!m) return { raw: l };
10895
11513
  return { commit: m[2], boundary: m[1] === "^", author: (m[3] ?? "").trim(), date: (m[4] ?? "").trim(), line: Number.parseInt(m[5] ?? "0", 10), content: m[6] };
10896
11514
  });
10897
- emit2(JSON.stringify({ symbol: sym.name, file: filePath, lines }, null, 2));
11515
+ emit2(displaySafeJson({ symbol: sym.name, file: filePath, lines }));
10898
11516
  return 0;
10899
11517
  }
10900
- emit2(`${sym.name} ${toDisplayPath(getDisplayRoot(opts.cwd), filePath)}:${start}-${end}`);
10901
- emit2(raw.trim());
11518
+ emit2(`${displaySafeText(sym.name)} ${displaySafeText(toDisplayPath(getDisplayRoot(opts.cwd), filePath))}:${start}-${end}`);
11519
+ emit2(fenceUntrusted(raw.trim(), UNTRUSTED_FILE_TAG));
10902
11520
  return 0;
10903
11521
  }
10904
11522
  function runAsk(opts) {
@@ -10914,17 +11532,18 @@ function runAsk(opts) {
10914
11532
  if (moreBeyondTop) {
10915
11533
  emitErr2(`Answer is grounded in the top ${countNoun(top, "match", "matches")}; more matched (raise --top to widen the evidence).`);
10916
11534
  }
11535
+ warnIfFilesStale(hits.map((h) => h.filePath));
10917
11536
  const BACKEND_ENV = "TOKEN_GOAT_ASK_BACKEND";
10918
11537
  const backendLabel = process.env[BACKEND_ENV] ?? "";
10919
11538
  const entries = hits.map((h) => ({ file: h.filePath, symbol: h.name, kind: h.kind, line: h.lineStart, readCmd: `token-goat read "${h.filePath}::${h.name}@${h.lineStart}"` }));
10920
11539
  const degrade = (reason, extraNote) => {
10921
11540
  if (opts.json === true) {
10922
- emit2(JSON.stringify({ degraded: true, note: reason, ...extraNote !== void 0 ? { hint: extraNote } : {}, context: entries }, null, 2));
11541
+ emit2(displaySafeJson({ degraded: true, note: reason, ...extraNote !== void 0 ? { hint: extraNote } : {}, context: entries }));
10923
11542
  return 0;
10924
11543
  }
10925
11544
  emit2(`[degraded mode - ${reason}]`);
10926
11545
  if (extraNote !== void 0) emit2(extraNote);
10927
- for (const e of entries) emit2(`token-goat read "${toDisplayPath(rootDir, e.file)}::${e.symbol}@${e.line}"`);
11546
+ for (const e of entries) emit2(`token-goat read "${displaySafeText(toDisplayPath(rootDir, e.file))}::${displaySafeText(e.symbol)}@${e.line}"`);
10928
11547
  return 0;
10929
11548
  };
10930
11549
  if (!backendLabel) return degrade(`set ${BACKEND_ENV}=claude|codex for LLM synthesis`);
@@ -10971,7 +11590,7 @@ ANSWER:`;
10971
11590
  }
10972
11591
  if (answer) {
10973
11592
  if (opts.json === true) {
10974
- emit2(JSON.stringify({ answer, context: entries }, null, 2));
11593
+ emit2(displaySafeJson({ answer, context: entries }));
10975
11594
  } else {
10976
11595
  emit2(answer);
10977
11596
  }
@@ -11113,9 +11732,8 @@ function resolveHandoff(name, opts = {}) {
11113
11732
  }
11114
11733
 
11115
11734
  export {
11116
- extractSection,
11117
- readSection,
11118
- listSections,
11735
+ isAvailable2 as isAvailable,
11736
+ loadError,
11119
11737
  buildImportGraph,
11120
11738
  ALL_SYMBOLS_IN_FILE_LIMIT,
11121
11739
  enclosingSymbol,
@@ -11139,8 +11757,6 @@ export {
11139
11757
  symbolNamesInFile,
11140
11758
  computeFileFingerprint,
11141
11759
  upsertNote,
11142
- isAvailable2 as isAvailable,
11143
- loadError,
11144
11760
  AMBIGUOUS_HEADING_LIMIT,
11145
11761
  ConfinementIdentityError,
11146
11762
  ABSENT_PIN,
@@ -11149,6 +11765,7 @@ export {
11149
11765
  withPinnedReads,
11150
11766
  resolveBody,
11151
11767
  healStaleIndex,
11768
+ warnIfFilesStale,
11152
11769
  findSpecSeparator,
11153
11770
  rankSimilarNames,
11154
11771
  filterSimilarHeadings,
@@ -11169,6 +11786,9 @@ export {
11169
11786
  runYamlQuery,
11170
11787
  runXmlOutline,
11171
11788
  runXmlQuery,
11789
+ runHtmlOutline,
11790
+ runHtmlQuery,
11791
+ runHtmlLint,
11172
11792
  runOpenApiOutline,
11173
11793
  runOpenApiOp,
11174
11794
  runZipList,