token-goat 2.6.24 → 2.6.25

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.
@@ -49,7 +49,7 @@ var init_define_import_meta_env = __esm({
49
49
  import { createRequire } from "node:module";
50
50
  function resolveVersion() {
51
51
  if (true) {
52
- return "2.6.24";
52
+ return "2.6.25";
53
53
  }
54
54
  const require2 = createRequire(import.meta.url);
55
55
  const pkg = require2("../package.json");
@@ -1469,7 +1469,7 @@ function resolveIndexPath(file2, base = process.cwd()) {
1469
1469
  }
1470
1470
  function toDisplayPath(root, target) {
1471
1471
  if (root === void 0) return target;
1472
- const rel = path2.relative(root, target).replace(/\\/g, "/");
1472
+ const rel = path2.relative(normalizePath(root), normalizePath(target)).replace(/\\/g, "/");
1473
1473
  if (rel === "" || rel.startsWith("..") || path2.isAbsolute(rel)) {
1474
1474
  return rel === "" ? "." : target;
1475
1475
  }
@@ -1531,6 +1531,15 @@ function foldCase(s) {
1531
1531
  function runGit(args, opts = {}) {
1532
1532
  const subArgs = args[0] === "diff" ? [args[0], "--no-ext-diff", "--no-textconv", ...args.slice(1)] : args;
1533
1533
  const fullArgs = [
1534
+ // Never take an optional lock. Every git call here is on someone else's
1535
+ // working repo, and a `status` that refreshes the index writes
1536
+ // `.git/index.lock`; if this process is killed mid-call -- which the hint
1537
+ // paths deliberately invite, since they spawn under a short timeout -- the
1538
+ // orphaned lock blocks every subsequent commit in that repo until a human
1539
+ // deletes it. Observed doing exactly that on 2026-08-05. `--no-optional-
1540
+ // locks` suppresses only locks git considers optional, so write commands
1541
+ // that genuinely need one are unaffected.
1542
+ "--no-optional-locks",
1534
1543
  "-c",
1535
1544
  "core.fsmonitor=",
1536
1545
  "-c",
@@ -1854,6 +1863,28 @@ function writeIfDifferent(p, content, backup = false) {
1854
1863
  atomicWriteText(p, content);
1855
1864
  return true;
1856
1865
  }
1866
+ function buildContextWindow(absPath, line, contextLines) {
1867
+ if (!Number.isFinite(contextLines) || contextLines <= 0) return null;
1868
+ let text;
1869
+ try {
1870
+ text = readFileSync(absPath, "utf-8");
1871
+ } catch {
1872
+ return null;
1873
+ }
1874
+ const lines2 = text.split(/\r?\n/);
1875
+ const idx = line - 1;
1876
+ if (idx < 0 || idx >= lines2.length) return null;
1877
+ const start = Math.max(0, idx - contextLines);
1878
+ const end = Math.min(lines2.length - 1, idx + contextLines);
1879
+ const out2 = [];
1880
+ for (let i = start; i <= end; i++) out2.push({ line: i + 1, text: lines2[i] ?? "" });
1881
+ return out2;
1882
+ }
1883
+ function renderContextWindow(displayFile, matchLine3, window, matchSuffix = "", indent = "") {
1884
+ return window.map(
1885
+ (c) => c.line === matchLine3 ? `${indent}${displayFile}:${c.line}: ${c.text}${matchSuffix}` : `${indent}${displayFile}-${c.line}- ${c.text}`
1886
+ );
1887
+ }
1857
1888
  function toKB(bytes) {
1858
1889
  return Math.round(bytes / 1024);
1859
1890
  }
@@ -3724,7 +3755,8 @@ function defaultConfig() {
3724
3755
  compression: getDefaultConfig("compression"),
3725
3756
  context: getDefaultConfig("context"),
3726
3757
  injection: getDefaultConfig("injection"),
3727
- hint_stats: getDefaultConfig("hint_stats")
3758
+ hint_stats: getDefaultConfig("hint_stats"),
3759
+ semantic: getDefaultConfig("semantic")
3728
3760
  };
3729
3761
  }
3730
3762
  function validatedBool(raw, def) {
@@ -4166,6 +4198,10 @@ function _buildConfig(raw, projectRaw = {}) {
4166
4198
  const hs = getDefaultConfig("hint_stats");
4167
4199
  hs.suppress_threshold_pct = validatedInt(hs_raw["suppress_threshold_pct"], hs.suppress_threshold_pct, ...boundsOf("hint_stats.suppress_threshold_pct"));
4168
4200
  hs.min_sample_size = validatedInt(hs_raw["min_sample_size"], hs.min_sample_size, ...boundsOf("hint_stats.min_sample_size"));
4201
+ const sem_raw = section(raw, "semantic");
4202
+ const sem = getDefaultConfig("semantic");
4203
+ sem.archive_weight = validatedFloat(sem_raw["archive_weight"], sem.archive_weight, ...boundsOf("semantic.archive_weight"));
4204
+ sem.docs_weight = validatedFloat(sem_raw["docs_weight"], sem.docs_weight, ...boundsOf("semantic.docs_weight"));
4169
4205
  return {
4170
4206
  compact_assist: ca,
4171
4207
  bash_compress: bc,
@@ -4188,7 +4224,8 @@ function _buildConfig(raw, projectRaw = {}) {
4188
4224
  compression: cpr,
4189
4225
  context: ctx,
4190
4226
  injection: inj,
4191
- hint_stats: hs
4227
+ hint_stats: hs,
4228
+ semantic: sem
4192
4229
  };
4193
4230
  }
4194
4231
  function saveConfig(config2) {
@@ -4345,6 +4382,10 @@ function saveConfig(config2) {
4345
4382
  hint_stats: {
4346
4383
  suppress_threshold_pct: config2.hint_stats.suppress_threshold_pct,
4347
4384
  min_sample_size: config2.hint_stats.min_sample_size
4385
+ },
4386
+ semantic: {
4387
+ archive_weight: config2.semantic.archive_weight,
4388
+ docs_weight: config2.semantic.docs_weight
4348
4389
  }
4349
4390
  };
4350
4391
  const toml = stringify(data);
@@ -4537,6 +4578,10 @@ var init_config = __esm({
4537
4578
  hint_stats: {
4538
4579
  suppress_threshold_pct: 15,
4539
4580
  min_sample_size: 5
4581
+ },
4582
+ semantic: {
4583
+ archive_weight: 0.7,
4584
+ docs_weight: 0.92
4540
4585
  }
4541
4586
  };
4542
4587
  NUMERIC_FIELD_BOUNDS = {
@@ -4600,7 +4645,9 @@ var init_config = __esm({
4600
4645
  "indexing.large_file_skip_kb": { min: 1, max: 1048576 },
4601
4646
  "context.model_window_tokens": { min: 1e4, max: 1e7 },
4602
4647
  "hint_stats.suppress_threshold_pct": { min: 0, max: 100 },
4603
- "hint_stats.min_sample_size": { min: 1, max: 1e4 }
4648
+ "hint_stats.min_sample_size": { min: 1, max: 1e4 },
4649
+ "semantic.archive_weight": { min: 0.05, max: 1 },
4650
+ "semantic.docs_weight": { min: 0.05, max: 1 }
4604
4651
  };
4605
4652
  ENUM_FIELD_VALUES = {
4606
4653
  "compression.profile": ["auto", "aggressive", "balanced", "minimal"],
@@ -13971,7 +14018,8 @@ function rerankHits(hits, query, topK) {
13971
14018
  boost = Math.min(matches2 * _VERBATIM_TOKEN_BOOST, _MAX_VERBATIM_BOOST);
13972
14019
  }
13973
14020
  const penalty = _isGeneratedPath(hit.filePath) ? _GENERATED_PATH_PENALTY : 0;
13974
- return { hit, index, adjusted: hit.distance - boost + penalty };
14021
+ const pathPenalty = _pathPriorityPenalty(hit.filePath);
14022
+ return { hit, index, adjusted: hit.distance - boost + penalty + pathPenalty };
13975
14023
  });
13976
14024
  scored.sort((a, b) => a.adjusted - b.adjusted || a.index - b.index);
13977
14025
  return scored.slice(0, topK).map((entry) => ({ ...entry.hit, adjustedDistance: entry.adjusted }));
@@ -14097,11 +14145,26 @@ function _isGeneratedPath(filePath) {
14097
14145
  }
14098
14146
  return false;
14099
14147
  }
14100
- var _require3, _transformer, _transformerError, _transformerLoadAttempted, DEFAULT_MODEL, DEFAULT_DIM, QUERY_INSTRUCTION_PREFIX, _extractorCache, _pipelineFnOverride, PIPELINE_RETRY_ATTEMPTS, PIPELINE_RETRY_DELAY_MS, DEFAULT_PIPELINE_RETRY_DELAY_MS, MIN_CHUNK_CHARS, MAX_CHUNK_CHARS, DEFAULT_DISTANCE_THRESHOLD, _GENERATED_PATH_SEGMENTS, _GENERATED_PATH_PENALTY, _VERBATIM_TOKEN_BOOST, _MAX_VERBATIM_BOOST, _TOKEN_RE, _MIN_TOKEN_LEN, OVER_FETCH_FACTOR, MAX_OVER_FETCH, BACKFILL_MULTIPLIER, _chunkVectorsUsable;
14148
+ function _pathPriorityPenalty(filePath) {
14149
+ const segments = filePath.split(/[/\\]+/);
14150
+ const basename22 = segments[segments.length - 1] ?? filePath;
14151
+ const weights = loadConfig().semantic;
14152
+ const isArchive = _ARCHIVE_FILE_RE.test(basename22) || segments.some((seg) => _ARCHIVE_PATH_SEGMENTS.has(seg.toLowerCase()));
14153
+ if (isArchive) {
14154
+ return 1 - weights.archive_weight;
14155
+ }
14156
+ const isDocs = _DOCS_FILE_RE.test(basename22) || segments.some((seg) => seg.toLowerCase() === _DOCS_DIR_SEGMENT);
14157
+ if (isDocs) {
14158
+ return 1 - weights.docs_weight;
14159
+ }
14160
+ return 0;
14161
+ }
14162
+ var _require3, _transformer, _transformerError, _transformerLoadAttempted, DEFAULT_MODEL, DEFAULT_DIM, QUERY_INSTRUCTION_PREFIX, _extractorCache, _pipelineFnOverride, PIPELINE_RETRY_ATTEMPTS, PIPELINE_RETRY_DELAY_MS, DEFAULT_PIPELINE_RETRY_DELAY_MS, MIN_CHUNK_CHARS, MAX_CHUNK_CHARS, DEFAULT_DISTANCE_THRESHOLD, _GENERATED_PATH_SEGMENTS, _GENERATED_PATH_PENALTY, _ARCHIVE_PATH_SEGMENTS, _ARCHIVE_FILE_RE, _DOCS_FILE_RE, _DOCS_DIR_SEGMENT, _VERBATIM_TOKEN_BOOST, _MAX_VERBATIM_BOOST, _TOKEN_RE, _MIN_TOKEN_LEN, OVER_FETCH_FACTOR, MAX_OVER_FETCH, BACKFILL_MULTIPLIER, _chunkVectorsUsable;
14101
14163
  var init_embeddings = __esm({
14102
14164
  "src/embeddings.ts"() {
14103
14165
  "use strict";
14104
14166
  init_define_import_meta_env();
14167
+ init_config();
14105
14168
  init_sql_path();
14106
14169
  init_util2();
14107
14170
  init_reset();
@@ -14150,6 +14213,17 @@ var init_embeddings = __esm({
14150
14213
  ".ruff_cache"
14151
14214
  ]);
14152
14215
  _GENERATED_PATH_PENALTY = 0.5;
14216
+ _ARCHIVE_PATH_SEGMENTS = /* @__PURE__ */ new Set([
14217
+ "archive",
14218
+ "archived",
14219
+ "old",
14220
+ "deprecated",
14221
+ "plans",
14222
+ "drafts"
14223
+ ]);
14224
+ _ARCHIVE_FILE_RE = /(^changelog|\.bak$|\.orig$)/i;
14225
+ _DOCS_FILE_RE = /\.md$/i;
14226
+ _DOCS_DIR_SEGMENT = "docs";
14153
14227
  _VERBATIM_TOKEN_BOOST = 0.05;
14154
14228
  _MAX_VERBATIM_BOOST = 0.25;
14155
14229
  _TOKEN_RE = /\w+/g;
@@ -24529,14 +24603,32 @@ function fetchTopSymbols(limit, dbPath, rootDir) {
24529
24603
  try {
24530
24604
  const db = getDb(dbPath);
24531
24605
  const { clause, param } = projectScopeClause("file_path");
24606
+ const refScope = projectScopeClause("file_path");
24532
24607
  const rows = db.prepare(
24608
+ // refs carry only a bare name, so a name defined N times cannot claim all N copies' references: divide by the number of same-named definitions, and keep one representative per name so a generic helper like `apply` occupies one slot instead of seven.
24533
24609
  `SELECT file_path, name, kind, line_start, line_end, body, docstring, parent
24534
- FROM symbols
24535
- WHERE kind IN ('class', 'function', 'interface') AND ${clause}
24536
- ORDER BY CASE kind WHEN 'class' THEN 0 WHEN 'interface' THEN 1 ELSE 2 END,
24610
+ FROM (
24611
+ SELECT s.file_path, s.name, s.kind, s.line_start, s.line_end, s.body, s.docstring, s.parent,
24612
+ COALESCE(r.ref_count, 0) * 1.0 / COUNT(*) OVER (PARTITION BY s.name) AS score,
24613
+ ROW_NUMBER() OVER (
24614
+ PARTITION BY s.name
24615
+ ORDER BY LENGTH(COALESCE(s.body, '')) DESC, s.file_path
24616
+ ) AS rn
24617
+ FROM symbols s
24618
+ LEFT JOIN (
24619
+ SELECT name, COUNT(*) AS ref_count
24620
+ FROM refs
24621
+ WHERE ${refScope.clause}
24622
+ GROUP BY name
24623
+ ) r ON r.name = s.name
24624
+ WHERE s.kind IN ('class', 'function', 'interface') AND ${clause}
24625
+ )
24626
+ WHERE rn = 1
24627
+ ORDER BY score DESC,
24628
+ CASE kind WHEN 'class' THEN 0 WHEN 'interface' THEN 1 ELSE 2 END,
24537
24629
  LENGTH(COALESCE(body, '')) DESC
24538
24630
  LIMIT ?`
24539
- ).all(param(rootDir), limit);
24631
+ ).all(refScope.param(rootDir), param(rootDir), limit);
24540
24632
  return rows.map((r) => ({
24541
24633
  filePath: r.file_path,
24542
24634
  name: r.name,
@@ -24588,13 +24680,12 @@ function formatProjectMap(map3, compact = false) {
24588
24680
  lines2.push("");
24589
24681
  lines2.push("## Top symbols");
24590
24682
  for (const s of map3.topSymbols) {
24591
- if (compact) {
24592
- lines2.push(`- ${s.name} (${s.kind})`);
24593
- } else {
24594
- const loc = `${path34.basename(s.filePath)}:${s.lineStart}-${s.lineEnd}`;
24595
- lines2.push(`- ${s.name} (${s.kind}) \u2014 ${loc}`);
24596
- }
24683
+ const loc = `${toDisplayPath(map3.rootDir, s.filePath)}:${s.lineStart}-${s.lineEnd}`;
24684
+ lines2.push(`- ${s.name} (${s.kind}) \u2014 ${loc}`);
24597
24685
  }
24686
+ } else {
24687
+ lines2.push("");
24688
+ lines2.push("## Top symbols: none \u2014 no files indexed for this project; run 'token-goat index .'");
24598
24689
  }
24599
24690
  if (!compact && map3.recentFiles.length > 0) {
24600
24691
  lines2.push("");
@@ -25102,6 +25193,11 @@ function isDeadSymbol(name2, refCount) {
25102
25193
  if (ENTRY_NAMES.has(name2)) return false;
25103
25194
  return refCount === 0;
25104
25195
  }
25196
+ function parseGraphSymbolSpec(spec) {
25197
+ const colonIdx = findSpecSeparator(spec);
25198
+ if (colonIdx === -1) return { name: spec };
25199
+ return { name: spec.slice(colonIdx + 2), file: spec.slice(0, colonIdx) };
25200
+ }
25105
25201
  function buildFileSymCache() {
25106
25202
  const cache = /* @__PURE__ */ new Map();
25107
25203
  return (fp) => {
@@ -25119,9 +25215,10 @@ function fileDefinesName(fp, name2, getSyms) {
25119
25215
  function filterRefsForSymbol(refs, name2, filePath, getSyms) {
25120
25216
  return refs.filter((ref2) => ref2.filePath === filePath || !fileDefinesName(ref2.filePath, name2, getSyms));
25121
25217
  }
25122
- function resolveCallers(name2, limit, filePath, rootDir) {
25218
+ function resolveCallers(name2, limit, filePath, rootDir, excludeTests) {
25123
25219
  const resolvedRootDir = rootDir ?? resolveProjectRoot({ project: process.cwd() });
25124
- const refs = queryRefs({ name: name2, limit: limit ?? 500, rootDir: resolvedRootDir });
25220
+ const queryLimit = excludeTests === true ? UNBOUNDED_REF_LIMIT : limit ?? 500;
25221
+ const refs = queryRefs({ name: name2, limit: queryLimit, rootDir: resolvedRootDir });
25125
25222
  const getSyms = buildFileSymCache();
25126
25223
  const scoped = filePath === void 0 ? refs : filterRefsForSymbol(refs, name2, filePath, getSyms);
25127
25224
  return scoped.map((ref2) => {
@@ -25140,17 +25237,37 @@ function runCallers(opts) {
25140
25237
  return 1;
25141
25238
  }
25142
25239
  const rootDir = resolveProjectRoot({ project: process.cwd() });
25143
- const entries = resolveCallers(opts.symbol, opts.limit, void 0, rootDir);
25240
+ const { name: name2, file: file2 } = parseGraphSymbolSpec(opts.symbol);
25241
+ const fileHint = file2 !== void 0 ? resolveIndexPath(file2, rootDir) : void 0;
25242
+ if (fileHint !== void 0 && querySymbols({ name: name2, filePath: fileHint, limit: 1 }).length === 0) {
25243
+ emitErr(`Symbol '${name2}' not found in '${file2}'`);
25244
+ return 1;
25245
+ }
25246
+ const resolved = resolveCallers(name2, opts.limit, fileHint, rootDir, opts.excludeTests);
25247
+ const suppressed = opts.excludeTests === true ? resolved.filter((e) => isTestFile(e.file)).length : 0;
25248
+ const entries = opts.excludeTests === true ? resolved.filter((e) => !isTestFile(e.file)).slice(0, opts.limit ?? 500) : resolved;
25144
25249
  if (entries.length === 0) {
25250
+ if (opts.excludeTests === true && suppressed > 0) {
25251
+ emitErr(`No non-test references found for '${opts.symbol}' (${suppressed} in test files hidden by --exclude-tests)`);
25252
+ return 1;
25253
+ }
25145
25254
  emitErr(`No references found for '${opts.symbol}'`);
25146
25255
  return 1;
25147
25256
  }
25257
+ const contextLines = opts.context ?? 0;
25148
25258
  if (opts.json === true) {
25149
- emit2(JSON.stringify(entries, null, 2));
25259
+ const payload = contextLines > 0 ? entries.map((e) => ({ ...e, contextLines: buildContextWindow(e.file, e.line, contextLines) ?? [] })) : entries;
25260
+ emit2(JSON.stringify(payload, null, 2));
25150
25261
  return 0;
25151
25262
  }
25263
+ if (opts.excludeTests === true && suppressed > 0) {
25264
+ emit2(`${entries.length} callers found (${suppressed} in test files hidden by --exclude-tests)`);
25265
+ }
25152
25266
  for (const e of entries) {
25153
- emit2(`${e.caller} ${toDisplayPath(rootDir, e.file)}:${e.line}`);
25267
+ const displayPath = toDisplayPath(rootDir, e.file);
25268
+ emit2(`${e.caller} ${displayPath}:${e.line}`);
25269
+ const window = buildContextWindow(e.file, e.line, contextLines);
25270
+ if (window !== null) for (const l of renderContextWindow(displayPath, e.line, window, "", " ")) emit2(l);
25154
25271
  }
25155
25272
  return 0;
25156
25273
  }
@@ -25161,28 +25278,36 @@ function runCallChain(opts) {
25161
25278
  }
25162
25279
  const maxDepth = opts.depth ?? 8;
25163
25280
  const rootDir = resolveProjectRoot({ project: process.cwd() });
25164
- if (querySymbols({ name: opts.symbol, rootDir, limit: 1 }).length === 0) {
25281
+ const { name: name2, file: file2 } = parseGraphSymbolSpec(opts.symbol);
25282
+ const fileHint = file2 !== void 0 ? resolveIndexPath(file2, rootDir) : void 0;
25283
+ if (fileHint !== void 0) {
25284
+ if (querySymbols({ name: name2, filePath: fileHint, limit: 1 }).length === 0) {
25285
+ emitErr(`Symbol '${name2}' not found in '${file2}'`);
25286
+ return 1;
25287
+ }
25288
+ } else if (querySymbols({ name: name2, rootDir, limit: 1 }).length === 0) {
25165
25289
  emitErr(`Symbol not found: ${opts.symbol}`);
25166
25290
  return 1;
25167
25291
  }
25168
25292
  const getSyms = buildFileSymCache();
25169
- const callersOf = (name2) => {
25170
- const refs = queryRefs({ name: name2, limit: DEFAULT_REF_QUERY_LIMIT, rootDir });
25293
+ const callersOf = (n) => {
25294
+ const refs = queryRefs({ name: n, limit: DEFAULT_REF_QUERY_LIMIT, rootDir });
25171
25295
  if (refs.length === 0) return [];
25296
+ const scoped = fileHint !== void 0 && n === name2 ? filterRefsForSymbol(refs, n, fileHint, getSyms) : refs;
25172
25297
  const names = /* @__PURE__ */ new Set();
25173
- for (const ref2 of refs) {
25298
+ for (const ref2 of scoped) {
25174
25299
  const enc = enclosingSymbol(getSyms(ref2.filePath), ref2.line);
25175
25300
  if (enc !== null) names.add(enc.name);
25176
25301
  }
25177
25302
  return [...names];
25178
25303
  };
25179
- const chains = bfsCallChains(opts.symbol, callersOf, maxDepth);
25304
+ const chains = bfsCallChains(name2, callersOf, maxDepth);
25180
25305
  if (opts.json === true) {
25181
25306
  emit2(JSON.stringify({ chains }, null, 2));
25182
25307
  return 0;
25183
25308
  }
25184
- if (chains.length === 1 && chains[0]?.length === 1 && chains[0][0] === opts.symbol) {
25185
- emit2(`${opts.symbol} (no callers)`);
25309
+ if (chains.length === 1 && chains[0]?.length === 1 && chains[0][0] === name2) {
25310
+ emit2(`${name2} (no callers)`);
25186
25311
  return 0;
25187
25312
  }
25188
25313
  for (const chain2 of chains) {
@@ -25202,16 +25327,23 @@ function runImpact(opts) {
25202
25327
  const top = opts.top ?? 20;
25203
25328
  const DEPTH_CAP = 8;
25204
25329
  const rootDir = resolveProjectRoot({ project: process.cwd() });
25330
+ const { name: rootName, file: file2 } = parseGraphSymbolSpec(opts.symbol);
25331
+ const fileHint = file2 !== void 0 ? resolveIndexPath(file2, rootDir) : void 0;
25332
+ if (fileHint !== void 0 && querySymbols({ name: rootName, filePath: fileHint, limit: 1 }).length === 0) {
25333
+ emitErr(`Symbol '${rootName}' not found in '${file2}'`);
25334
+ return 1;
25335
+ }
25205
25336
  const getSyms = buildFileSymCache();
25206
- const hops = /* @__PURE__ */ new Map([[opts.symbol, 0]]);
25207
- const queue = [[opts.symbol, 0]];
25337
+ const hops = /* @__PURE__ */ new Map([[rootName, 0]]);
25338
+ const queue = [[rootName, 0]];
25208
25339
  while (queue.length > 0) {
25209
25340
  const item = queue.shift();
25210
25341
  if (item === void 0) break;
25211
25342
  const [name2, depth] = item;
25212
25343
  if (depth >= DEPTH_CAP) continue;
25213
25344
  const refs = queryRefs({ name: name2, limit: DEFAULT_REF_QUERY_LIMIT, rootDir });
25214
- for (const ref2 of refs) {
25345
+ const scoped = fileHint !== void 0 && name2 === rootName ? filterRefsForSymbol(refs, name2, fileHint, getSyms) : refs;
25346
+ for (const ref2 of scoped) {
25215
25347
  const newHop = depth + 1;
25216
25348
  const enc = enclosingSymbol(getSyms(ref2.filePath), ref2.line);
25217
25349
  if (enc === null) {
@@ -25228,7 +25360,7 @@ function runImpact(opts) {
25228
25360
  }
25229
25361
  }
25230
25362
  }
25231
- hops.delete(opts.symbol);
25363
+ hops.delete(rootName);
25232
25364
  const sorted = [...hops.entries()].sort(compareHopEntries).slice(0, top);
25233
25365
  if (sorted.length === 0) {
25234
25366
  emitErr(`No callers found for '${opts.symbol}'`);
@@ -25280,6 +25412,7 @@ function runDead(opts) {
25280
25412
  const syms = querySymbols({ kind, limit: 5e3, rootDir });
25281
25413
  const getSyms = buildFileSymCache();
25282
25414
  const results = [];
25415
+ let suppressed = 0;
25283
25416
  for (const sym of syms) {
25284
25417
  if (opts.includePrivate !== true && sym.name.startsWith("_")) continue;
25285
25418
  const refs = queryRefs({ name: sym.name, limit: DEFAULT_REF_QUERY_LIMIT, rootDir });
@@ -25289,6 +25422,10 @@ function runDead(opts) {
25289
25422
  const ownScope = enclosingNamedScope(getSyms(sym.filePath), sym.lineStart);
25290
25423
  if (ownScope !== null && hasAncestorDispatchRef(sym.name, ownScope.name, sym.filePath, rootDir)) continue;
25291
25424
  }
25425
+ if (opts.excludeTests === true && isTestFile(sym.filePath)) {
25426
+ suppressed += 1;
25427
+ continue;
25428
+ }
25292
25429
  results.push({ name: sym.name, kind: sym.kind, file: sym.filePath, line: sym.lineStart });
25293
25430
  }
25294
25431
  const sliced = results.slice(0, opts.top ?? results.length);
@@ -25297,9 +25434,16 @@ function runDead(opts) {
25297
25434
  return 0;
25298
25435
  }
25299
25436
  if (sliced.length === 0) {
25300
- emit2("No dead symbols found.");
25437
+ if (opts.excludeTests === true && suppressed > 0) {
25438
+ emit2(`No dead symbols found (${suppressed} in test files hidden by --exclude-tests).`);
25439
+ } else {
25440
+ emit2("No dead symbols found.");
25441
+ }
25301
25442
  return 0;
25302
25443
  }
25444
+ if (opts.excludeTests === true && suppressed > 0) {
25445
+ emit2(`${sliced.length} dead symbols (${suppressed} in test files hidden by --exclude-tests)`);
25446
+ }
25303
25447
  for (const r of sliced) {
25304
25448
  emit2(`${r.name} ${toDisplayPath(rootDir, r.file)}:${r.line}`);
25305
25449
  }
@@ -25549,16 +25693,9 @@ function runSimilar(opts) {
25549
25693
  emitErr(`Invalid spec - expected "file::symbol", got: ${opts.spec}`);
25550
25694
  return 1;
25551
25695
  }
25552
- const fileArg = opts.spec.slice(0, sepIdx);
25553
- const symbolArg = opts.spec.slice(sepIdx + 2);
25554
25696
  const top = opts.top ?? 10;
25555
- const filePath = resolveIndexPath(fileArg);
25556
- const anchors = querySymbols({ name: symbolArg, filePath });
25557
- if (anchors.length === 0) {
25558
- emitErr(`Symbol '${symbolArg}' not found in '${fileArg}'`);
25559
- return 1;
25560
- }
25561
- const anchor = anchors[0];
25697
+ const anchor = resolveSymbolSpecOrEmitError("similar", opts.spec, void 0);
25698
+ if (anchor === null) return 1;
25562
25699
  const words = [anchor.name, ...(anchor.docstring ?? "").split(/\s+/).filter((w) => w.length > 4)];
25563
25700
  const query = words.slice(0, 8).join(" ");
25564
25701
  const rootDir = resolveProjectRoot({ project: process.cwd() });
@@ -25590,13 +25727,13 @@ function runContextFor(opts) {
25590
25727
  const bodyTokens = estimateTokens(h.body ?? "");
25591
25728
  if (budget !== void 0 && tokensSoFar + bodyTokens > budget) continue;
25592
25729
  tokensSoFar += bodyTokens;
25593
- entries.push({ file: h.filePath, symbol: h.name, kind: h.kind, readCmd: `token-goat read "${h.filePath}::${h.name}"` });
25730
+ entries.push({ file: h.filePath, symbol: h.name, kind: h.kind, line: h.lineStart, readCmd: `token-goat read "${h.filePath}::${h.name}@${h.lineStart}"` });
25594
25731
  }
25595
25732
  if (opts.json === true) {
25596
25733
  emit2(JSON.stringify(entries, null, 2));
25597
25734
  return 0;
25598
25735
  }
25599
- for (const e of entries) emit2(`token-goat read "${toDisplayPath(rootDir, e.file)}::${e.symbol}"`);
25736
+ for (const e of entries) emit2(`token-goat read "${toDisplayPath(rootDir, e.file)}::${e.symbol}@${e.line}"`);
25600
25737
  return 0;
25601
25738
  }
25602
25739
  function runTestFor(opts) {
@@ -25741,16 +25878,10 @@ function runBlame(opts) {
25741
25878
  emitErr(`Invalid spec - expected "file::symbol", got: ${opts.spec}`);
25742
25879
  return 1;
25743
25880
  }
25744
- const fileArg = opts.spec.slice(0, sepIdx);
25745
- const symbolArg = opts.spec.slice(sepIdx + 2);
25746
25881
  const cwd = opts.cwd ?? process.cwd();
25747
- const filePath = resolveIndexPath(fileArg);
25748
- const syms = querySymbols({ name: symbolArg, filePath });
25749
- if (syms.length === 0) {
25750
- emitErr(`Symbol '${symbolArg}' not found in '${fileArg}'`);
25751
- return 1;
25752
- }
25753
- const sym = syms[0];
25882
+ const sym = resolveSymbolSpecOrEmitError("blame", opts.spec, void 0);
25883
+ if (sym === null) return 1;
25884
+ const filePath = sym.filePath;
25754
25885
  const start = sym.lineStart;
25755
25886
  const end = sym.lineEnd;
25756
25887
  let raw;
@@ -25771,10 +25902,10 @@ function runBlame(opts) {
25771
25902
  if (!m) return { raw: l };
25772
25903
  return { commit: m[1], author: (m[2] ?? "").trim(), date: (m[3] ?? "").trim(), line: Number.parseInt(m[4] ?? "0", 10), content: m[5] };
25773
25904
  });
25774
- emit2(JSON.stringify({ symbol: symbolArg, file: filePath, lines: lines2 }, null, 2));
25905
+ emit2(JSON.stringify({ symbol: sym.name, file: filePath, lines: lines2 }, null, 2));
25775
25906
  return 0;
25776
25907
  }
25777
- emit2(`${symbolArg} ${toDisplayPath(getDisplayRoot(opts.cwd), filePath)}:${start}-${end}`);
25908
+ emit2(`${sym.name} ${toDisplayPath(getDisplayRoot(opts.cwd), filePath)}:${start}-${end}`);
25778
25909
  emit2(raw.trim());
25779
25910
  return 0;
25780
25911
  }
@@ -25788,14 +25919,14 @@ function runAsk(opts) {
25788
25919
  const hits = searchSymbolsFts(opts.question, top, void 0, rootDir);
25789
25920
  const BACKEND_ENV = "TOKEN_GOAT_ASK_BACKEND";
25790
25921
  const backendLabel = process.env[BACKEND_ENV] ?? "";
25791
- const entries = hits.map((h) => ({ file: h.filePath, symbol: h.name, kind: h.kind, readCmd: `token-goat read "${h.filePath}::${h.name}"` }));
25922
+ 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}"` }));
25792
25923
  const degrade = () => {
25793
25924
  if (opts.json === true) {
25794
25925
  emit2(JSON.stringify({ degraded: true, note: `Set ${BACKEND_ENV}=claude|codex for LLM synthesis`, context: entries }, null, 2));
25795
25926
  return 0;
25796
25927
  }
25797
25928
  emit2(`[degraded mode - set ${BACKEND_ENV}=claude|codex for LLM synthesis]`);
25798
- for (const e of entries) emit2(`token-goat read "${toDisplayPath(rootDir, e.file)}::${e.symbol}"`);
25929
+ for (const e of entries) emit2(`token-goat read "${toDisplayPath(rootDir, e.file)}::${e.symbol}@${e.line}"`);
25799
25930
  return 0;
25800
25931
  };
25801
25932
  if (!backendLabel) return degrade();
@@ -29963,6 +30094,42 @@ function formatBareNameSpecError(command, name2, projectRoot) {
29963
30094
  }
29964
30095
  return lines2.join("\n");
29965
30096
  }
30097
+ function formatCrossFileLead(command, name2, excludeFilePath, projectRoot) {
30098
+ const rootDir = projectRoot ?? process.cwd();
30099
+ const matches2 = querySymbols({ name: name2, limit: 50, rootDir });
30100
+ const excludeResolved = resolveIndexPath(excludeFilePath, rootDir);
30101
+ const seen = /* @__PURE__ */ new Set();
30102
+ const specs = [];
30103
+ for (const m of matches2) {
30104
+ if (foldPath(m.filePath) === foldPath(excludeResolved)) continue;
30105
+ const spec = `${toDisplayPath(rootDir, m.filePath)}::${m.name}`;
30106
+ if (seen.has(spec)) continue;
30107
+ seen.add(spec);
30108
+ specs.push(spec);
30109
+ }
30110
+ if (specs.length === 0) return "";
30111
+ const firstSpec = specs[0];
30112
+ const lines2 = [`'${name2}' is defined in ${firstSpec !== void 0 ? firstSpec.split("::")[0] : ""}`];
30113
+ for (const spec of specs.slice(0, DIDYOUMEAN_LIMIT)) {
30114
+ lines2.push(` - token-goat ${command} "${spec}"`);
30115
+ }
30116
+ if (specs.length > DIDYOUMEAN_LIMIT) {
30117
+ lines2.push(` (${specs.length - DIDYOUMEAN_LIMIT} more not shown)`);
30118
+ }
30119
+ return lines2.join("\n");
30120
+ }
30121
+ function resolveEnclosingSymbol(filePath, chunkStartLine) {
30122
+ const symbols = querySymbols({ filePath, limit: 1e5 }, globalDbPath());
30123
+ let best = null;
30124
+ for (const s of symbols) {
30125
+ if (s.lineStart <= chunkStartLine && chunkStartLine <= s.lineEnd) {
30126
+ if (best === null || s.lineEnd - s.lineStart < best.lineEnd - best.lineStart) {
30127
+ best = s;
30128
+ }
30129
+ }
30130
+ }
30131
+ return best === null ? null : { name: best.name, kind: best.kind };
30132
+ }
29966
30133
  function trimBlankLines(lines2) {
29967
30134
  let start = 0;
29968
30135
  let end = lines2.length;
@@ -30058,10 +30225,21 @@ function parseCrossFileMultiSpec(spec) {
30058
30225
  }
30059
30226
  return pairs.length > 1 ? pairs : null;
30060
30227
  }
30228
+ function parseMultiFileSpec(spec) {
30229
+ if (!spec.includes(",")) return null;
30230
+ if (fileExists(spec)) return null;
30231
+ if (findSpecSeparator(spec) !== -1) return null;
30232
+ const parts = spec.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
30233
+ return parts.length > 1 ? parts : null;
30234
+ }
30235
+ function extraFileArgsNote(command, first2, extras) {
30236
+ return `Note: ${extras.length} extra file argument(s) ignored (${extras.join(", ")}). ${command} reads one file, or a comma-separated list: token-goat ${command} "${[first2, ...extras].join(",")}"`;
30237
+ }
30061
30238
  function parseLineRange(spec) {
30062
30239
  const m = /^(.+)@(\d+)(?:-(\d+))?$/.exec(spec);
30063
30240
  if (m === null) return null;
30064
30241
  if (fileExists(spec)) return null;
30242
+ if (m[1].includes("::")) return null;
30065
30243
  const start = parseInt(m[2], 10);
30066
30244
  const end = m[3] !== void 0 ? parseInt(m[3], 10) : start;
30067
30245
  return { file: m[1], start, end };
@@ -30120,30 +30298,49 @@ function findParentName(entry, fileSymbols) {
30120
30298
  if (doc !== "" && PARENT_IDENTIFIER_RE.test(doc)) return doc;
30121
30299
  return null;
30122
30300
  }
30123
- function formatAmbiguity(symbol3, file2, candidates, explicitRoot) {
30301
+ function formatAmbiguity(symbol3, file2, candidates, explicitRoot, commandName = "read") {
30124
30302
  const multiFile = new Set(candidates.map((c) => c.filePath)).size > 1;
30125
30303
  const displayRoot = getDisplayRoot(explicitRoot);
30126
30304
  const lines2 = [
30127
30305
  `Ambiguous symbol '${symbol3}' in '${file2}': ${candidates.length} definitions match. Retry with one of the qualified commands below to pick one:`
30128
30306
  ];
30129
30307
  const fileSymCache = /* @__PURE__ */ new Map();
30130
- for (const c of candidates) {
30131
- let fileSyms = fileSymCache.get(c.filePath);
30308
+ const getFileSyms = (filePath) => {
30309
+ let fileSyms = fileSymCache.get(filePath);
30132
30310
  if (fileSyms === void 0) {
30133
- fileSyms = querySymbols({ filePath: c.filePath, limit: 1e3 });
30134
- fileSymCache.set(c.filePath, fileSyms);
30311
+ fileSyms = querySymbols({ filePath, limit: 1e3 });
30312
+ fileSymCache.set(filePath, fileSyms);
30135
30313
  }
30136
- const parent = findParentName(c, fileSyms);
30137
- const qualifier = parent !== null ? `${parent}.${symbol3}` : symbol3;
30314
+ return fileSyms;
30315
+ };
30316
+ const parents = candidates.map((c) => findParentName(c, getFileSyms(c.filePath)));
30317
+ const plainQualifiers = candidates.map((c, i) => parents[i] !== null ? `${parents[i]}.${symbol3}` : symbol3);
30318
+ const qualifierCounts = /* @__PURE__ */ new Map();
30319
+ const fileGroupSize = /* @__PURE__ */ new Map();
30320
+ for (let i = 0; i < candidates.length; i++) {
30321
+ const c = candidates[i];
30322
+ const key = `${c.filePath} ${plainQualifiers[i]}`;
30323
+ qualifierCounts.set(key, (qualifierCounts.get(key) ?? 0) + 1);
30324
+ fileGroupSize.set(c.filePath, (fileGroupSize.get(c.filePath) ?? 0) + 1);
30325
+ }
30326
+ for (let i = 0; i < candidates.length; i++) {
30327
+ const c = candidates[i];
30328
+ const parent = parents[i];
30329
+ const plainQualifier = plainQualifiers[i];
30330
+ const collides = (qualifierCounts.get(`${c.filePath} ${plainQualifier}`) ?? 0) > 1 || parent === null && (fileGroupSize.get(c.filePath) ?? 0) > 1;
30331
+ const qualifier = collides ? `${plainQualifier}@${c.lineStart}` : plainQualifier;
30138
30332
  const retryFile = multiFile ? toDisplayPath(displayRoot, c.filePath) : file2;
30139
30333
  const label = multiFile ? `${toDisplayPath(displayRoot, c.filePath)}::${qualifier}` : qualifier;
30140
- lines2.push(` - ${label} (line ${c.lineStart}) -> token-goat read "${retryFile}::${qualifier}"`);
30334
+ lines2.push(` - ${label} (line ${c.lineStart}) -> token-goat ${commandName} "${retryFile}::${qualifier}"`);
30141
30335
  }
30142
30336
  return lines2.join("\n");
30143
30337
  }
30144
30338
  function resolveSymbolSpec(spec, forceRefresh, projectRoot) {
30145
- const { file: file2, symbol: symbol3 } = parseReadSpec(spec);
30146
- if (symbol3 === void 0 || symbol3 === "") return { kind: "none" };
30339
+ const { file: file2, symbol: rawSymbol } = parseReadSpec(spec);
30340
+ if (rawSymbol === void 0 || rawSymbol === "") return { kind: "none" };
30341
+ const anchorMatch = /^(.+)@(\d+)$/.exec(rawSymbol);
30342
+ const symbol3 = anchorMatch !== null ? anchorMatch[1] : rawSymbol;
30343
+ const lineAnchor = anchorMatch !== null ? parseInt(anchorMatch[2], 10) : void 0;
30147
30344
  const resolved = resolveIndexPath(file2, projectRoot ?? process.cwd());
30148
30345
  if (forceRefresh === true) {
30149
30346
  indexFileSync(resolved, globalDbPath());
@@ -30160,9 +30357,10 @@ function resolveSymbolSpec(spec, forceRefresh, projectRoot) {
30160
30357
  seen.add(key);
30161
30358
  distinct.push(c);
30162
30359
  }
30163
- if (distinct.length === 0) return { kind: "none" };
30164
- if (distinct.length === 1) return { kind: "ok", entry: distinct[0] };
30165
- return { kind: "ambiguous", symbol: displaySymbol, file: file2, candidates: distinct };
30360
+ const anchored = lineAnchor === void 0 ? distinct : distinct.filter((c) => c.lineStart === lineAnchor);
30361
+ if (anchored.length === 0) return { kind: "none" };
30362
+ if (anchored.length === 1) return { kind: "ok", entry: anchored[0] };
30363
+ return { kind: "ambiguous", symbol: displaySymbol, file: file2, candidates: anchored };
30166
30364
  };
30167
30365
  if (symbol3.includes(".")) {
30168
30366
  const exactMatch = querySymbols({ name: symbol3, filePath: resolved, limit: 10 });
@@ -30242,6 +30440,8 @@ function runRead(opts) {
30242
30440
  return runLineRange({ file: file2, start: lineSpec.start, end: lineSpec.end }, opts);
30243
30441
  }
30244
30442
  const messages = [`Symbol '${symbol3}' not found in '${file2}'`];
30443
+ const crossFileLead = formatCrossFileLead("read", symbol3, file2, opts.projectRoot);
30444
+ if (crossFileLead !== "") messages.push(crossFileLead);
30245
30445
  const resolved = resolveIndexPath(file2, opts.projectRoot ?? process.cwd());
30246
30446
  const closes = querySymbols({ filePath: resolved, limit: DIDYOUMEAN_LIMIT }).map((s) => s.name);
30247
30447
  if (closes.length > 0) messages.push(didYouMean(closes));
@@ -30302,6 +30502,8 @@ ${sub.text}`);
30302
30502
  return { text, code: 1 };
30303
30503
  }
30304
30504
  function runSection(opts) {
30505
+ const crossFilePairs = parseCrossFileMultiSpec(opts.spec);
30506
+ if (crossFilePairs !== null) return runSectionCrossFile(crossFilePairs, opts);
30305
30507
  const colonIdx = findSpecSeparator(opts.spec);
30306
30508
  if (colonIdx === -1) {
30307
30509
  return { text: `Invalid section spec \u2014 expected "file::Heading", got: ${opts.spec}`, code: 1 };
@@ -30358,6 +30560,42 @@ ${sub.text}`);
30358
30560
  if (anyFound) recordReadStat("section_read", fullSourceBytes, text, opts.spec);
30359
30561
  return { text, code: anyFound ? 0 : 1 };
30360
30562
  }
30563
+ function runSectionCrossFile(pairs, opts) {
30564
+ let anyFound = false;
30565
+ const jsonOut = {};
30566
+ const textBlocks = [];
30567
+ const distinctFiles = new Set(pairs.map((p) => p.file));
30568
+ const keyFor = (p) => distinctFiles.size === 1 ? p.symbol : `${p.file}::${p.symbol}`;
30569
+ for (const { file: file2, symbol: heading } of pairs) {
30570
+ const sub = runSection({ ...opts, spec: `${file2}::${heading}`, suppressStat: true });
30571
+ if (sub.code === 0) anyFound = true;
30572
+ const key = keyFor({ file: file2, symbol: heading });
30573
+ if (opts.json === true) {
30574
+ jsonOut[key] = sub.code === 0 ? JSON.parse(sub.text) : { error: sub.text };
30575
+ continue;
30576
+ }
30577
+ textBlocks.push(`${key}:
30578
+ ${sub.text}`);
30579
+ }
30580
+ const resolvePath = (f) => opts.projectRoot !== void 0 && !path47.isAbsolute(f) ? path47.resolve(opts.projectRoot, f) : f;
30581
+ const text = opts.json === true ? JSON.stringify(jsonOut, null, 2) : textBlocks.join("\n\n");
30582
+ if (anyFound) {
30583
+ const fullSourceBytes = sumFileSizes(Array.from(distinctFiles, resolvePath));
30584
+ recordReadStat("section_read", fullSourceBytes, text, opts.spec);
30585
+ }
30586
+ return { text, code: anyFound ? 0 : 1 };
30587
+ }
30588
+ function renderRefLines(ref2, displayRoot, contextLines, indent = " ") {
30589
+ const displayPath = toDisplayPath(displayRoot, ref2.filePath);
30590
+ const base = `${indent}${displayPath}:${ref2.line}: ${ref2.context}`;
30591
+ const window = buildContextWindow(ref2.filePath, ref2.line, contextLines);
30592
+ if (window === null) return [base];
30593
+ return [base, ...renderContextWindow(displayPath, ref2.line, window, "", `${indent} `)];
30594
+ }
30595
+ function withContextLines(items, contextLines) {
30596
+ if (!(contextLines > 0)) return items;
30597
+ return items.map((r) => ({ ...r, contextLines: buildContextWindow(r.filePath, r.line, contextLines) ?? [] }));
30598
+ }
30361
30599
  function applyTypedRefsTier(symName, file2, results) {
30362
30600
  if (results.length === 0) return results;
30363
30601
  try {
@@ -30395,6 +30633,8 @@ function runRefs(opts) {
30395
30633
  emitErr2(`--top must be a positive number, got: ${opts.top}`);
30396
30634
  return 1;
30397
30635
  }
30636
+ const crossFilePairs = parseCrossFileMultiSpec(opts.spec);
30637
+ if (crossFilePairs !== null) return runRefsCrossFile(crossFilePairs, opts);
30398
30638
  const { file: file2, symbols } = parseMultiRefsSpec(opts.spec);
30399
30639
  if (symbols.length <= 1) return runRefsSingle(opts);
30400
30640
  const jsonOut = {};
@@ -30403,10 +30643,18 @@ function runRefs(opts) {
30403
30643
  const refFilePaths = [];
30404
30644
  for (const sym of symbols) {
30405
30645
  const queryOpts = { name: sym };
30406
- if (file2 !== void 0 && opts.callers !== true) queryOpts.filePath = resolveIndexPath(file2);
30407
- if (opts.limit !== void 0) queryOpts.limit = opts.limit;
30646
+ if (opts.excludeTests === true) queryOpts.limit = REFS_TOP_SCAN_LIMIT;
30647
+ else if (opts.limit !== void 0) queryOpts.limit = opts.limit;
30408
30648
  else if (opts.top !== void 0) queryOpts.limit = REFS_TOP_SCAN_LIMIT;
30409
- const results = applyTypedRefsTier(sym, file2, queryRefs(queryOpts));
30649
+ let results = applyTypedRefsTier(sym, file2, queryRefs(queryOpts));
30650
+ let suppressed = 0;
30651
+ let filteredTotal;
30652
+ if (opts.excludeTests === true) {
30653
+ const f = applyExcludeTestsFilter(results);
30654
+ suppressed = f.suppressed;
30655
+ filteredTotal = f.refs.length;
30656
+ results = opts.top !== void 0 ? f.refs : f.refs.slice(0, opts.limit ?? 100);
30657
+ }
30410
30658
  if (results.length > 0) anyFound = true;
30411
30659
  refFilePaths.push(...results.map((r) => r.filePath));
30412
30660
  if (opts.json === true) {
@@ -30414,22 +30662,85 @@ function runRefs(opts) {
30414
30662
  jsonOut[sym] = topFilesJsonPayload(results, opts.top);
30415
30663
  } else {
30416
30664
  const capped = guardJsonRows(results);
30417
- const trueTotal = countRefs(queryOpts);
30418
- jsonOut[sym] = { items: capped.items, truncated: capped.truncated || trueTotal > results.length, totalCount: trueTotal };
30665
+ const trueTotal = opts.excludeTests === true ? filteredTotal ?? results.length : countRefs(queryOpts);
30666
+ jsonOut[sym] = { items: withContextLines(capped.items, opts.context ?? 0), truncated: capped.truncated || trueTotal > results.length, totalCount: trueTotal };
30419
30667
  }
30420
30668
  continue;
30421
30669
  }
30422
30670
  if (results.length === 0) {
30423
- lines2.push(`${sym}: (no references found)`);
30671
+ lines2.push(opts.excludeTests === true && suppressed > 0 ? `${sym}: (no non-test references found; ${suppressed} in test files hidden by --exclude-tests)` : `${sym}: (no references found)`);
30424
30672
  continue;
30425
30673
  }
30426
30674
  lines2.push(`${sym}:`);
30427
30675
  if (opts.top !== void 0) {
30428
- lines2.push(...renderTopFilesSummary(results, opts.top));
30676
+ lines2.push(...renderTopFilesSummary(results, opts.top, void 0, suppressed));
30429
30677
  } else if (opts.callers === true) {
30430
- lines2.push(...renderCallerGroups(results));
30678
+ if (opts.excludeTests === true && suppressed > 0) lines2.push(` ${results.length} references (${suppressed} in test files hidden by --exclude-tests)`);
30679
+ lines2.push(...renderCallerGroups(results, void 0, opts.context ?? 0));
30431
30680
  } else {
30432
- for (const ref2 of results) lines2.push(` ${ref2.filePath}:${ref2.line}: ${ref2.context}`);
30681
+ if (opts.excludeTests === true && suppressed > 0) lines2.push(` ${results.length} references (${suppressed} in test files hidden by --exclude-tests)`);
30682
+ for (const ref2 of results) lines2.push(...renderRefLines(ref2, void 0, opts.context ?? 0));
30683
+ }
30684
+ }
30685
+ const fullSourceBytes = sumFileSizes(refFilePaths);
30686
+ if (opts.json === true) {
30687
+ const text2 = JSON.stringify(jsonOut, null, 2);
30688
+ emit3(text2);
30689
+ if (anyFound) recordReadStat("symbol_read", fullSourceBytes, text2, opts.spec);
30690
+ return anyFound ? 0 : 1;
30691
+ }
30692
+ const text = lines2.join("\n");
30693
+ emitGuarded(text, "symbol");
30694
+ if (anyFound) recordReadStat("symbol_read", fullSourceBytes, text, opts.spec);
30695
+ return anyFound ? 0 : 1;
30696
+ }
30697
+ function runRefsCrossFile(pairs, opts) {
30698
+ const distinctFiles = new Set(pairs.map((p) => p.file));
30699
+ const keyFor = (p) => distinctFiles.size === 1 ? p.symbol : `${p.file}::${p.symbol}`;
30700
+ const jsonOut = {};
30701
+ let anyFound = false;
30702
+ const lines2 = [];
30703
+ const refFilePaths = [];
30704
+ for (const { file: file2, symbol: symbol3 } of pairs) {
30705
+ const key = keyFor({ file: file2, symbol: symbol3 });
30706
+ const queryOpts = { name: symbol3 };
30707
+ if (opts.excludeTests === true) queryOpts.limit = REFS_TOP_SCAN_LIMIT;
30708
+ else if (opts.limit !== void 0) queryOpts.limit = opts.limit;
30709
+ else if (opts.top !== void 0) queryOpts.limit = REFS_TOP_SCAN_LIMIT;
30710
+ let results = applyTypedRefsTier(symbol3, file2, queryRefs(queryOpts));
30711
+ let suppressed = 0;
30712
+ let filteredTotal;
30713
+ if (opts.excludeTests === true) {
30714
+ const f = applyExcludeTestsFilter(results);
30715
+ suppressed = f.suppressed;
30716
+ filteredTotal = f.refs.length;
30717
+ results = opts.top !== void 0 ? f.refs : f.refs.slice(0, opts.limit ?? 100);
30718
+ }
30719
+ if (results.length > 0) anyFound = true;
30720
+ refFilePaths.push(...results.map((r) => r.filePath));
30721
+ if (opts.json === true) {
30722
+ if (opts.top !== void 0) {
30723
+ jsonOut[key] = topFilesJsonPayload(results, opts.top);
30724
+ } else {
30725
+ const capped = guardJsonRows(results);
30726
+ const trueTotal = opts.excludeTests === true ? filteredTotal ?? results.length : countRefs(queryOpts);
30727
+ jsonOut[key] = { items: withContextLines(capped.items, opts.context ?? 0), truncated: capped.truncated || trueTotal > results.length, totalCount: trueTotal };
30728
+ }
30729
+ continue;
30730
+ }
30731
+ if (results.length === 0) {
30732
+ lines2.push(opts.excludeTests === true && suppressed > 0 ? `${key}: (no non-test references found; ${suppressed} in test files hidden by --exclude-tests)` : `${key}: (no references found)`);
30733
+ continue;
30734
+ }
30735
+ lines2.push(`${key}:`);
30736
+ if (opts.top !== void 0) {
30737
+ lines2.push(...renderTopFilesSummary(results, opts.top, void 0, suppressed));
30738
+ } else if (opts.callers === true) {
30739
+ if (opts.excludeTests === true && suppressed > 0) lines2.push(` ${results.length} references (${suppressed} in test files hidden by --exclude-tests)`);
30740
+ lines2.push(...renderCallerGroups(results, void 0, opts.context ?? 0));
30741
+ } else {
30742
+ if (opts.excludeTests === true && suppressed > 0) lines2.push(` ${results.length} references (${suppressed} in test files hidden by --exclude-tests)`);
30743
+ for (const ref2 of results) lines2.push(...renderRefLines(ref2, void 0, opts.context ?? 0));
30433
30744
  }
30434
30745
  }
30435
30746
  const fullSourceBytes = sumFileSizes(refFilePaths);
@@ -30449,11 +30760,23 @@ function runRefsSingle(opts) {
30449
30760
  const symName = symbol3 ?? file2;
30450
30761
  const queryOpts = { name: symName };
30451
30762
  const defFileHint = symbol3 !== void 0 ? resolveIndexPath(file2) : void 0;
30452
- if (defFileHint !== void 0 && opts.callers !== true) queryOpts.filePath = defFileHint;
30453
- if (opts.limit !== void 0) queryOpts.limit = opts.limit;
30763
+ if (opts.excludeTests === true) queryOpts.limit = REFS_TOP_SCAN_LIMIT;
30764
+ else if (opts.limit !== void 0) queryOpts.limit = opts.limit;
30454
30765
  else if (opts.top !== void 0) queryOpts.limit = REFS_TOP_SCAN_LIMIT;
30455
- const results = applyTypedRefsTier(symName, defFileHint, queryRefs(queryOpts));
30766
+ let results = applyTypedRefsTier(symName, defFileHint, queryRefs(queryOpts));
30767
+ let suppressed = 0;
30768
+ let filteredTotal;
30769
+ if (opts.excludeTests === true) {
30770
+ const f = applyExcludeTestsFilter(results);
30771
+ suppressed = f.suppressed;
30772
+ filteredTotal = f.refs.length;
30773
+ results = opts.top !== void 0 ? f.refs : f.refs.slice(0, opts.limit ?? 100);
30774
+ }
30456
30775
  if (results.length === 0) {
30776
+ if (opts.excludeTests === true && suppressed > 0) {
30777
+ emitErr2(`No non-test references found for '${symName}' (${suppressed} in test files hidden by --exclude-tests)`);
30778
+ return 1;
30779
+ }
30457
30780
  emitErr2(`No references found for '${symName}'`);
30458
30781
  return 1;
30459
30782
  }
@@ -30464,8 +30787,8 @@ function runRefsSingle(opts) {
30464
30787
  payload = topFilesJsonPayload(results, opts.top);
30465
30788
  } else {
30466
30789
  const capped = guardJsonRows(results);
30467
- const trueTotal = countRefs(queryOpts);
30468
- payload = { items: capped.items, truncated: capped.truncated || trueTotal > results.length, totalCount: trueTotal };
30790
+ const trueTotal = opts.excludeTests === true ? filteredTotal ?? results.length : countRefs(queryOpts);
30791
+ payload = { items: withContextLines(capped.items, opts.context ?? 0), truncated: capped.truncated || trueTotal > results.length, totalCount: trueTotal };
30469
30792
  }
30470
30793
  const text2 = JSON.stringify(payload, null, 2);
30471
30794
  emit3(text2);
@@ -30473,21 +30796,26 @@ function runRefsSingle(opts) {
30473
30796
  return 0;
30474
30797
  }
30475
30798
  const displayRoot = getDisplayRoot();
30476
- const lines2 = opts.top !== void 0 ? renderTopFilesSummary(results, opts.top, displayRoot) : opts.callers === true ? renderCallerGroups(results, displayRoot) : results.map((ref2) => `${toDisplayPath(displayRoot, ref2.filePath)}:${ref2.line}: ${ref2.context}`);
30799
+ const lines2 = opts.top !== void 0 ? renderTopFilesSummary(results, opts.top, displayRoot, suppressed) : opts.callers === true ? [...opts.excludeTests === true && suppressed > 0 ? [`${results.length} references (${suppressed} in test files hidden by --exclude-tests)`] : [], ...renderCallerGroups(results, displayRoot, opts.context ?? 0)] : [...opts.excludeTests === true && suppressed > 0 ? [`${results.length} references (${suppressed} in test files hidden by --exclude-tests)`] : [], ...results.flatMap((ref2) => renderRefLines(ref2, displayRoot, opts.context ?? 0, ""))];
30477
30800
  const text = lines2.join("\n");
30478
30801
  emitGuarded(text, "symbol");
30479
30802
  recordReadStat("symbol_read", fullSourceBytes, text, symName);
30480
30803
  return 0;
30481
30804
  }
30805
+ function applyExcludeTestsFilter(refs) {
30806
+ const filtered = refs.filter((r) => !isTestFile(r.filePath));
30807
+ return { refs: filtered, suppressed: refs.length - filtered.length };
30808
+ }
30482
30809
  function groupRefsByFile(refs) {
30483
30810
  const byFile = /* @__PURE__ */ new Map();
30484
30811
  for (const ref2 of refs) byFile.set(ref2.filePath, (byFile.get(ref2.filePath) ?? 0) + 1);
30485
30812
  return [...byFile.entries()].map(([file2, count]) => ({ file: file2, count })).sort((a, b) => b.count - a.count || (a.file < b.file ? -1 : a.file > b.file ? 1 : 0));
30486
30813
  }
30487
- function renderTopFilesSummary(refs, topN, displayRoot) {
30814
+ function renderTopFilesSummary(refs, topN, displayRoot, suppressed) {
30488
30815
  const grouped = groupRefsByFile(refs);
30489
30816
  const shown = grouped.slice(0, topN);
30490
- const lines2 = [`${refs.length} references across ${grouped.length} files (showing top ${shown.length})`];
30817
+ const suppressedNote = suppressed !== void 0 && suppressed > 0 ? ` (${suppressed} in test files hidden by --exclude-tests)` : "";
30818
+ const lines2 = [`${refs.length} references across ${grouped.length} files (showing top ${shown.length})${suppressedNote}`];
30491
30819
  for (const { file: file2, count } of shown) lines2.push(` ${count} ${toDisplayPath(displayRoot, file2)}`);
30492
30820
  const omittedFiles = grouped.length - shown.length;
30493
30821
  if (omittedFiles > 0) {
@@ -30501,7 +30829,7 @@ function topFilesJsonPayload(refs, topN) {
30501
30829
  const shown = grouped.slice(0, topN);
30502
30830
  return { fileCounts: shown, totalFiles: grouped.length, totalRefs: refs.length, shown: shown.length };
30503
30831
  }
30504
- function renderCallerGroups(refs, displayRoot) {
30832
+ function renderCallerGroups(refs, displayRoot, contextLines = 0) {
30505
30833
  const byFile = /* @__PURE__ */ new Map();
30506
30834
  for (const ref2 of refs) {
30507
30835
  const bucket = byFile.get(ref2.filePath);
@@ -30513,9 +30841,12 @@ function renderCallerGroups(refs, displayRoot) {
30513
30841
  }
30514
30842
  const lines2 = [];
30515
30843
  for (const [file2, fileRefs] of byFile) {
30516
- lines2.push(`${toDisplayPath(displayRoot, file2)}:`);
30844
+ const displayPath = toDisplayPath(displayRoot, file2);
30845
+ lines2.push(`${displayPath}:`);
30517
30846
  for (const ref2 of fileRefs) {
30518
30847
  lines2.push(` :${ref2.line} ${ref2.context !== "" ? ref2.context : "(module scope)"}`);
30848
+ const window = buildContextWindow(file2, ref2.line, contextLines);
30849
+ if (window !== null) lines2.push(...renderContextWindow(displayPath, ref2.line, window, "", " "));
30519
30850
  }
30520
30851
  }
30521
30852
  return lines2;
@@ -30554,7 +30885,19 @@ function prepareSymbolListing(file2, opts) {
30554
30885
  const fullSourceBytes = sumFileSizes([resolved]);
30555
30886
  return { kind: "ok", resolved, filtered, refCounts, fullSourceBytes, symbolsTruncated, trueSymbolCount };
30556
30887
  }
30888
+ function runPerFileListing(files, run2) {
30889
+ const blocks = [];
30890
+ let anyOk = false;
30891
+ for (const file2 of files) {
30892
+ const r = run2(file2);
30893
+ if (r.code === 0) anyOk = true;
30894
+ blocks.push(r.text);
30895
+ }
30896
+ return { text: blocks.join("\n\n"), code: anyOk ? 0 : 1 };
30897
+ }
30557
30898
  function runSkeleton(opts) {
30899
+ const multiFiles = parseMultiFileSpec(opts.file);
30900
+ if (multiFiles !== null) return runPerFileListing(multiFiles, (file2) => runSkeleton({ ...opts, file: file2 }));
30558
30901
  const prep = prepareSymbolListing(opts.file, opts);
30559
30902
  if (prep.kind === "empty") {
30560
30903
  return { text: prep.text, code: 1 };
@@ -30590,6 +30933,8 @@ function runSkeleton(opts) {
30590
30933
  return { text, code: 0 };
30591
30934
  }
30592
30935
  function runOutline(opts) {
30936
+ const multiFiles = parseMultiFileSpec(opts.file);
30937
+ if (multiFiles !== null) return runPerFileListing(multiFiles, (file2) => runOutline({ ...opts, file: file2 }));
30593
30938
  const prep = prepareSymbolListing(opts.file, opts);
30594
30939
  if (prep.kind === "empty") {
30595
30940
  return { text: prep.text, code: 1 };
@@ -31164,10 +31509,13 @@ function runBriefCore(opts) {
31164
31509
  const resolution = resolveSymbolSpec(opts.spec);
31165
31510
  if (resolution.kind === "ambiguous") {
31166
31511
  return {
31512
+ // Name the command explicitly: formatAmbiguity defaults to 'read', so brief's retry lines would otherwise tell the user to run `token-goat read`, which answers a different question than the one they asked.
31167
31513
  text: formatAmbiguity(
31168
31514
  resolution.symbol,
31169
31515
  resolution.file,
31170
- resolution.candidates
31516
+ resolution.candidates,
31517
+ void 0,
31518
+ "brief"
31171
31519
  ),
31172
31520
  code: 1
31173
31521
  };
@@ -31190,7 +31538,7 @@ function runBriefCore(opts) {
31190
31538
  if (opts.json === true) {
31191
31539
  const result = {
31192
31540
  symbol: match2,
31193
- callers: shown,
31541
+ callers: (opts.context ?? 0) > 0 ? shown.map((c) => ({ ...c, contextLines: buildContextWindow(c.file, c.line, opts.context ?? 0) ?? [] })) : shown,
31194
31542
  totalCallers,
31195
31543
  truncated,
31196
31544
  section: section2
@@ -31209,7 +31557,10 @@ function runBriefCore(opts) {
31209
31557
  ];
31210
31558
  lines2.push(`Callers (${totalCallers}):`);
31211
31559
  for (const c of shown) {
31212
- lines2.push(` ${c.caller} ${toDisplayPath(rootDir, c.file)}:${c.line}`);
31560
+ const callerDisplayPath = toDisplayPath(rootDir, c.file);
31561
+ lines2.push(` ${c.caller} ${callerDisplayPath}:${c.line}`);
31562
+ const window = buildContextWindow(c.file, c.line, opts.context ?? 0);
31563
+ if (window !== null) lines2.push(...renderContextWindow(callerDisplayPath, c.line, window, "", " "));
31213
31564
  }
31214
31565
  if (truncated) {
31215
31566
  lines2.push(` ...(${totalCallers - shown.length} more elided)`);
@@ -31241,11 +31592,40 @@ ${sub.text}`);
31241
31592
  if (anyFound) recordReadStat("brief_view", fullSourceBytes, text, opts.spec);
31242
31593
  return { text, code: anyFound ? 0 : 1 };
31243
31594
  }
31595
+ function runBriefCrossFile(pairs, opts) {
31596
+ const distinctFiles = new Set(pairs.map((p) => p.file));
31597
+ const keyFor = (p) => distinctFiles.size === 1 ? p.symbol : `${p.file}::${p.symbol}`;
31598
+ let anyFound = false;
31599
+ const jsonOut = {};
31600
+ const textBlocks = [];
31601
+ for (const { file: file2, symbol: symbol3 } of pairs) {
31602
+ const key = keyFor({ file: file2, symbol: symbol3 });
31603
+ const sub = runBriefCore({ ...opts, spec: `${file2}::${symbol3}`, suppressStat: true });
31604
+ if (sub.code === 0) anyFound = true;
31605
+ if (opts.json === true) {
31606
+ jsonOut[key] = sub.code === 0 ? JSON.parse(sub.text) : { error: sub.text };
31607
+ continue;
31608
+ }
31609
+ textBlocks.push(`${key}:
31610
+ ${sub.text}`);
31611
+ }
31612
+ const fullSourceBytes = sumFileSizes([...distinctFiles].map((f) => resolveIndexPath(f, process.cwd())));
31613
+ const text = opts.json === true ? JSON.stringify(jsonOut, null, 2) : textBlocks.join("\n\n");
31614
+ if (anyFound) recordReadStat("brief_view", fullSourceBytes, text, opts.spec);
31615
+ return { text, code: anyFound ? 0 : 1 };
31616
+ }
31244
31617
  function runBrief(opts) {
31245
31618
  if (opts.limit !== void 0 && opts.limit <= 0) {
31246
31619
  emitErr2(`--limit must be a positive number, got: ${opts.limit}`);
31247
31620
  return 1;
31248
31621
  }
31622
+ const crossFilePairs = parseCrossFileMultiSpec(opts.spec);
31623
+ if (crossFilePairs !== null) {
31624
+ const { text: text2, code: code2 } = runBriefCrossFile(crossFilePairs, opts);
31625
+ if (code2 === 0) emit3(text2);
31626
+ else emitErr2(text2);
31627
+ return code2;
31628
+ }
31249
31629
  const { file: file2, symbol: symbol3 } = parseReadSpec(opts.spec);
31250
31630
  if (symbol3 !== void 0 && symbol3 !== "" && symbol3.includes(",")) {
31251
31631
  const multiSymbols = symbol3.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
@@ -31337,6 +31717,34 @@ function parseDiffHunks(diffText) {
31337
31717
  }
31338
31718
  return hunksByFile;
31339
31719
  }
31720
+ function buildChangedRefHint(cwd, ref2) {
31721
+ const countResult = runGit(["rev-list", "--count", "HEAD"], { cwd });
31722
+ if (countResult.exitCode !== 0) {
31723
+ return null;
31724
+ }
31725
+ const commitCount = Number.parseInt(countResult.stdout.trim(), 10);
31726
+ if (!Number.isFinite(commitCount) || commitCount < 1) {
31727
+ return null;
31728
+ }
31729
+ const refResolves = runGit(["rev-parse", "--verify", "--quiet", ref2], { cwd });
31730
+ if (refResolves.exitCode === 0) {
31731
+ return null;
31732
+ }
31733
+ let suggestedRef = null;
31734
+ for (let n = commitCount - 1; n >= 1; n--) {
31735
+ const candidate = `HEAD~${n}`;
31736
+ const candidateResolves = runGit(["rev-parse", "--verify", "--quiet", candidate], { cwd });
31737
+ if (candidateResolves.exitCode === 0) {
31738
+ suggestedRef = candidate;
31739
+ break;
31740
+ }
31741
+ }
31742
+ if (suggestedRef === null) {
31743
+ suggestedRef = EMPTY_TREE_HASH;
31744
+ }
31745
+ const commitWord = commitCount === 1 ? "1 commit" : `${commitCount} commits`;
31746
+ return `Hint: this repo has only ${commitWord}; '${ref2}' does not exist. Try: token-goat changed --since ${suggestedRef}`;
31747
+ }
31340
31748
  function runChanged(opts = {}) {
31341
31749
  const ref2 = opts.ref ?? "HEAD~5";
31342
31750
  const cwd = opts.projectRoot ?? process.cwd();
@@ -31346,6 +31754,10 @@ function runChanged(opts = {}) {
31346
31754
  const result = runGit(["diff", ref2, "--name-only"], { cwd });
31347
31755
  if (result.exitCode !== 0) {
31348
31756
  emitErr2(`git diff failed: ${result.stderr}`);
31757
+ const hint = buildChangedRefHint(cwd, ref2);
31758
+ if (hint !== null) {
31759
+ emitErr2(hint);
31760
+ }
31349
31761
  return 1;
31350
31762
  }
31351
31763
  changedFiles = result.stdout.trim().split(/\r?\n/).filter(Boolean);
@@ -31451,13 +31863,16 @@ function resolveSymbolSpecOrEmitError(commandName, spec, projectRoot) {
31451
31863
  resolution.symbol,
31452
31864
  resolution.file,
31453
31865
  resolution.candidates,
31454
- projectRoot
31866
+ projectRoot,
31867
+ commandName
31455
31868
  )
31456
31869
  );
31457
31870
  return null;
31458
31871
  }
31459
31872
  if (resolution.kind === "none") {
31460
31873
  const messages = [`Symbol '${symbol3}' not found in '${file2}'`];
31874
+ const crossFileLead = formatCrossFileLead(commandName, symbol3, file2, projectRoot);
31875
+ if (crossFileLead !== "") messages.push(crossFileLead);
31461
31876
  const resolved = resolveIndexPath(file2, projectRoot ?? process.cwd());
31462
31877
  const closes = querySymbols({ filePath: resolved, limit: DIDYOUMEAN_LIMIT }).map((s) => s.name);
31463
31878
  if (closes.length > 0) messages.push(didYouMean(closes));
@@ -31667,22 +32082,29 @@ function runGrep(opts) {
31667
32082
  return 1;
31668
32083
  }
31669
32084
  const truncated = hits.slice(0, maxLines);
32085
+ if (opts.symbol === true) {
32086
+ const symbolsByFile = /* @__PURE__ */ new Map();
32087
+ for (const hit of truncated) {
32088
+ let syms = symbolsByFile.get(hit.file);
32089
+ if (syms === void 0) {
32090
+ syms = querySymbols({ filePath: resolveIndexPath(hit.file), limit: ALL_SYMBOLS_IN_FILE_LIMIT });
32091
+ symbolsByFile.set(hit.file, syms);
32092
+ }
32093
+ const enc = enclosingSymbol(syms, hit.line);
32094
+ hit.symbol = enc === null ? null : { name: enc.name, kind: enc.kind, lineStart: enc.lineStart, lineEnd: enc.lineEnd };
32095
+ }
32096
+ }
31670
32097
  if (opts.json === true) {
31671
32098
  const payload = { items: truncated, truncated: hits.length > maxLines, totalCount: hits.length };
31672
32099
  emit3(JSON.stringify(payload, null, 2));
31673
32100
  return 0;
31674
32101
  }
31675
32102
  for (const hit of truncated) {
32103
+ const symbolTag = opts.symbol === true && hit.symbol != null ? ` [${hit.symbol.name} (${hit.symbol.kind})]` : "";
31676
32104
  if (hit.context !== void 0) {
31677
- for (const ctxLine of hit.context) {
31678
- if (ctxLine.line === hit.line) {
31679
- emit3(`${hit.file}:${ctxLine.line}: ${ctxLine.text}`);
31680
- } else {
31681
- emit3(`${hit.file}-${ctxLine.line}- ${ctxLine.text}`);
31682
- }
31683
- }
32105
+ for (const line of renderContextWindow(hit.file, hit.line, hit.context, symbolTag)) emit3(line);
31684
32106
  } else {
31685
- emit3(`${hit.file}:${hit.line}: ${hit.text}`);
32107
+ emit3(`${hit.file}:${hit.line}: ${hit.text}${symbolTag}`);
31686
32108
  }
31687
32109
  }
31688
32110
  if (hits.length > maxLines) {
@@ -31865,9 +32287,24 @@ function extractExportNames(text, ext2) {
31865
32287
  }
31866
32288
  return names;
31867
32289
  }
32290
+ function runPerFileEmitting(files, label, run2) {
32291
+ let anyOk = false;
32292
+ files.forEach((file2, i) => {
32293
+ if (i > 0) emit3("");
32294
+ emit3(`# ${label}: ${file2}`);
32295
+ if (run2(file2) === 0) anyOk = true;
32296
+ });
32297
+ return anyOk ? 0 : 1;
32298
+ }
31868
32299
  function runExports(opts) {
32300
+ const multiFiles = parseMultiFileSpec(opts.file);
32301
+ if (multiFiles !== null) return runPerFileEmitting(multiFiles, "Exports", (file2) => runExports({ ...opts, file: file2 }));
31869
32302
  const symbols = querySymbols({ filePath: resolveIndexPath(opts.file), limit: 500 });
31870
32303
  const kindOf = (name2) => symbols.find((s) => s.name === name2)?.kind ?? "export";
32304
+ const locOf = (name2) => {
32305
+ const s = symbols.find((sym) => sym.name === name2);
32306
+ return s === void 0 ? null : { lineStart: s.lineStart, lineEnd: s.lineEnd };
32307
+ };
31871
32308
  const names = [];
31872
32309
  for (const s of symbols) {
31873
32310
  if (/^(?:export|pub\b|public\b)/.test(s.body.trimStart()) && !names.includes(s.name)) {
@@ -31892,12 +32329,23 @@ function runExports(opts) {
31892
32329
  }
31893
32330
  const fullSourceBytes = sumFileSizes([opts.file]);
31894
32331
  if (opts.json === true) {
31895
- const jsonText = JSON.stringify(names.map((n) => ({ name: n, kind: kindOf(n) })), null, 2);
32332
+ const jsonText = JSON.stringify(
32333
+ names.map((n) => {
32334
+ const loc = locOf(n);
32335
+ return { name: n, kind: kindOf(n), lineStart: loc?.lineStart ?? null, lineEnd: loc?.lineEnd ?? null };
32336
+ }),
32337
+ null,
32338
+ 2
32339
+ );
31896
32340
  emit3(jsonText);
31897
32341
  recordReadStat("exports", fullSourceBytes, jsonText, opts.file);
31898
32342
  return 0;
31899
32343
  }
31900
- const outLines = names.map((n) => `${kindOf(n).padEnd(10)} ${n}`);
32344
+ const outLines = names.map((n) => {
32345
+ const loc = locOf(n);
32346
+ const locSuffix = loc === null ? "" : ` (${loc.lineStart}-${loc.lineEnd})`;
32347
+ return `${kindOf(n).padEnd(10)} ${n}${locSuffix}`;
32348
+ });
31901
32349
  for (const line of outLines) {
31902
32350
  emit3(line);
31903
32351
  }
@@ -32191,6 +32639,8 @@ function importsExtensionFor(filePath) {
32191
32639
  return path47.extname(filePath);
32192
32640
  }
32193
32641
  function runImports(opts) {
32642
+ const multiFiles = parseMultiFileSpec(opts.file);
32643
+ if (multiFiles !== null) return runPerFileEmitting(multiFiles, "Imports", (file2) => runImports({ ...opts, file: file2 }));
32194
32644
  const text = readFileText(opts.file);
32195
32645
  if (text === null) {
32196
32646
  emitErr2(`Could not read: ${opts.file}`);
@@ -32284,11 +32734,12 @@ async function runSemantic(query, opts) {
32284
32734
  );
32285
32735
  const hits = mergeNearbyHits(rawHits).slice(0, n);
32286
32736
  if (hits.length > 0) {
32737
+ const enclosing = hits.map((h) => resolveEnclosingSymbol(h.filePath, h.startLine));
32287
32738
  if (opts.json === true) {
32288
- const items = hits.map((h) => ({
32739
+ const items = hits.map((h, i) => ({
32289
32740
  filePath: h.filePath,
32290
- name: null,
32291
- kind: null,
32741
+ name: enclosing[i]?.name ?? null,
32742
+ kind: enclosing[i]?.kind ?? null,
32292
32743
  startLine: h.startLine,
32293
32744
  endLine: h.endLine,
32294
32745
  distance: h.distance,
@@ -32299,10 +32750,12 @@ async function runSemantic(query, opts) {
32299
32750
  recordReadStat("semantic_search", sumFileSizes(hits.map((h) => h.filePath)), text3, query);
32300
32751
  return { text: text3, code: 0 };
32301
32752
  }
32302
- const blocks2 = hits.map(
32303
- (h) => `# ${toDisplayPath(rootDir, h.filePath)}:${h.startLine}-${h.endLine} (distance ${h.distance.toFixed(3)})
32304
- ${previewLines(h.text, 3)}`
32305
- );
32753
+ const blocks2 = hits.map((h, i) => {
32754
+ const enc = enclosing[i] ?? null;
32755
+ const suffix = enc !== null ? ` \u2014 inside ${enc.name} (${enc.kind})` : "";
32756
+ return `# ${toDisplayPath(rootDir, h.filePath)}:${h.startLine}-${h.endLine} (distance ${h.distance.toFixed(3)})${suffix}
32757
+ ${previewLines(h.text, 3)}`;
32758
+ });
32306
32759
  const text2 = guardText(blocks2.join("\n\n"), "semantic");
32307
32760
  recordReadStat("semantic_search", sumFileSizes(hits.map((h) => h.filePath)), text2, query);
32308
32761
  return { text: text2, code: 0 };
@@ -32391,7 +32844,7 @@ function runNoteList(opts = {}) {
32391
32844
  });
32392
32845
  return { text: lines2.join("\n"), code: 0 };
32393
32846
  }
32394
- var DIDYOUMEAN_LIMIT, MIN_REVERSE_MATCH_LEN, GREP_MAX_LINES, FIND_SCAN_LIMIT, REFS_TOP_SCAN_LIMIT, STALE_WARNING, PARENT_IDENTIFIER_RE, SKELETON_SYMBOL_CAP, HUNK_HEADER_RE, DEFAULT_LOG_MAX_COUNT;
32847
+ var DIDYOUMEAN_LIMIT, MIN_REVERSE_MATCH_LEN, GREP_MAX_LINES, FIND_SCAN_LIMIT, REFS_TOP_SCAN_LIMIT, STALE_WARNING, PARENT_IDENTIFIER_RE, SKELETON_SYMBOL_CAP, HUNK_HEADER_RE, EMPTY_TREE_HASH, DEFAULT_LOG_MAX_COUNT;
32395
32848
  var init_read_commands = __esm({
32396
32849
  "src/read_commands.ts"() {
32397
32850
  "use strict";
@@ -32436,6 +32889,7 @@ var init_read_commands = __esm({
32436
32889
  PARENT_IDENTIFIER_RE = /^[\w$]+$/;
32437
32890
  SKELETON_SYMBOL_CAP = 5e3;
32438
32891
  HUNK_HEADER_RE = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/;
32892
+ EMPTY_TREE_HASH = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
32439
32893
  DEFAULT_LOG_MAX_COUNT = 20;
32440
32894
  }
32441
32895
  });
@@ -49289,6 +49743,7 @@ init_index_reader();
49289
49743
  init_parser_types();
49290
49744
  init_doc_embed_extract();
49291
49745
  init_paths();
49746
+ init_project();
49292
49747
  init_hooks_index();
49293
49748
  init_install();
49294
49749
  init_codex_install();
@@ -64888,7 +65343,7 @@ function checkDbExists(dataDir2) {
64888
65343
  return {
64889
65344
  name: "Database",
64890
65345
  status: "ok",
64891
- message: `global.db exists (${toKB(sizeBytes)} KB)`
65346
+ message: `global.db exists (${toKB(sizeBytes)} KB) at ${dbPath}`
64892
65347
  };
64893
65348
  }
64894
65349
  function checkSymbolBodySize(dbPath) {
@@ -64936,6 +65391,13 @@ function checkSymbolCount(dbPath, rootDir) {
64936
65391
  message: `${fileCount} file(s) indexed but 0 symbols extracted \u2014 the parser may not be running (check the worker log); try 'token-goat index --force'`
64937
65392
  };
64938
65393
  }
65394
+ if (fileCount === 0 && symbolCount === 0) {
65395
+ return {
65396
+ name: "Symbols",
65397
+ status: "warn",
65398
+ message: `no files indexed for this project \u2014 every read command will return empty, which looks like a genuine "not found" rather than a missing index; run 'token-goat index .' here`
65399
+ };
65400
+ }
64939
65401
  return {
64940
65402
  name: "Symbols",
64941
65403
  status: "ok",
@@ -72036,6 +72498,9 @@ function runHintStatsCommand(opts = {}) {
72036
72498
  `);
72037
72499
  return;
72038
72500
  }
72501
+ if (rows.every((r) => r.emitted === 0 && r.actedOn === 0)) {
72502
+ process.stdout.write("No hint emissions recorded yet \u2014 the zeros below are absence of data, not measured ineffectiveness.\n");
72503
+ }
72039
72504
  printSummary(rows);
72040
72505
  }
72041
72506
 
@@ -72294,7 +72759,11 @@ async function cmdIndex(pathArg, opts = {}) {
72294
72759
  function cmdMap(opts) {
72295
72760
  const map3 = buildProjectMap(process.cwd(), { compact: opts.compact === true });
72296
72761
  const text = formatProjectMap(map3, map3.compact);
72297
- out(text);
72762
+ if (opts.json === true) {
72763
+ out(JSON.stringify(map3));
72764
+ } else {
72765
+ out(text);
72766
+ }
72298
72767
  const bytesSaved = mapLookupBytesSaved(map3, text);
72299
72768
  recordStat("map_lookup", bytesSaved, Math.round(bytesSaved / 4));
72300
72769
  }
@@ -72544,6 +73013,14 @@ async function cmdDoctor(opts) {
72544
73013
  if (project !== null) {
72545
73014
  doctorOpts.rootDir = project.root;
72546
73015
  }
73016
+ if (opts.json === true) {
73017
+ const results = runDoctor(doctorOpts.dataDir, doctorOpts.configPath, doctorOpts.rootDir);
73018
+ out(JSON.stringify(results));
73019
+ if (results.some((r) => r.status === "fail")) {
73020
+ throw new CliError("doctor checks failed");
73021
+ }
73022
+ return;
73023
+ }
72547
73024
  const code = await runDoctorAndExit(doctorOpts);
72548
73025
  if (code !== 0) {
72549
73026
  throw new CliError("doctor checks failed");
@@ -73047,6 +73524,16 @@ function runExitText(fn) {
73047
73524
  process.exitCode = 1;
73048
73525
  }
73049
73526
  }
73527
+ function noteExtraFileArgs(command, first2, extras, fn) {
73528
+ const result = fn();
73529
+ if (extras === void 0 || extras.length === 0) return result;
73530
+ return { text: `${extraFileArgsNote(command, first2, extras)}
73531
+ ${result.text}`, code: result.code };
73532
+ }
73533
+ function emitExtraFileArgsNote(command, first2, extras) {
73534
+ if (extras === void 0 || extras.length === 0) return;
73535
+ out(extraFileArgsNote(command, first2, extras));
73536
+ }
73050
73537
  function cmdCompress(opts) {
73051
73538
  try {
73052
73539
  if (opts.compress === false) {
@@ -73171,6 +73658,10 @@ async function cmdSkillList(opts) {
73171
73658
  return `${s.name.padEnd(25)} ${bodyKb.padStart(6)}K ${compactKb.padStart(6)}K ${marker} ${s.hitCount.toString().padStart(3)} ${age.padStart(3)} ${staleStatus}`;
73172
73659
  });
73173
73660
  const header = `${"Name".padEnd(25)} ${"Body".padStart(6)} ${"Compact".padStart(6)} Marker Hits Age Status`;
73661
+ if (skills.length === 0) {
73662
+ out("No skills cached yet.");
73663
+ return;
73664
+ }
73174
73665
  out([header, ...lines2].join("\n"));
73175
73666
  }
73176
73667
  }
@@ -74008,19 +74499,26 @@ function buildProgram() {
74008
74499
  process.exitCode = 1;
74009
74500
  }
74010
74501
  };
74011
- program2.command("symbol <name>").description("search for a symbol by name").option("-l, --limit <n>", "max results").option("-f, --file <path>", "restrict to one file").option("-k, --kind <kind>", "restrict to one kind (function, class, ...)").option("-j, --json", "output as JSON").action(
74012
- (name2, opts) => runExitText(
74502
+ program2.command("symbol <name>").description("search for a symbol by name").option("-l, --limit <n>", "max results").option("-f, --file <path>", "restrict to one file").option("-k, --kind <kind>", "restrict to one kind (function, class, ...)").option("-p, --project [path]", "scope search to one project root instead of the global index (defaults to cwd)").option("-j, --json", "output as JSON").action((name2, opts) => {
74503
+ let projectRoot;
74504
+ if (opts.project === true) {
74505
+ projectRoot = resolveProjectRoot({ project: process.cwd() });
74506
+ } else if (typeof opts.project === "string") {
74507
+ projectRoot = resolveProjectRoot({ project: opts.project });
74508
+ }
74509
+ return runExitText(
74013
74510
  () => runSymbol({
74014
74511
  name: name2,
74015
74512
  limit: opts.limit !== void 0 ? requireNonNegativeInt("--limit", opts.limit) : 20,
74016
74513
  ...opts.file !== void 0 ? { file: opts.file } : {},
74017
74514
  ...opts.kind !== void 0 ? { kind: opts.kind } : {},
74515
+ ...projectRoot !== void 0 ? { projectRoot } : {},
74018
74516
  ...opts.json === true ? { json: true } : {}
74019
74517
  })
74020
- )
74021
- );
74518
+ );
74519
+ });
74022
74520
  program2.command("read <spec>").description(
74023
- "read one symbol's full body (spec: file::symbol; disambiguate a name shared by several classes with file::Parent.symbol; comma-separated file::a,b for a merged multi-symbol view, or a::x,b::y to merge symbols across several files)"
74521
+ "read one symbol's full body (spec: file::symbol; disambiguate a name shared by several classes with file::Parent.symbol; a trailing @LINE anchor -- file::symbol@LINE, or combined as file::Parent.symbol@LINE -- picks out a specific candidate by its exact starting line, for the case a Parent qualifier can't reach (e.g. a top-level definition); comma-separated file::a,b for a merged multi-symbol view, or a::x,b::y to merge symbols across several files)"
74024
74522
  ).option("-j, --json", "output as JSON").option("--force-refresh", "reparse file from disk before querying (ignore stale index)").option("--stats", "add per-symbol reference count and doc-coverage flag").action(
74025
74523
  (spec, opts) => runExitText(
74026
74524
  () => runRead({
@@ -74032,13 +74530,14 @@ function buildProgram() {
74032
74530
  )
74033
74531
  );
74034
74532
  program2.command("brief <spec>").description(
74035
- "symbol body + callers + containing doc section in one call (spec: file::symbol; comma-separated file::a,b for a merged multi-symbol view)"
74036
- ).option("-j, --json", "output as JSON").option("--limit <n>", "max callers to show (default: 20)").action(
74533
+ "symbol body + callers + containing doc section in one call (spec: file::symbol; also accepts the file::symbol@LINE anchor form documented under `read`; comma-separated file::a,b for a merged multi-symbol view; cross-file a.ts::x,b.ts::y is also supported)"
74534
+ ).option("-j, --json", "output as JSON").option("--limit <n>", "max callers to show (default: 20)").option("-C, --context <n>", "lines of call-site source to show before and after each caller (default 0)").action(
74037
74535
  (spec, opts) => runExit(
74038
74536
  () => runBrief({
74039
74537
  spec,
74040
74538
  ...opts.json === true ? { json: true } : {},
74041
- ...opts.limit !== void 0 ? { limit: requireNonNegativeInt("--limit", opts.limit) } : {}
74539
+ ...opts.limit !== void 0 ? { limit: requireNonNegativeInt("--limit", opts.limit) } : {},
74540
+ ...opts.context !== void 0 ? { context: requireNonNegativeInt("--context", opts.context) } : {}
74042
74541
  })
74043
74542
  )
74044
74543
  );
@@ -74048,44 +74547,56 @@ function buildProgram() {
74048
74547
  (spec, opts) => opts.list === true ? runExit(() => runListSections({ file: spec, ...opts.json === true ? { json: true } : {} })) : runExitText(() => runSection({ spec, ...opts.json === true ? { json: true } : {} }))
74049
74548
  );
74050
74549
  program2.command("semantic <query>").description("semantic search (falls back to full-text search)").option("-l, --limit <n>", "max results").option("-j, --json", "output as JSON").action(guard(cmdSemantic));
74051
- program2.command("skeleton <file>").description("list all symbols in a file without bodies").option("-j, --json", "output as JSON").option("--min-lines <n>", "only show symbols at least N lines long").option("--force-refresh", "reparse file from disk before querying (ignore stale index)").option("--stats", "add per-symbol reference count and doc-coverage flag").action(
74052
- (file2, opts) => runExitText(
74053
- () => runSkeleton({
74054
- file: file2,
74055
- ...opts.json === true ? { json: true } : {},
74056
- ...opts.minLines !== void 0 ? { minLines: requireNonNegativeInt("--min-lines", opts.minLines) } : {},
74057
- ...opts.forceRefresh === true ? { forceRefresh: true } : {},
74058
- ...opts.stats === true ? { stats: true } : {}
74059
- })
74550
+ program2.command("skeleton <file> [more...]").description('list all symbols in a file without bodies (also accepts a comma-separated file list "a,b,c" for one headed block per file)').option("-j, --json", "output as JSON").option("--min-lines <n>", "only show symbols at least N lines long").option("--force-refresh", "reparse file from disk before querying (ignore stale index)").option("--stats", "add per-symbol reference count and doc-coverage flag").action(
74551
+ (file2, more, opts) => runExitText(
74552
+ () => noteExtraFileArgs(
74553
+ "skeleton",
74554
+ file2,
74555
+ more,
74556
+ () => runSkeleton({
74557
+ file: file2,
74558
+ ...opts.json === true ? { json: true } : {},
74559
+ ...opts.minLines !== void 0 ? { minLines: requireNonNegativeInt("--min-lines", opts.minLines) } : {},
74560
+ ...opts.forceRefresh === true ? { forceRefresh: true } : {},
74561
+ ...opts.stats === true ? { stats: true } : {}
74562
+ })
74563
+ )
74060
74564
  )
74061
74565
  );
74062
- program2.command("outline <file>").description("list symbols with line ranges and docstrings").option("-j, --json", "output as JSON").option("--min-lines <n>", "only show symbols at least N lines long").option("--force-refresh", "reparse file from disk before querying (ignore stale index)").option("--stats", "add per-symbol reference count and doc-coverage flag").action(
74063
- (file2, opts) => runExitText(
74064
- () => runOutline({
74065
- file: file2,
74066
- ...opts.json === true ? { json: true } : {},
74067
- ...opts.minLines !== void 0 ? { minLines: requireNonNegativeInt("--min-lines", opts.minLines) } : {},
74068
- ...opts.forceRefresh === true ? { forceRefresh: true } : {},
74069
- ...opts.stats === true ? { stats: true } : {}
74070
- })
74566
+ program2.command("outline <file> [more...]").description('list symbols with line ranges and docstrings (also accepts a comma-separated file list "a,b,c" for one headed block per file)').option("-j, --json", "output as JSON").option("--min-lines <n>", "only show symbols at least N lines long").option("--force-refresh", "reparse file from disk before querying (ignore stale index)").option("--stats", "add per-symbol reference count and doc-coverage flag").action(
74567
+ (file2, more, opts) => runExitText(
74568
+ () => noteExtraFileArgs(
74569
+ "outline",
74570
+ file2,
74571
+ more,
74572
+ () => runOutline({
74573
+ file: file2,
74574
+ ...opts.json === true ? { json: true } : {},
74575
+ ...opts.minLines !== void 0 ? { minLines: requireNonNegativeInt("--min-lines", opts.minLines) } : {},
74576
+ ...opts.forceRefresh === true ? { forceRefresh: true } : {},
74577
+ ...opts.stats === true ? { stats: true } : {}
74578
+ })
74579
+ )
74071
74580
  )
74072
74581
  );
74073
- program2.command("refs <spec>").description("find references to one or more symbols (spec: file::symbol, symbol, or comma-separated a,b,c / file::a,b for a merged multi-symbol view). For an unambiguous TypeScript symbol, automatically type-resolves candidates via the TypeScript compiler API to drop same-named-different-symbol false positives; falls back to name-based matching when that is not possible.").option("--callers", "group references by their enclosing caller symbol").option("-l, --limit <n>", "max results").option(
74582
+ program2.command("refs <spec>").description("find references to one or more symbols (spec: file::symbol, symbol, or comma-separated a,b,c / file::a,b for a merged multi-symbol view; cross-file a.ts::x,b.ts::y is also supported). For an unambiguous TypeScript symbol, automatically type-resolves candidates via the TypeScript compiler API to drop same-named-different-symbol false positives; falls back to name-based matching when that is not possible.").option("--callers", "group references by their enclosing caller symbol").option("-l, --limit <n>", "max results").option(
74074
74583
  "--top <n>",
74075
74584
  "for a high-fanout symbol, group references by file (count only) and show only the top N files by reference count instead of a per-line dump"
74076
- ).option("-j, --json", "output as JSON").action(
74585
+ ).option("-C, --context <n>", "lines of call-site source to show before and after each reference (default 0)").option("-j, --json", "output as JSON").option("--exclude-tests", "hide references whose call site lives in a test file (opt-in; default output is unchanged)").action(
74077
74586
  (spec, opts) => runExit(
74078
74587
  () => runRefs({
74079
74588
  spec,
74080
74589
  ...opts.callers === true ? { callers: true } : {},
74081
74590
  ...opts.json === true ? { json: true } : {},
74082
74591
  ...opts.limit !== void 0 ? { limit: requireNonNegativeInt("--limit", opts.limit) } : {},
74083
- ...opts.top !== void 0 ? { top: requireNonNegativeInt("--top", opts.top) } : {}
74592
+ ...opts.top !== void 0 ? { top: requireNonNegativeInt("--top", opts.top) } : {},
74593
+ ...opts.context !== void 0 ? { context: requireNonNegativeInt("--context", opts.context) } : {},
74594
+ ...opts.excludeTests === true ? { excludeTests: true } : {}
74084
74595
  })
74085
74596
  )
74086
74597
  );
74087
74598
  program2.command("index [path]").description("parse all git-tracked files and (re)build the symbol index").option("--walk", "if not a git repo, index a bounded directory walk instead (skips .env / generated / oversized trees)").option("--force", "bypass the SHA-freshness cache and reindex every tracked file, even byte-identical ones (e.g. after a parser upgrade changes what gets extracted)").option("--force-walk", `index a non-git folder via --walk and raise its ${MAX_FILES_SCANNED} source-file refusal to ${MAX_FILES_SCANNED_FORCED} (slow; produces a large index)`).action(guard(cmdIndex));
74088
- program2.command("map").description("project overview").option("-c, --compact", "compact, low-token summary").action(guard(cmdMap));
74599
+ program2.command("map").description("project overview").option("-c, --compact", "compact, low-token summary").option("--json", "emit the project map as JSON instead of text").action(guard(cmdMap));
74089
74600
  program2.command("bridges-status").description("hook-event parity matrix across every AI-harness bridge (read-only static analysis, never invokes a real harness binary)").option("--json", "emit the matrix as JSON instead of text").action(guard(cmdBridgesStatus));
74090
74601
  program2.command("commands").description("machine-readable manifest of every registered command, its options, and its arguments").option("--json", "emit the manifest as JSON instead of text").option("--grep <pattern>", "filter to commands whose name, description, or aliases match this regex").action(guard(cmdCommands));
74091
74602
  program2.command("mcp-serve").description("run token-goat as an MCP stdio server exposing surgical reads and local compression/handoff tools").action(guard(cmdMcpServe));
@@ -74101,7 +74612,7 @@ function buildProgram() {
74101
74612
  worker.command("stop").description("stop the background indexer").action(guard(cmdWorkerStop));
74102
74613
  worker.command("status").description("check if the indexer is running").action(guard(cmdWorkerStatus));
74103
74614
  program2.command("stats").description("show session statistics (bare = totals only; --full for the breakdown)").option("-j, --json", "output as JSON").option("--full", "show the full breakdown (by source, by command, by day)").option("--short", "force the rich short KPI view even when stdout is not a TTY (e.g. piped)").option("--window-days <days>", "days to include (0 = all time)", "30").option("--home-dir <path>", "home directory (for testing)").action(guard(cmdStats));
74104
- program2.command("doctor").description("diagnose token-goat health").option("--context", "include context footprint analysis").action(guard(cmdDoctor));
74615
+ program2.command("doctor").description("diagnose token-goat health").option("--context", "include context footprint analysis").option("--json", "emit check results as JSON instead of text").action(guard(cmdDoctor));
74105
74616
  program2.command("context-stats").description("show context statistics").option("--project <path>", "project root to analyze").option("-j, --json", "output as JSON").option("--fix", "apply automatic fixes (confirm-gated; shows a diff before writing)").option("-y, --yes", "with --fix, apply without prompting (non-interactive / scripted use)").action(guard(cmdContextStats));
74106
74617
  program2.command("bootstrap-audit").description("audit Claude Code startup-context contributors without reading prompt bodies").option("--project <path>", "project root to analyze").option("--home <path>", "home directory override (for CI/testing)").option("--follow-links", "follow external symlink/junction roots and direct children").option("-j, --json", "output as JSON").option("--top <n>", "largest metadata entries to show (default 10)", "10").option("--warn-tokens <n>", "warn when total estimated startup tokens exceed n").option("--fail-tokens <n>", "fail when total estimated startup tokens exceed n").option("--warn-bytes <n>", "warn when agent/skill metadata bytes exceed n").option("--fail-bytes <n>", "fail when agent/skill metadata bytes exceed n").action(guard(cmdBootstrapAudit));
74107
74618
  program2.command("memory").description("analyze CLAUDE.md files for duplicate/overlapping content (--fix to apply safe mechanical fixes)").option("--project <path>", "project root to analyze").option("--analyze", "report-only analysis (default)").option("--fix", "remove exact-duplicate lines (confirm-gated; shows a diff before writing)").option("--yes", "apply --fix changes without prompting (non-interactive)").action(guard(cmdMemory));
@@ -74115,11 +74626,17 @@ function buildProgram() {
74115
74626
  program2.command("bash-output [id]").description("retrieve cached bash output by ID or file").option("--head <n>", "show first N lines").option("--tail <n>", "show last N lines").option("--grep <pattern>", "filter lines matching regex").option("--max-matches <n>", "cap --grep output to the first N matching lines").option("--section <heading>", "extract a specific section from the output").option("--full", "print the entire cached entry with no head/tail elision").option("--file <path>", "read from raw output file instead of cache").option("--transcript", "parse the --file as a JSONL agent transcript: keep assistant text blocks in order before filtering").action(guard(cmdBashOutput));
74116
74627
  program2.command("web-output [id]").description("retrieve a cached WebFetch response body by ID").option("--head <n>", "show first N lines").option("--tail <n>", "show last N lines").option("--grep <pattern>", "filter lines matching regex").option("--max-matches <n>", "cap --grep output to the first N matching lines").option("--section <heading>", "extract a specific section from the response").option("--full", "print the entire cached entry with no head/tail elision").action(guard(cmdWebOutput));
74117
74628
  program2.command("mcp-output [id]").description("retrieve a cached MCP tool result by ID (the id an MCP post_tool_use hook cached, or a `[token-goat: compressed, full via mcp-output <id>]` label points here)").option("--head <n>", "show first N lines").option("--tail <n>", "show last N lines").option("--grep <pattern>", "filter lines matching regex").option("--max-matches <n>", "cap --grep output to the first N matching lines").option("--section <heading>", "extract a specific section from the result").option("--full", "print the entire cached entry with no head/tail elision").action(guard(cmdMcpOutput));
74118
- program2.command("exports <file>").description("list exported (public) symbols in a file").option("-j, --json", "output as JSON").action(
74119
- (file2, opts) => runExit(() => runExports({ file: file2, ...opts.json === true ? { json: true } : {} }))
74629
+ program2.command("exports <file> [more...]").description('list exported (public) symbols in a file (also accepts a comma-separated file list "a,b,c" for one headed block per file)').option("-j, --json", "output as JSON").action(
74630
+ (file2, more, opts) => runExit(() => {
74631
+ emitExtraFileArgsNote("exports", file2, more);
74632
+ return runExports({ file: file2, ...opts.json === true ? { json: true } : {} });
74633
+ })
74120
74634
  );
74121
- program2.command("imports <file>").description("list the modules a file imports").option("-j, --json", "output as JSON").action(
74122
- (file2, opts) => runExit(() => runImports({ file: file2, ...opts.json === true ? { json: true } : {} }))
74635
+ program2.command("imports <file> [more...]").description('list the modules a file imports (also accepts a comma-separated file list "a,b,c" for one headed block per file)').option("-j, --json", "output as JSON").action(
74636
+ (file2, more, opts) => runExit(() => {
74637
+ emitExtraFileArgsNote("imports", file2, more);
74638
+ return runImports({ file: file2, ...opts.json === true ? { json: true } : {} });
74639
+ })
74123
74640
  );
74124
74641
  program2.command("find <pattern>").description("find files containing a symbol matching a pattern").option("-j, --json", "output as JSON").option("-l, --limit <n>", "max results").action(
74125
74642
  (pattern, opts) => runExit(
@@ -74130,7 +74647,7 @@ function buildProgram() {
74130
74647
  })
74131
74648
  )
74132
74649
  );
74133
- program2.command("grep <pattern> [paths...]").description("regex search over files, caching nothing (session-aware grep)").option("-j, --json", "output as JSON").option("--max-lines <n>", "max matching lines to print").option("--no-recursive", "do not descend into subdirectories").option("-C, --context <n>", "lines of context to show before and after each match").action(
74650
+ program2.command("grep <pattern> [paths...]").description("regex search over files, caching nothing (session-aware grep)").option("-j, --json", "output as JSON").option("--max-lines <n>", "max matching lines to print").option("--no-recursive", "do not descend into subdirectories").option("-C, --context <n>", "lines of context to show before and after each match").option("--symbol", "annotate each hit with its enclosing symbol (name and kind)").action(
74134
74651
  (pattern, paths, opts) => runExit(
74135
74652
  () => runGrep({
74136
74653
  pattern,
@@ -74138,7 +74655,8 @@ function buildProgram() {
74138
74655
  ...opts.json === true ? { json: true } : {},
74139
74656
  ...opts.maxLines !== void 0 ? { maxLines: requirePositiveInt("--max-lines", opts.maxLines) } : {},
74140
74657
  ...opts.recursive === false ? { recursive: false } : {},
74141
- ...opts.context !== void 0 ? { context: requireNonNegativeInt("--context", opts.context) } : {}
74658
+ ...opts.context !== void 0 ? { context: requireNonNegativeInt("--context", opts.context) } : {},
74659
+ ...opts.symbol === true ? { symbol: true } : {}
74142
74660
  })
74143
74661
  )
74144
74662
  );
@@ -74149,16 +74667,18 @@ function buildProgram() {
74149
74667
  program2.command("skill-history").description("list cached skill versions newest-first").option("-j, --json", "output as JSON").action(guard(cmdSkillHistory));
74150
74668
  program2.command("skill-diff <name>").description("show diff between two cached versions of a skill").action(guard(cmdSkillDiff));
74151
74669
  program2.command("skill-section <nameHeading> [headingArg]").description("extract a named section from a skill").action(guard(cmdSkillSection));
74152
- program2.command("callers <symbol>").description("find all callers of a symbol, resolved to their enclosing function").option("-j, --json", "output as JSON").option("-l, --limit <n>", "max references to scan").action(
74670
+ program2.command("callers <symbol>").description("find all callers of a symbol, resolved to their enclosing function (accepts file::symbol to disambiguate which same-named definition is meant)").option("-j, --json", "output as JSON").option("-l, --limit <n>", "max references to scan").option("-C, --context <n>", "lines of call-site source to show before and after each caller (default 0)").option("--exclude-tests", "hide callers whose call site lives in a test file (opt-in; default output is unchanged)").action(
74153
74671
  (symbol3, opts) => runExit(
74154
74672
  () => runCallers({
74155
74673
  symbol: symbol3,
74156
74674
  ...opts.json === true ? { json: true } : {},
74157
- ...opts.limit !== void 0 ? { limit: requireNonNegativeInt("--limit", opts.limit) } : {}
74675
+ ...opts.limit !== void 0 ? { limit: requireNonNegativeInt("--limit", opts.limit) } : {},
74676
+ ...opts.context !== void 0 ? { context: requireNonNegativeInt("--context", opts.context) } : {},
74677
+ ...opts.excludeTests === true ? { excludeTests: true } : {}
74158
74678
  })
74159
74679
  )
74160
74680
  );
74161
- program2.command("call-chain <symbol>").description("transitive callers up toward entry points (BFS, cycle-safe)").option("-d, --depth <n>", "max BFS depth (default 8)").option("-j, --json", "output as JSON").action(
74681
+ program2.command("call-chain <symbol>").description("transitive callers up toward entry points (BFS, cycle-safe; accepts file::symbol to disambiguate which same-named definition is meant)").option("-d, --depth <n>", "max BFS depth (default 8)").option("-j, --json", "output as JSON").action(
74162
74682
  (symbol3, opts) => runExit(
74163
74683
  () => runCallChain({
74164
74684
  symbol: symbol3,
@@ -74167,7 +74687,7 @@ function buildProgram() {
74167
74687
  })
74168
74688
  )
74169
74689
  );
74170
- program2.command("impact <symbol>").description("transitive set of callers impacted by a change (with hop depth)").option("--top <n>", "limit output to top N results").option("-j, --json", "output as JSON").action(
74690
+ program2.command("impact <symbol>").description("transitive set of callers impacted by a change (with hop depth; accepts file::symbol to disambiguate which same-named definition is meant)").option("--top <n>", "limit output to top N results").option("-j, --json", "output as JSON").action(
74171
74691
  (symbol3, opts) => runExit(
74172
74692
  () => runImpact({
74173
74693
  symbol: symbol3,
@@ -74176,13 +74696,14 @@ function buildProgram() {
74176
74696
  })
74177
74697
  )
74178
74698
  );
74179
- program2.command("dead").description("symbols with zero references (default kind: function)").option("-k, --kind <kind>", "symbol kind to check (function, method, class, ...)").option("--include-private", "include _-prefixed names").option("--top <n>", "limit output to top N results").option("-j, --json", "output as JSON").action(
74699
+ program2.command("dead").description("symbols with zero references (default kind: function)").option("-k, --kind <kind>", "symbol kind to check (function, method, class, ...)").option("--include-private", "include _-prefixed names").option("--top <n>", "limit output to top N results").option("-j, --json", "output as JSON").option("--exclude-tests", "hide dead symbols defined in a test file (opt-in; default output is unchanged)").action(
74180
74700
  (opts) => runExit(
74181
74701
  () => runDead({
74182
74702
  ...opts.kind !== void 0 ? { kind: opts.kind } : {},
74183
74703
  ...opts.includePrivate === true ? { includePrivate: true } : {},
74184
74704
  ...opts.top !== void 0 ? { top: requireNonNegativeInt("--top", opts.top) } : {},
74185
- ...opts.json === true ? { json: true } : {}
74705
+ ...opts.json === true ? { json: true } : {},
74706
+ ...opts.excludeTests === true ? { excludeTests: true } : {}
74186
74707
  })
74187
74708
  )
74188
74709
  );
@@ -74201,7 +74722,7 @@ function buildProgram() {
74201
74722
  program2.command("scope <fileColonLine>").description("list symbols enclosing a file:line position, innermost first").option("-j, --json", "output as JSON").action(
74202
74723
  (spec, opts) => runExit(() => runScope({ spec, ...opts.json === true ? { json: true } : {} }))
74203
74724
  );
74204
- program2.command("similar <spec>").description('find symbols similar to a given "file::symbol" anchor using FTS').option("--top <n>", "max results (default 10)").option("-j, --json", "output as JSON").action(
74725
+ program2.command("similar <spec>").description('find symbols similar to a given "file::symbol" anchor using FTS (also accepts the file::symbol@LINE anchor form documented under `read`)').option("--top <n>", "max results (default 10)").option("-j, --json", "output as JSON").action(
74205
74726
  (spec, opts) => runExit(
74206
74727
  () => runSimilar({
74207
74728
  spec,
@@ -74240,7 +74761,7 @@ function buildProgram() {
74240
74761
  })
74241
74762
  )
74242
74763
  );
74243
- program2.command("blame <spec>").description('git blame for the line range of a symbol ("file::symbol")').option("-j, --json", "output as JSON").action(
74764
+ program2.command("blame <spec>").description('git blame for the line range of a symbol ("file::symbol"; also accepts the file::symbol@LINE anchor form documented under `read`)').option("-j, --json", "output as JSON").action(
74244
74765
  (spec, opts) => runExit(() => runBlame({ spec, ...opts.json === true ? { json: true } : {} }))
74245
74766
  );
74246
74767
  program2.command("ask <question>").description("(experimental) find relevant code context; synthesize with an LLM if TOKEN_GOAT_ASK_BACKEND is set").option("--top <n>", "max FTS hits to surface (default 8)").option("-j, --json", "output as JSON").action(
@@ -74324,16 +74845,16 @@ function buildProgram() {
74324
74845
  }))());
74325
74846
  program2.command("fetch-image <url>").description("fetch an image URL and shrink it (saves to --out path or a temp file)").option("--out <path>", "output file path").option("-j, --json", "output as JSON").action((url2, opts) => guard(() => cmdFetchImage({ url: url2, ...opts.out !== void 0 ? { out: opts.out } : {}, ...opts.json === true ? { json: true } : {} }))());
74326
74847
  program2.command("history").description("show recent session history: bash commands and web fetches (current-session or recent cache)").option("--limit <n>", "max entries to show (default: 30)").option("-j, --json", "output as JSON").action((opts) => guard(() => cmdHistory(opts))());
74327
- program2.command("changed").description("list files or symbols changed since a git ref").option("--since <ref>", "git ref to compare against (default: HEAD~5)").option("--symbol", "list symbols instead of files").option("-j, --json", "output as JSON").action(
74328
- (opts) => runExit(
74848
+ program2.command("changed [ref]").description("list files or symbols changed since a git ref").option("--since <ref>", "git ref to compare against (default: HEAD~5)").option("--symbol", "list symbols instead of files").option("-j, --json", "output as JSON").action(
74849
+ (ref2, opts) => runExit(
74329
74850
  () => runChanged({
74330
- ref: opts.since ?? "HEAD~5",
74851
+ ref: opts.since ?? ref2 ?? "HEAD~5",
74331
74852
  ...opts.symbol === true ? { symbolMode: true } : {},
74332
74853
  ...opts.json === true ? { json: true } : {}
74333
74854
  })
74334
74855
  )
74335
74856
  );
74336
- program2.command("diff <spec> [ref]").description('show only the git diff hunk(s) that fall within one symbol\'s line range, e.g. `token-goat diff "file.ts::myFn" HEAD~3..HEAD`').option("-j, --json", "output as JSON").action(
74857
+ program2.command("diff <spec> [ref]").description('show only the git diff hunk(s) that fall within one symbol\'s line range, e.g. `token-goat diff "file.ts::myFn" HEAD~3..HEAD` (also accepts the file::symbol@LINE anchor form documented under `read`)').option("-j, --json", "output as JSON").action(
74337
74858
  (spec, ref2, opts) => runExit(
74338
74859
  () => runDiff({
74339
74860
  spec,
@@ -74342,7 +74863,7 @@ function buildProgram() {
74342
74863
  })
74343
74864
  )
74344
74865
  );
74345
- program2.command("log <spec> [ref]").description('show git commit history scoped to one symbol\'s line range, e.g. `token-goat log "file.ts::myFn" HEAD~10`').option("--max-count <n>", "maximum number of commits to show (default 20)").option("-j, --json", "output as JSON").action(
74866
+ program2.command("log <spec> [ref]").description('show git commit history scoped to one symbol\'s line range, e.g. `token-goat log "file.ts::myFn" HEAD~10` (also accepts the file::symbol@LINE anchor form documented under `read`)').option("--max-count <n>", "maximum number of commits to show (default 20)").option("-j, --json", "output as JSON").action(
74346
74867
  (spec, ref2, opts) => runExit(
74347
74868
  () => runLog({
74348
74869
  spec,