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.
@@ -3050,7 +3050,7 @@ var require_commander = __commonJS({
3050
3050
  import { createRequire } from "node:module";
3051
3051
  function resolveVersion() {
3052
3052
  if (true) {
3053
- return "2.6.24";
3053
+ return "2.6.25";
3054
3054
  }
3055
3055
  const require2 = createRequire(import.meta.url);
3056
3056
  const pkg = require2("../package.json");
@@ -4288,7 +4288,7 @@ function resolveIndexPath(file2, base = process.cwd()) {
4288
4288
  }
4289
4289
  function toDisplayPath(root, target) {
4290
4290
  if (root === void 0) return target;
4291
- const rel = path2.relative(root, target).replace(/\\/g, "/");
4291
+ const rel = path2.relative(normalizePath(root), normalizePath(target)).replace(/\\/g, "/");
4292
4292
  if (rel === "" || rel.startsWith("..") || path2.isAbsolute(rel)) {
4293
4293
  return rel === "" ? "." : target;
4294
4294
  }
@@ -4350,6 +4350,15 @@ function foldCase(s) {
4350
4350
  function runGit(args, opts = {}) {
4351
4351
  const subArgs = args[0] === "diff" ? [args[0], "--no-ext-diff", "--no-textconv", ...args.slice(1)] : args;
4352
4352
  const fullArgs = [
4353
+ // Never take an optional lock. Every git call here is on someone else's
4354
+ // working repo, and a `status` that refreshes the index writes
4355
+ // `.git/index.lock`; if this process is killed mid-call -- which the hint
4356
+ // paths deliberately invite, since they spawn under a short timeout -- the
4357
+ // orphaned lock blocks every subsequent commit in that repo until a human
4358
+ // deletes it. Observed doing exactly that on 2026-08-05. `--no-optional-
4359
+ // locks` suppresses only locks git considers optional, so write commands
4360
+ // that genuinely need one are unaffected.
4361
+ "--no-optional-locks",
4353
4362
  "-c",
4354
4363
  "core.fsmonitor=",
4355
4364
  "-c",
@@ -4673,6 +4682,28 @@ function writeIfDifferent(p, content, backup = false) {
4673
4682
  atomicWriteText(p, content);
4674
4683
  return true;
4675
4684
  }
4685
+ function buildContextWindow(absPath, line, contextLines) {
4686
+ if (!Number.isFinite(contextLines) || contextLines <= 0) return null;
4687
+ let text;
4688
+ try {
4689
+ text = readFileSync2(absPath, "utf-8");
4690
+ } catch {
4691
+ return null;
4692
+ }
4693
+ const lines2 = text.split(/\r?\n/);
4694
+ const idx = line - 1;
4695
+ if (idx < 0 || idx >= lines2.length) return null;
4696
+ const start = Math.max(0, idx - contextLines);
4697
+ const end = Math.min(lines2.length - 1, idx + contextLines);
4698
+ const out2 = [];
4699
+ for (let i = start; i <= end; i++) out2.push({ line: i + 1, text: lines2[i] ?? "" });
4700
+ return out2;
4701
+ }
4702
+ function renderContextWindow(displayFile, matchLine3, window, matchSuffix = "", indent = "") {
4703
+ return window.map(
4704
+ (c) => c.line === matchLine3 ? `${indent}${displayFile}:${c.line}: ${c.text}${matchSuffix}` : `${indent}${displayFile}-${c.line}- ${c.text}`
4705
+ );
4706
+ }
4676
4707
  function toKB(bytes) {
4677
4708
  return Math.round(bytes / 1024);
4678
4709
  }
@@ -4725,6 +4756,19 @@ function isCodeFenceDelimiter(line) {
4725
4756
  function pad(s, n) {
4726
4757
  return s.length >= n ? s : s + " ".repeat(n - s.length);
4727
4758
  }
4759
+ function installEpipeGuard(streams) {
4760
+ const targets = (streams ?? [process.stdout, process.stderr]).filter((s) => s !== void 0);
4761
+ for (const stream of targets) {
4762
+ stream.on("error", (err2) => {
4763
+ if (err2.code === "EPIPE") {
4764
+ process.exitCode = 0;
4765
+ return;
4766
+ }
4767
+ throw err2;
4768
+ });
4769
+ }
4770
+ return targets;
4771
+ }
4728
4772
  function normalizePathForwardSlash(p, toLowerCase) {
4729
4773
  let result = normalizePath(p).replace(/\\/g, "/");
4730
4774
  if (toLowerCase) result = result.toLowerCase();
@@ -5056,7 +5100,8 @@ function defaultConfig() {
5056
5100
  compression: getDefaultConfig("compression"),
5057
5101
  context: getDefaultConfig("context"),
5058
5102
  injection: getDefaultConfig("injection"),
5059
- hint_stats: getDefaultConfig("hint_stats")
5103
+ hint_stats: getDefaultConfig("hint_stats"),
5104
+ semantic: getDefaultConfig("semantic")
5060
5105
  };
5061
5106
  }
5062
5107
  function validatedBool(raw, def) {
@@ -5498,6 +5543,10 @@ function _buildConfig(raw, projectRaw = {}) {
5498
5543
  const hs = getDefaultConfig("hint_stats");
5499
5544
  hs.suppress_threshold_pct = validatedInt(hs_raw["suppress_threshold_pct"], hs.suppress_threshold_pct, ...boundsOf("hint_stats.suppress_threshold_pct"));
5500
5545
  hs.min_sample_size = validatedInt(hs_raw["min_sample_size"], hs.min_sample_size, ...boundsOf("hint_stats.min_sample_size"));
5546
+ const sem_raw = section(raw, "semantic");
5547
+ const sem = getDefaultConfig("semantic");
5548
+ sem.archive_weight = validatedFloat(sem_raw["archive_weight"], sem.archive_weight, ...boundsOf("semantic.archive_weight"));
5549
+ sem.docs_weight = validatedFloat(sem_raw["docs_weight"], sem.docs_weight, ...boundsOf("semantic.docs_weight"));
5501
5550
  return {
5502
5551
  compact_assist: ca,
5503
5552
  bash_compress: bc,
@@ -5520,7 +5569,8 @@ function _buildConfig(raw, projectRaw = {}) {
5520
5569
  compression: cpr,
5521
5570
  context: ctx,
5522
5571
  injection: inj,
5523
- hint_stats: hs
5572
+ hint_stats: hs,
5573
+ semantic: sem
5524
5574
  };
5525
5575
  }
5526
5576
  function saveConfig(config2) {
@@ -5677,6 +5727,10 @@ function saveConfig(config2) {
5677
5727
  hint_stats: {
5678
5728
  suppress_threshold_pct: config2.hint_stats.suppress_threshold_pct,
5679
5729
  min_sample_size: config2.hint_stats.min_sample_size
5730
+ },
5731
+ semantic: {
5732
+ archive_weight: config2.semantic.archive_weight,
5733
+ docs_weight: config2.semantic.docs_weight
5680
5734
  }
5681
5735
  };
5682
5736
  const toml = stringify(data);
@@ -5869,6 +5923,10 @@ var init_config = __esm({
5869
5923
  hint_stats: {
5870
5924
  suppress_threshold_pct: 15,
5871
5925
  min_sample_size: 5
5926
+ },
5927
+ semantic: {
5928
+ archive_weight: 0.7,
5929
+ docs_weight: 0.92
5872
5930
  }
5873
5931
  };
5874
5932
  NUMERIC_FIELD_BOUNDS = {
@@ -5932,7 +5990,9 @@ var init_config = __esm({
5932
5990
  "indexing.large_file_skip_kb": { min: 1, max: 1048576 },
5933
5991
  "context.model_window_tokens": { min: 1e4, max: 1e7 },
5934
5992
  "hint_stats.suppress_threshold_pct": { min: 0, max: 100 },
5935
- "hint_stats.min_sample_size": { min: 1, max: 1e4 }
5993
+ "hint_stats.min_sample_size": { min: 1, max: 1e4 },
5994
+ "semantic.archive_weight": { min: 0.05, max: 1 },
5995
+ "semantic.docs_weight": { min: 0.05, max: 1 }
5936
5996
  };
5937
5997
  ENUM_FIELD_VALUES = {
5938
5998
  "compression.profile": ["auto", "aggressive", "balanced", "minimal"],
@@ -17955,7 +18015,8 @@ function rerankHits(hits, query, topK) {
17955
18015
  boost = Math.min(matches2 * _VERBATIM_TOKEN_BOOST, _MAX_VERBATIM_BOOST);
17956
18016
  }
17957
18017
  const penalty = _isGeneratedPath(hit.filePath) ? _GENERATED_PATH_PENALTY : 0;
17958
- return { hit, index, adjusted: hit.distance - boost + penalty };
18018
+ const pathPenalty = _pathPriorityPenalty(hit.filePath);
18019
+ return { hit, index, adjusted: hit.distance - boost + penalty + pathPenalty };
17959
18020
  });
17960
18021
  scored.sort((a, b) => a.adjusted - b.adjusted || a.index - b.index);
17961
18022
  return scored.slice(0, topK).map((entry) => ({ ...entry.hit, adjustedDistance: entry.adjusted }));
@@ -18081,11 +18142,26 @@ function _isGeneratedPath(filePath) {
18081
18142
  }
18082
18143
  return false;
18083
18144
  }
18084
- 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;
18145
+ function _pathPriorityPenalty(filePath) {
18146
+ const segments = filePath.split(/[/\\]+/);
18147
+ const basename22 = segments[segments.length - 1] ?? filePath;
18148
+ const weights = loadConfig().semantic;
18149
+ const isArchive = _ARCHIVE_FILE_RE.test(basename22) || segments.some((seg) => _ARCHIVE_PATH_SEGMENTS.has(seg.toLowerCase()));
18150
+ if (isArchive) {
18151
+ return 1 - weights.archive_weight;
18152
+ }
18153
+ const isDocs = _DOCS_FILE_RE.test(basename22) || segments.some((seg) => seg.toLowerCase() === _DOCS_DIR_SEGMENT);
18154
+ if (isDocs) {
18155
+ return 1 - weights.docs_weight;
18156
+ }
18157
+ return 0;
18158
+ }
18159
+ 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;
18085
18160
  var init_embeddings = __esm({
18086
18161
  "src/embeddings.ts"() {
18087
18162
  "use strict";
18088
18163
  init_define_import_meta_env();
18164
+ init_config();
18089
18165
  init_sql_path();
18090
18166
  init_util2();
18091
18167
  init_reset();
@@ -18134,6 +18210,17 @@ var init_embeddings = __esm({
18134
18210
  ".ruff_cache"
18135
18211
  ]);
18136
18212
  _GENERATED_PATH_PENALTY = 0.5;
18213
+ _ARCHIVE_PATH_SEGMENTS = /* @__PURE__ */ new Set([
18214
+ "archive",
18215
+ "archived",
18216
+ "old",
18217
+ "deprecated",
18218
+ "plans",
18219
+ "drafts"
18220
+ ]);
18221
+ _ARCHIVE_FILE_RE = /(^changelog|\.bak$|\.orig$)/i;
18222
+ _DOCS_FILE_RE = /\.md$/i;
18223
+ _DOCS_DIR_SEGMENT = "docs";
18137
18224
  _VERBATIM_TOKEN_BOOST = 0.05;
18138
18225
  _MAX_VERBATIM_BOOST = 0.25;
18139
18226
  _TOKEN_RE = /\w+/g;
@@ -18676,14 +18763,32 @@ function fetchTopSymbols(limit, dbPath, rootDir) {
18676
18763
  try {
18677
18764
  const db = getDb(dbPath);
18678
18765
  const { clause, param } = projectScopeClause("file_path");
18766
+ const refScope = projectScopeClause("file_path");
18679
18767
  const rows = db.prepare(
18768
+ // 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.
18680
18769
  `SELECT file_path, name, kind, line_start, line_end, body, docstring, parent
18681
- FROM symbols
18682
- WHERE kind IN ('class', 'function', 'interface') AND ${clause}
18683
- ORDER BY CASE kind WHEN 'class' THEN 0 WHEN 'interface' THEN 1 ELSE 2 END,
18770
+ FROM (
18771
+ SELECT s.file_path, s.name, s.kind, s.line_start, s.line_end, s.body, s.docstring, s.parent,
18772
+ COALESCE(r.ref_count, 0) * 1.0 / COUNT(*) OVER (PARTITION BY s.name) AS score,
18773
+ ROW_NUMBER() OVER (
18774
+ PARTITION BY s.name
18775
+ ORDER BY LENGTH(COALESCE(s.body, '')) DESC, s.file_path
18776
+ ) AS rn
18777
+ FROM symbols s
18778
+ LEFT JOIN (
18779
+ SELECT name, COUNT(*) AS ref_count
18780
+ FROM refs
18781
+ WHERE ${refScope.clause}
18782
+ GROUP BY name
18783
+ ) r ON r.name = s.name
18784
+ WHERE s.kind IN ('class', 'function', 'interface') AND ${clause}
18785
+ )
18786
+ WHERE rn = 1
18787
+ ORDER BY score DESC,
18788
+ CASE kind WHEN 'class' THEN 0 WHEN 'interface' THEN 1 ELSE 2 END,
18684
18789
  LENGTH(COALESCE(body, '')) DESC
18685
18790
  LIMIT ?`
18686
- ).all(param(rootDir), limit);
18791
+ ).all(refScope.param(rootDir), param(rootDir), limit);
18687
18792
  return rows.map((r) => ({
18688
18793
  filePath: r.file_path,
18689
18794
  name: r.name,
@@ -18735,13 +18840,12 @@ function formatProjectMap(map3, compact = false) {
18735
18840
  lines2.push("");
18736
18841
  lines2.push("## Top symbols");
18737
18842
  for (const s of map3.topSymbols) {
18738
- if (compact) {
18739
- lines2.push(`- ${s.name} (${s.kind})`);
18740
- } else {
18741
- const loc = `${path25.basename(s.filePath)}:${s.lineStart}-${s.lineEnd}`;
18742
- lines2.push(`- ${s.name} (${s.kind}) \u2014 ${loc}`);
18743
- }
18843
+ const loc = `${toDisplayPath(map3.rootDir, s.filePath)}:${s.lineStart}-${s.lineEnd}`;
18844
+ lines2.push(`- ${s.name} (${s.kind}) \u2014 ${loc}`);
18744
18845
  }
18846
+ } else {
18847
+ lines2.push("");
18848
+ lines2.push("## Top symbols: none \u2014 no files indexed for this project; run 'token-goat index .'");
18745
18849
  }
18746
18850
  if (!compact && map3.recentFiles.length > 0) {
18747
18851
  lines2.push("");
@@ -25762,6 +25866,11 @@ function isDeadSymbol(name2, refCount) {
25762
25866
  if (ENTRY_NAMES.has(name2)) return false;
25763
25867
  return refCount === 0;
25764
25868
  }
25869
+ function parseGraphSymbolSpec(spec) {
25870
+ const colonIdx = findSpecSeparator(spec);
25871
+ if (colonIdx === -1) return { name: spec };
25872
+ return { name: spec.slice(colonIdx + 2), file: spec.slice(0, colonIdx) };
25873
+ }
25765
25874
  function buildFileSymCache() {
25766
25875
  const cache = /* @__PURE__ */ new Map();
25767
25876
  return (fp) => {
@@ -25779,9 +25888,10 @@ function fileDefinesName(fp, name2, getSyms) {
25779
25888
  function filterRefsForSymbol(refs, name2, filePath, getSyms) {
25780
25889
  return refs.filter((ref2) => ref2.filePath === filePath || !fileDefinesName(ref2.filePath, name2, getSyms));
25781
25890
  }
25782
- function resolveCallers(name2, limit, filePath, rootDir) {
25891
+ function resolveCallers(name2, limit, filePath, rootDir, excludeTests) {
25783
25892
  const resolvedRootDir = rootDir ?? resolveProjectRoot({ project: process.cwd() });
25784
- const refs = queryRefs({ name: name2, limit: limit ?? 500, rootDir: resolvedRootDir });
25893
+ const queryLimit = excludeTests === true ? UNBOUNDED_REF_LIMIT : limit ?? 500;
25894
+ const refs = queryRefs({ name: name2, limit: queryLimit, rootDir: resolvedRootDir });
25785
25895
  const getSyms = buildFileSymCache();
25786
25896
  const scoped = filePath === void 0 ? refs : filterRefsForSymbol(refs, name2, filePath, getSyms);
25787
25897
  return scoped.map((ref2) => {
@@ -25800,17 +25910,37 @@ function runCallers(opts) {
25800
25910
  return 1;
25801
25911
  }
25802
25912
  const rootDir = resolveProjectRoot({ project: process.cwd() });
25803
- const entries = resolveCallers(opts.symbol, opts.limit, void 0, rootDir);
25913
+ const { name: name2, file: file2 } = parseGraphSymbolSpec(opts.symbol);
25914
+ const fileHint = file2 !== void 0 ? resolveIndexPath(file2, rootDir) : void 0;
25915
+ if (fileHint !== void 0 && querySymbols({ name: name2, filePath: fileHint, limit: 1 }).length === 0) {
25916
+ emitErr(`Symbol '${name2}' not found in '${file2}'`);
25917
+ return 1;
25918
+ }
25919
+ const resolved = resolveCallers(name2, opts.limit, fileHint, rootDir, opts.excludeTests);
25920
+ const suppressed = opts.excludeTests === true ? resolved.filter((e) => isTestFile(e.file)).length : 0;
25921
+ const entries = opts.excludeTests === true ? resolved.filter((e) => !isTestFile(e.file)).slice(0, opts.limit ?? 500) : resolved;
25804
25922
  if (entries.length === 0) {
25923
+ if (opts.excludeTests === true && suppressed > 0) {
25924
+ emitErr(`No non-test references found for '${opts.symbol}' (${suppressed} in test files hidden by --exclude-tests)`);
25925
+ return 1;
25926
+ }
25805
25927
  emitErr(`No references found for '${opts.symbol}'`);
25806
25928
  return 1;
25807
25929
  }
25930
+ const contextLines = opts.context ?? 0;
25808
25931
  if (opts.json === true) {
25809
- emit2(JSON.stringify(entries, null, 2));
25932
+ const payload = contextLines > 0 ? entries.map((e) => ({ ...e, contextLines: buildContextWindow(e.file, e.line, contextLines) ?? [] })) : entries;
25933
+ emit2(JSON.stringify(payload, null, 2));
25810
25934
  return 0;
25811
25935
  }
25936
+ if (opts.excludeTests === true && suppressed > 0) {
25937
+ emit2(`${entries.length} callers found (${suppressed} in test files hidden by --exclude-tests)`);
25938
+ }
25812
25939
  for (const e of entries) {
25813
- emit2(`${e.caller} ${toDisplayPath(rootDir, e.file)}:${e.line}`);
25940
+ const displayPath = toDisplayPath(rootDir, e.file);
25941
+ emit2(`${e.caller} ${displayPath}:${e.line}`);
25942
+ const window = buildContextWindow(e.file, e.line, contextLines);
25943
+ if (window !== null) for (const l of renderContextWindow(displayPath, e.line, window, "", " ")) emit2(l);
25814
25944
  }
25815
25945
  return 0;
25816
25946
  }
@@ -25821,28 +25951,36 @@ function runCallChain(opts) {
25821
25951
  }
25822
25952
  const maxDepth = opts.depth ?? 8;
25823
25953
  const rootDir = resolveProjectRoot({ project: process.cwd() });
25824
- if (querySymbols({ name: opts.symbol, rootDir, limit: 1 }).length === 0) {
25954
+ const { name: name2, file: file2 } = parseGraphSymbolSpec(opts.symbol);
25955
+ const fileHint = file2 !== void 0 ? resolveIndexPath(file2, rootDir) : void 0;
25956
+ if (fileHint !== void 0) {
25957
+ if (querySymbols({ name: name2, filePath: fileHint, limit: 1 }).length === 0) {
25958
+ emitErr(`Symbol '${name2}' not found in '${file2}'`);
25959
+ return 1;
25960
+ }
25961
+ } else if (querySymbols({ name: name2, rootDir, limit: 1 }).length === 0) {
25825
25962
  emitErr(`Symbol not found: ${opts.symbol}`);
25826
25963
  return 1;
25827
25964
  }
25828
25965
  const getSyms = buildFileSymCache();
25829
- const callersOf = (name2) => {
25830
- const refs = queryRefs({ name: name2, limit: DEFAULT_REF_QUERY_LIMIT, rootDir });
25966
+ const callersOf = (n) => {
25967
+ const refs = queryRefs({ name: n, limit: DEFAULT_REF_QUERY_LIMIT, rootDir });
25831
25968
  if (refs.length === 0) return [];
25969
+ const scoped = fileHint !== void 0 && n === name2 ? filterRefsForSymbol(refs, n, fileHint, getSyms) : refs;
25832
25970
  const names = /* @__PURE__ */ new Set();
25833
- for (const ref2 of refs) {
25971
+ for (const ref2 of scoped) {
25834
25972
  const enc = enclosingSymbol(getSyms(ref2.filePath), ref2.line);
25835
25973
  if (enc !== null) names.add(enc.name);
25836
25974
  }
25837
25975
  return [...names];
25838
25976
  };
25839
- const chains = bfsCallChains(opts.symbol, callersOf, maxDepth);
25977
+ const chains = bfsCallChains(name2, callersOf, maxDepth);
25840
25978
  if (opts.json === true) {
25841
25979
  emit2(JSON.stringify({ chains }, null, 2));
25842
25980
  return 0;
25843
25981
  }
25844
- if (chains.length === 1 && chains[0]?.length === 1 && chains[0][0] === opts.symbol) {
25845
- emit2(`${opts.symbol} (no callers)`);
25982
+ if (chains.length === 1 && chains[0]?.length === 1 && chains[0][0] === name2) {
25983
+ emit2(`${name2} (no callers)`);
25846
25984
  return 0;
25847
25985
  }
25848
25986
  for (const chain2 of chains) {
@@ -25862,16 +26000,23 @@ function runImpact(opts) {
25862
26000
  const top = opts.top ?? 20;
25863
26001
  const DEPTH_CAP = 8;
25864
26002
  const rootDir = resolveProjectRoot({ project: process.cwd() });
26003
+ const { name: rootName, file: file2 } = parseGraphSymbolSpec(opts.symbol);
26004
+ const fileHint = file2 !== void 0 ? resolveIndexPath(file2, rootDir) : void 0;
26005
+ if (fileHint !== void 0 && querySymbols({ name: rootName, filePath: fileHint, limit: 1 }).length === 0) {
26006
+ emitErr(`Symbol '${rootName}' not found in '${file2}'`);
26007
+ return 1;
26008
+ }
25865
26009
  const getSyms = buildFileSymCache();
25866
- const hops = /* @__PURE__ */ new Map([[opts.symbol, 0]]);
25867
- const queue = [[opts.symbol, 0]];
26010
+ const hops = /* @__PURE__ */ new Map([[rootName, 0]]);
26011
+ const queue = [[rootName, 0]];
25868
26012
  while (queue.length > 0) {
25869
26013
  const item = queue.shift();
25870
26014
  if (item === void 0) break;
25871
26015
  const [name2, depth] = item;
25872
26016
  if (depth >= DEPTH_CAP) continue;
25873
26017
  const refs = queryRefs({ name: name2, limit: DEFAULT_REF_QUERY_LIMIT, rootDir });
25874
- for (const ref2 of refs) {
26018
+ const scoped = fileHint !== void 0 && name2 === rootName ? filterRefsForSymbol(refs, name2, fileHint, getSyms) : refs;
26019
+ for (const ref2 of scoped) {
25875
26020
  const newHop = depth + 1;
25876
26021
  const enc = enclosingSymbol(getSyms(ref2.filePath), ref2.line);
25877
26022
  if (enc === null) {
@@ -25888,7 +26033,7 @@ function runImpact(opts) {
25888
26033
  }
25889
26034
  }
25890
26035
  }
25891
- hops.delete(opts.symbol);
26036
+ hops.delete(rootName);
25892
26037
  const sorted = [...hops.entries()].sort(compareHopEntries).slice(0, top);
25893
26038
  if (sorted.length === 0) {
25894
26039
  emitErr(`No callers found for '${opts.symbol}'`);
@@ -25940,6 +26085,7 @@ function runDead(opts) {
25940
26085
  const syms = querySymbols({ kind, limit: 5e3, rootDir });
25941
26086
  const getSyms = buildFileSymCache();
25942
26087
  const results = [];
26088
+ let suppressed = 0;
25943
26089
  for (const sym of syms) {
25944
26090
  if (opts.includePrivate !== true && sym.name.startsWith("_")) continue;
25945
26091
  const refs = queryRefs({ name: sym.name, limit: DEFAULT_REF_QUERY_LIMIT, rootDir });
@@ -25949,6 +26095,10 @@ function runDead(opts) {
25949
26095
  const ownScope = enclosingNamedScope(getSyms(sym.filePath), sym.lineStart);
25950
26096
  if (ownScope !== null && hasAncestorDispatchRef(sym.name, ownScope.name, sym.filePath, rootDir)) continue;
25951
26097
  }
26098
+ if (opts.excludeTests === true && isTestFile(sym.filePath)) {
26099
+ suppressed += 1;
26100
+ continue;
26101
+ }
25952
26102
  results.push({ name: sym.name, kind: sym.kind, file: sym.filePath, line: sym.lineStart });
25953
26103
  }
25954
26104
  const sliced = results.slice(0, opts.top ?? results.length);
@@ -25957,9 +26107,16 @@ function runDead(opts) {
25957
26107
  return 0;
25958
26108
  }
25959
26109
  if (sliced.length === 0) {
25960
- emit2("No dead symbols found.");
26110
+ if (opts.excludeTests === true && suppressed > 0) {
26111
+ emit2(`No dead symbols found (${suppressed} in test files hidden by --exclude-tests).`);
26112
+ } else {
26113
+ emit2("No dead symbols found.");
26114
+ }
25961
26115
  return 0;
25962
26116
  }
26117
+ if (opts.excludeTests === true && suppressed > 0) {
26118
+ emit2(`${sliced.length} dead symbols (${suppressed} in test files hidden by --exclude-tests)`);
26119
+ }
25963
26120
  for (const r of sliced) {
25964
26121
  emit2(`${r.name} ${toDisplayPath(rootDir, r.file)}:${r.line}`);
25965
26122
  }
@@ -26209,16 +26366,9 @@ function runSimilar(opts) {
26209
26366
  emitErr(`Invalid spec - expected "file::symbol", got: ${opts.spec}`);
26210
26367
  return 1;
26211
26368
  }
26212
- const fileArg = opts.spec.slice(0, sepIdx);
26213
- const symbolArg = opts.spec.slice(sepIdx + 2);
26214
26369
  const top = opts.top ?? 10;
26215
- const filePath = resolveIndexPath(fileArg);
26216
- const anchors = querySymbols({ name: symbolArg, filePath });
26217
- if (anchors.length === 0) {
26218
- emitErr(`Symbol '${symbolArg}' not found in '${fileArg}'`);
26219
- return 1;
26220
- }
26221
- const anchor = anchors[0];
26370
+ const anchor = resolveSymbolSpecOrEmitError("similar", opts.spec, void 0);
26371
+ if (anchor === null) return 1;
26222
26372
  const words = [anchor.name, ...(anchor.docstring ?? "").split(/\s+/).filter((w) => w.length > 4)];
26223
26373
  const query = words.slice(0, 8).join(" ");
26224
26374
  const rootDir = resolveProjectRoot({ project: process.cwd() });
@@ -26250,13 +26400,13 @@ function runContextFor(opts) {
26250
26400
  const bodyTokens = estimateTokens(h.body ?? "");
26251
26401
  if (budget !== void 0 && tokensSoFar + bodyTokens > budget) continue;
26252
26402
  tokensSoFar += bodyTokens;
26253
- entries.push({ file: h.filePath, symbol: h.name, kind: h.kind, readCmd: `token-goat read "${h.filePath}::${h.name}"` });
26403
+ entries.push({ file: h.filePath, symbol: h.name, kind: h.kind, line: h.lineStart, readCmd: `token-goat read "${h.filePath}::${h.name}@${h.lineStart}"` });
26254
26404
  }
26255
26405
  if (opts.json === true) {
26256
26406
  emit2(JSON.stringify(entries, null, 2));
26257
26407
  return 0;
26258
26408
  }
26259
- for (const e of entries) emit2(`token-goat read "${toDisplayPath(rootDir, e.file)}::${e.symbol}"`);
26409
+ for (const e of entries) emit2(`token-goat read "${toDisplayPath(rootDir, e.file)}::${e.symbol}@${e.line}"`);
26260
26410
  return 0;
26261
26411
  }
26262
26412
  function runTestFor(opts) {
@@ -26401,16 +26551,10 @@ function runBlame(opts) {
26401
26551
  emitErr(`Invalid spec - expected "file::symbol", got: ${opts.spec}`);
26402
26552
  return 1;
26403
26553
  }
26404
- const fileArg = opts.spec.slice(0, sepIdx);
26405
- const symbolArg = opts.spec.slice(sepIdx + 2);
26406
26554
  const cwd = opts.cwd ?? process.cwd();
26407
- const filePath = resolveIndexPath(fileArg);
26408
- const syms = querySymbols({ name: symbolArg, filePath });
26409
- if (syms.length === 0) {
26410
- emitErr(`Symbol '${symbolArg}' not found in '${fileArg}'`);
26411
- return 1;
26412
- }
26413
- const sym = syms[0];
26555
+ const sym = resolveSymbolSpecOrEmitError("blame", opts.spec, void 0);
26556
+ if (sym === null) return 1;
26557
+ const filePath = sym.filePath;
26414
26558
  const start = sym.lineStart;
26415
26559
  const end = sym.lineEnd;
26416
26560
  let raw;
@@ -26431,10 +26575,10 @@ function runBlame(opts) {
26431
26575
  if (!m) return { raw: l };
26432
26576
  return { commit: m[1], author: (m[2] ?? "").trim(), date: (m[3] ?? "").trim(), line: Number.parseInt(m[4] ?? "0", 10), content: m[5] };
26433
26577
  });
26434
- emit2(JSON.stringify({ symbol: symbolArg, file: filePath, lines: lines2 }, null, 2));
26578
+ emit2(JSON.stringify({ symbol: sym.name, file: filePath, lines: lines2 }, null, 2));
26435
26579
  return 0;
26436
26580
  }
26437
- emit2(`${symbolArg} ${toDisplayPath(getDisplayRoot(opts.cwd), filePath)}:${start}-${end}`);
26581
+ emit2(`${sym.name} ${toDisplayPath(getDisplayRoot(opts.cwd), filePath)}:${start}-${end}`);
26438
26582
  emit2(raw.trim());
26439
26583
  return 0;
26440
26584
  }
@@ -26448,14 +26592,14 @@ function runAsk(opts) {
26448
26592
  const hits = searchSymbolsFts(opts.question, top, void 0, rootDir);
26449
26593
  const BACKEND_ENV = "TOKEN_GOAT_ASK_BACKEND";
26450
26594
  const backendLabel = process.env[BACKEND_ENV] ?? "";
26451
- const entries = hits.map((h) => ({ file: h.filePath, symbol: h.name, kind: h.kind, readCmd: `token-goat read "${h.filePath}::${h.name}"` }));
26595
+ 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}"` }));
26452
26596
  const degrade = () => {
26453
26597
  if (opts.json === true) {
26454
26598
  emit2(JSON.stringify({ degraded: true, note: `Set ${BACKEND_ENV}=claude|codex for LLM synthesis`, context: entries }, null, 2));
26455
26599
  return 0;
26456
26600
  }
26457
26601
  emit2(`[degraded mode - set ${BACKEND_ENV}=claude|codex for LLM synthesis]`);
26458
- for (const e of entries) emit2(`token-goat read "${toDisplayPath(rootDir, e.file)}::${e.symbol}"`);
26602
+ for (const e of entries) emit2(`token-goat read "${toDisplayPath(rootDir, e.file)}::${e.symbol}@${e.line}"`);
26459
26603
  return 0;
26460
26604
  };
26461
26605
  if (!backendLabel) return degrade();
@@ -30438,6 +30582,42 @@ function formatBareNameSpecError(command, name2, projectRoot) {
30438
30582
  }
30439
30583
  return lines2.join("\n");
30440
30584
  }
30585
+ function formatCrossFileLead(command, name2, excludeFilePath, projectRoot) {
30586
+ const rootDir = projectRoot ?? process.cwd();
30587
+ const matches2 = querySymbols({ name: name2, limit: 50, rootDir });
30588
+ const excludeResolved = resolveIndexPath(excludeFilePath, rootDir);
30589
+ const seen = /* @__PURE__ */ new Set();
30590
+ const specs = [];
30591
+ for (const m of matches2) {
30592
+ if (foldPath(m.filePath) === foldPath(excludeResolved)) continue;
30593
+ const spec = `${toDisplayPath(rootDir, m.filePath)}::${m.name}`;
30594
+ if (seen.has(spec)) continue;
30595
+ seen.add(spec);
30596
+ specs.push(spec);
30597
+ }
30598
+ if (specs.length === 0) return "";
30599
+ const firstSpec = specs[0];
30600
+ const lines2 = [`'${name2}' is defined in ${firstSpec !== void 0 ? firstSpec.split("::")[0] : ""}`];
30601
+ for (const spec of specs.slice(0, DIDYOUMEAN_LIMIT)) {
30602
+ lines2.push(` - token-goat ${command} "${spec}"`);
30603
+ }
30604
+ if (specs.length > DIDYOUMEAN_LIMIT) {
30605
+ lines2.push(` (${specs.length - DIDYOUMEAN_LIMIT} more not shown)`);
30606
+ }
30607
+ return lines2.join("\n");
30608
+ }
30609
+ function resolveEnclosingSymbol(filePath, chunkStartLine) {
30610
+ const symbols = querySymbols({ filePath, limit: 1e5 }, globalDbPath());
30611
+ let best = null;
30612
+ for (const s of symbols) {
30613
+ if (s.lineStart <= chunkStartLine && chunkStartLine <= s.lineEnd) {
30614
+ if (best === null || s.lineEnd - s.lineStart < best.lineEnd - best.lineStart) {
30615
+ best = s;
30616
+ }
30617
+ }
30618
+ }
30619
+ return best === null ? null : { name: best.name, kind: best.kind };
30620
+ }
30441
30621
  function trimBlankLines(lines2) {
30442
30622
  let start = 0;
30443
30623
  let end = lines2.length;
@@ -30533,10 +30713,21 @@ function parseCrossFileMultiSpec(spec) {
30533
30713
  }
30534
30714
  return pairs.length > 1 ? pairs : null;
30535
30715
  }
30716
+ function parseMultiFileSpec(spec) {
30717
+ if (!spec.includes(",")) return null;
30718
+ if (fileExists(spec)) return null;
30719
+ if (findSpecSeparator(spec) !== -1) return null;
30720
+ const parts = spec.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
30721
+ return parts.length > 1 ? parts : null;
30722
+ }
30723
+ function extraFileArgsNote(command, first2, extras) {
30724
+ 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(",")}"`;
30725
+ }
30536
30726
  function parseLineRange(spec) {
30537
30727
  const m = /^(.+)@(\d+)(?:-(\d+))?$/.exec(spec);
30538
30728
  if (m === null) return null;
30539
30729
  if (fileExists(spec)) return null;
30730
+ if (m[1].includes("::")) return null;
30540
30731
  const start = parseInt(m[2], 10);
30541
30732
  const end = m[3] !== void 0 ? parseInt(m[3], 10) : start;
30542
30733
  return { file: m[1], start, end };
@@ -30595,30 +30786,49 @@ function findParentName(entry, fileSymbols) {
30595
30786
  if (doc !== "" && PARENT_IDENTIFIER_RE.test(doc)) return doc;
30596
30787
  return null;
30597
30788
  }
30598
- function formatAmbiguity(symbol3, file2, candidates, explicitRoot) {
30789
+ function formatAmbiguity(symbol3, file2, candidates, explicitRoot, commandName = "read") {
30599
30790
  const multiFile = new Set(candidates.map((c) => c.filePath)).size > 1;
30600
30791
  const displayRoot = getDisplayRoot(explicitRoot);
30601
30792
  const lines2 = [
30602
30793
  `Ambiguous symbol '${symbol3}' in '${file2}': ${candidates.length} definitions match. Retry with one of the qualified commands below to pick one:`
30603
30794
  ];
30604
30795
  const fileSymCache = /* @__PURE__ */ new Map();
30605
- for (const c of candidates) {
30606
- let fileSyms = fileSymCache.get(c.filePath);
30796
+ const getFileSyms = (filePath) => {
30797
+ let fileSyms = fileSymCache.get(filePath);
30607
30798
  if (fileSyms === void 0) {
30608
- fileSyms = querySymbols({ filePath: c.filePath, limit: 1e3 });
30609
- fileSymCache.set(c.filePath, fileSyms);
30799
+ fileSyms = querySymbols({ filePath, limit: 1e3 });
30800
+ fileSymCache.set(filePath, fileSyms);
30610
30801
  }
30611
- const parent = findParentName(c, fileSyms);
30612
- const qualifier = parent !== null ? `${parent}.${symbol3}` : symbol3;
30802
+ return fileSyms;
30803
+ };
30804
+ const parents = candidates.map((c) => findParentName(c, getFileSyms(c.filePath)));
30805
+ const plainQualifiers = candidates.map((c, i) => parents[i] !== null ? `${parents[i]}.${symbol3}` : symbol3);
30806
+ const qualifierCounts = /* @__PURE__ */ new Map();
30807
+ const fileGroupSize = /* @__PURE__ */ new Map();
30808
+ for (let i = 0; i < candidates.length; i++) {
30809
+ const c = candidates[i];
30810
+ const key = `${c.filePath} ${plainQualifiers[i]}`;
30811
+ qualifierCounts.set(key, (qualifierCounts.get(key) ?? 0) + 1);
30812
+ fileGroupSize.set(c.filePath, (fileGroupSize.get(c.filePath) ?? 0) + 1);
30813
+ }
30814
+ for (let i = 0; i < candidates.length; i++) {
30815
+ const c = candidates[i];
30816
+ const parent = parents[i];
30817
+ const plainQualifier = plainQualifiers[i];
30818
+ const collides = (qualifierCounts.get(`${c.filePath} ${plainQualifier}`) ?? 0) > 1 || parent === null && (fileGroupSize.get(c.filePath) ?? 0) > 1;
30819
+ const qualifier = collides ? `${plainQualifier}@${c.lineStart}` : plainQualifier;
30613
30820
  const retryFile = multiFile ? toDisplayPath(displayRoot, c.filePath) : file2;
30614
30821
  const label = multiFile ? `${toDisplayPath(displayRoot, c.filePath)}::${qualifier}` : qualifier;
30615
- lines2.push(` - ${label} (line ${c.lineStart}) -> token-goat read "${retryFile}::${qualifier}"`);
30822
+ lines2.push(` - ${label} (line ${c.lineStart}) -> token-goat ${commandName} "${retryFile}::${qualifier}"`);
30616
30823
  }
30617
30824
  return lines2.join("\n");
30618
30825
  }
30619
30826
  function resolveSymbolSpec(spec, forceRefresh, projectRoot) {
30620
- const { file: file2, symbol: symbol3 } = parseReadSpec(spec);
30621
- if (symbol3 === void 0 || symbol3 === "") return { kind: "none" };
30827
+ const { file: file2, symbol: rawSymbol } = parseReadSpec(spec);
30828
+ if (rawSymbol === void 0 || rawSymbol === "") return { kind: "none" };
30829
+ const anchorMatch = /^(.+)@(\d+)$/.exec(rawSymbol);
30830
+ const symbol3 = anchorMatch !== null ? anchorMatch[1] : rawSymbol;
30831
+ const lineAnchor = anchorMatch !== null ? parseInt(anchorMatch[2], 10) : void 0;
30622
30832
  const resolved = resolveIndexPath(file2, projectRoot ?? process.cwd());
30623
30833
  if (forceRefresh === true) {
30624
30834
  indexFileSync(resolved, globalDbPath());
@@ -30635,9 +30845,10 @@ function resolveSymbolSpec(spec, forceRefresh, projectRoot) {
30635
30845
  seen.add(key);
30636
30846
  distinct.push(c);
30637
30847
  }
30638
- if (distinct.length === 0) return { kind: "none" };
30639
- if (distinct.length === 1) return { kind: "ok", entry: distinct[0] };
30640
- return { kind: "ambiguous", symbol: displaySymbol, file: file2, candidates: distinct };
30848
+ const anchored = lineAnchor === void 0 ? distinct : distinct.filter((c) => c.lineStart === lineAnchor);
30849
+ if (anchored.length === 0) return { kind: "none" };
30850
+ if (anchored.length === 1) return { kind: "ok", entry: anchored[0] };
30851
+ return { kind: "ambiguous", symbol: displaySymbol, file: file2, candidates: anchored };
30641
30852
  };
30642
30853
  if (symbol3.includes(".")) {
30643
30854
  const exactMatch = querySymbols({ name: symbol3, filePath: resolved, limit: 10 });
@@ -30717,6 +30928,8 @@ function runRead(opts) {
30717
30928
  return runLineRange({ file: file2, start: lineSpec.start, end: lineSpec.end }, opts);
30718
30929
  }
30719
30930
  const messages = [`Symbol '${symbol3}' not found in '${file2}'`];
30931
+ const crossFileLead = formatCrossFileLead("read", symbol3, file2, opts.projectRoot);
30932
+ if (crossFileLead !== "") messages.push(crossFileLead);
30720
30933
  const resolved = resolveIndexPath(file2, opts.projectRoot ?? process.cwd());
30721
30934
  const closes = querySymbols({ filePath: resolved, limit: DIDYOUMEAN_LIMIT }).map((s) => s.name);
30722
30935
  if (closes.length > 0) messages.push(didYouMean(closes));
@@ -30777,6 +30990,8 @@ ${sub.text}`);
30777
30990
  return { text, code: 1 };
30778
30991
  }
30779
30992
  function runSection(opts) {
30993
+ const crossFilePairs = parseCrossFileMultiSpec(opts.spec);
30994
+ if (crossFilePairs !== null) return runSectionCrossFile(crossFilePairs, opts);
30780
30995
  const colonIdx = findSpecSeparator(opts.spec);
30781
30996
  if (colonIdx === -1) {
30782
30997
  return { text: `Invalid section spec \u2014 expected "file::Heading", got: ${opts.spec}`, code: 1 };
@@ -30833,6 +31048,42 @@ ${sub.text}`);
30833
31048
  if (anyFound) recordReadStat("section_read", fullSourceBytes, text, opts.spec);
30834
31049
  return { text, code: anyFound ? 0 : 1 };
30835
31050
  }
31051
+ function runSectionCrossFile(pairs, opts) {
31052
+ let anyFound = false;
31053
+ const jsonOut = {};
31054
+ const textBlocks = [];
31055
+ const distinctFiles = new Set(pairs.map((p) => p.file));
31056
+ const keyFor = (p) => distinctFiles.size === 1 ? p.symbol : `${p.file}::${p.symbol}`;
31057
+ for (const { file: file2, symbol: heading } of pairs) {
31058
+ const sub = runSection({ ...opts, spec: `${file2}::${heading}`, suppressStat: true });
31059
+ if (sub.code === 0) anyFound = true;
31060
+ const key = keyFor({ file: file2, symbol: heading });
31061
+ if (opts.json === true) {
31062
+ jsonOut[key] = sub.code === 0 ? JSON.parse(sub.text) : { error: sub.text };
31063
+ continue;
31064
+ }
31065
+ textBlocks.push(`${key}:
31066
+ ${sub.text}`);
31067
+ }
31068
+ const resolvePath = (f) => opts.projectRoot !== void 0 && !path48.isAbsolute(f) ? path48.resolve(opts.projectRoot, f) : f;
31069
+ const text = opts.json === true ? JSON.stringify(jsonOut, null, 2) : textBlocks.join("\n\n");
31070
+ if (anyFound) {
31071
+ const fullSourceBytes = sumFileSizes(Array.from(distinctFiles, resolvePath));
31072
+ recordReadStat("section_read", fullSourceBytes, text, opts.spec);
31073
+ }
31074
+ return { text, code: anyFound ? 0 : 1 };
31075
+ }
31076
+ function renderRefLines(ref2, displayRoot, contextLines, indent = " ") {
31077
+ const displayPath = toDisplayPath(displayRoot, ref2.filePath);
31078
+ const base = `${indent}${displayPath}:${ref2.line}: ${ref2.context}`;
31079
+ const window = buildContextWindow(ref2.filePath, ref2.line, contextLines);
31080
+ if (window === null) return [base];
31081
+ return [base, ...renderContextWindow(displayPath, ref2.line, window, "", `${indent} `)];
31082
+ }
31083
+ function withContextLines(items, contextLines) {
31084
+ if (!(contextLines > 0)) return items;
31085
+ return items.map((r) => ({ ...r, contextLines: buildContextWindow(r.filePath, r.line, contextLines) ?? [] }));
31086
+ }
30836
31087
  function applyTypedRefsTier(symName, file2, results) {
30837
31088
  if (results.length === 0) return results;
30838
31089
  try {
@@ -30870,6 +31121,8 @@ function runRefs(opts) {
30870
31121
  emitErr2(`--top must be a positive number, got: ${opts.top}`);
30871
31122
  return 1;
30872
31123
  }
31124
+ const crossFilePairs = parseCrossFileMultiSpec(opts.spec);
31125
+ if (crossFilePairs !== null) return runRefsCrossFile(crossFilePairs, opts);
30873
31126
  const { file: file2, symbols } = parseMultiRefsSpec(opts.spec);
30874
31127
  if (symbols.length <= 1) return runRefsSingle(opts);
30875
31128
  const jsonOut = {};
@@ -30878,10 +31131,18 @@ function runRefs(opts) {
30878
31131
  const refFilePaths = [];
30879
31132
  for (const sym of symbols) {
30880
31133
  const queryOpts = { name: sym };
30881
- if (file2 !== void 0 && opts.callers !== true) queryOpts.filePath = resolveIndexPath(file2);
30882
- if (opts.limit !== void 0) queryOpts.limit = opts.limit;
31134
+ if (opts.excludeTests === true) queryOpts.limit = REFS_TOP_SCAN_LIMIT;
31135
+ else if (opts.limit !== void 0) queryOpts.limit = opts.limit;
30883
31136
  else if (opts.top !== void 0) queryOpts.limit = REFS_TOP_SCAN_LIMIT;
30884
- const results = applyTypedRefsTier(sym, file2, queryRefs(queryOpts));
31137
+ let results = applyTypedRefsTier(sym, file2, queryRefs(queryOpts));
31138
+ let suppressed = 0;
31139
+ let filteredTotal;
31140
+ if (opts.excludeTests === true) {
31141
+ const f = applyExcludeTestsFilter(results);
31142
+ suppressed = f.suppressed;
31143
+ filteredTotal = f.refs.length;
31144
+ results = opts.top !== void 0 ? f.refs : f.refs.slice(0, opts.limit ?? 100);
31145
+ }
30885
31146
  if (results.length > 0) anyFound = true;
30886
31147
  refFilePaths.push(...results.map((r) => r.filePath));
30887
31148
  if (opts.json === true) {
@@ -30889,22 +31150,85 @@ function runRefs(opts) {
30889
31150
  jsonOut[sym] = topFilesJsonPayload(results, opts.top);
30890
31151
  } else {
30891
31152
  const capped = guardJsonRows(results);
30892
- const trueTotal = countRefs(queryOpts);
30893
- jsonOut[sym] = { items: capped.items, truncated: capped.truncated || trueTotal > results.length, totalCount: trueTotal };
31153
+ const trueTotal = opts.excludeTests === true ? filteredTotal ?? results.length : countRefs(queryOpts);
31154
+ jsonOut[sym] = { items: withContextLines(capped.items, opts.context ?? 0), truncated: capped.truncated || trueTotal > results.length, totalCount: trueTotal };
30894
31155
  }
30895
31156
  continue;
30896
31157
  }
30897
31158
  if (results.length === 0) {
30898
- lines2.push(`${sym}: (no references found)`);
31159
+ 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)`);
30899
31160
  continue;
30900
31161
  }
30901
31162
  lines2.push(`${sym}:`);
30902
31163
  if (opts.top !== void 0) {
30903
- lines2.push(...renderTopFilesSummary(results, opts.top));
31164
+ lines2.push(...renderTopFilesSummary(results, opts.top, void 0, suppressed));
30904
31165
  } else if (opts.callers === true) {
30905
- lines2.push(...renderCallerGroups(results));
31166
+ if (opts.excludeTests === true && suppressed > 0) lines2.push(` ${results.length} references (${suppressed} in test files hidden by --exclude-tests)`);
31167
+ lines2.push(...renderCallerGroups(results, void 0, opts.context ?? 0));
30906
31168
  } else {
30907
- for (const ref2 of results) lines2.push(` ${ref2.filePath}:${ref2.line}: ${ref2.context}`);
31169
+ if (opts.excludeTests === true && suppressed > 0) lines2.push(` ${results.length} references (${suppressed} in test files hidden by --exclude-tests)`);
31170
+ for (const ref2 of results) lines2.push(...renderRefLines(ref2, void 0, opts.context ?? 0));
31171
+ }
31172
+ }
31173
+ const fullSourceBytes = sumFileSizes(refFilePaths);
31174
+ if (opts.json === true) {
31175
+ const text2 = JSON.stringify(jsonOut, null, 2);
31176
+ emit3(text2);
31177
+ if (anyFound) recordReadStat("symbol_read", fullSourceBytes, text2, opts.spec);
31178
+ return anyFound ? 0 : 1;
31179
+ }
31180
+ const text = lines2.join("\n");
31181
+ emitGuarded(text, "symbol");
31182
+ if (anyFound) recordReadStat("symbol_read", fullSourceBytes, text, opts.spec);
31183
+ return anyFound ? 0 : 1;
31184
+ }
31185
+ function runRefsCrossFile(pairs, opts) {
31186
+ const distinctFiles = new Set(pairs.map((p) => p.file));
31187
+ const keyFor = (p) => distinctFiles.size === 1 ? p.symbol : `${p.file}::${p.symbol}`;
31188
+ const jsonOut = {};
31189
+ let anyFound = false;
31190
+ const lines2 = [];
31191
+ const refFilePaths = [];
31192
+ for (const { file: file2, symbol: symbol3 } of pairs) {
31193
+ const key = keyFor({ file: file2, symbol: symbol3 });
31194
+ const queryOpts = { name: symbol3 };
31195
+ if (opts.excludeTests === true) queryOpts.limit = REFS_TOP_SCAN_LIMIT;
31196
+ else if (opts.limit !== void 0) queryOpts.limit = opts.limit;
31197
+ else if (opts.top !== void 0) queryOpts.limit = REFS_TOP_SCAN_LIMIT;
31198
+ let results = applyTypedRefsTier(symbol3, file2, queryRefs(queryOpts));
31199
+ let suppressed = 0;
31200
+ let filteredTotal;
31201
+ if (opts.excludeTests === true) {
31202
+ const f = applyExcludeTestsFilter(results);
31203
+ suppressed = f.suppressed;
31204
+ filteredTotal = f.refs.length;
31205
+ results = opts.top !== void 0 ? f.refs : f.refs.slice(0, opts.limit ?? 100);
31206
+ }
31207
+ if (results.length > 0) anyFound = true;
31208
+ refFilePaths.push(...results.map((r) => r.filePath));
31209
+ if (opts.json === true) {
31210
+ if (opts.top !== void 0) {
31211
+ jsonOut[key] = topFilesJsonPayload(results, opts.top);
31212
+ } else {
31213
+ const capped = guardJsonRows(results);
31214
+ const trueTotal = opts.excludeTests === true ? filteredTotal ?? results.length : countRefs(queryOpts);
31215
+ jsonOut[key] = { items: withContextLines(capped.items, opts.context ?? 0), truncated: capped.truncated || trueTotal > results.length, totalCount: trueTotal };
31216
+ }
31217
+ continue;
31218
+ }
31219
+ if (results.length === 0) {
31220
+ 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)`);
31221
+ continue;
31222
+ }
31223
+ lines2.push(`${key}:`);
31224
+ if (opts.top !== void 0) {
31225
+ lines2.push(...renderTopFilesSummary(results, opts.top, void 0, suppressed));
31226
+ } else if (opts.callers === true) {
31227
+ if (opts.excludeTests === true && suppressed > 0) lines2.push(` ${results.length} references (${suppressed} in test files hidden by --exclude-tests)`);
31228
+ lines2.push(...renderCallerGroups(results, void 0, opts.context ?? 0));
31229
+ } else {
31230
+ if (opts.excludeTests === true && suppressed > 0) lines2.push(` ${results.length} references (${suppressed} in test files hidden by --exclude-tests)`);
31231
+ for (const ref2 of results) lines2.push(...renderRefLines(ref2, void 0, opts.context ?? 0));
30908
31232
  }
30909
31233
  }
30910
31234
  const fullSourceBytes = sumFileSizes(refFilePaths);
@@ -30924,11 +31248,23 @@ function runRefsSingle(opts) {
30924
31248
  const symName = symbol3 ?? file2;
30925
31249
  const queryOpts = { name: symName };
30926
31250
  const defFileHint = symbol3 !== void 0 ? resolveIndexPath(file2) : void 0;
30927
- if (defFileHint !== void 0 && opts.callers !== true) queryOpts.filePath = defFileHint;
30928
- if (opts.limit !== void 0) queryOpts.limit = opts.limit;
31251
+ if (opts.excludeTests === true) queryOpts.limit = REFS_TOP_SCAN_LIMIT;
31252
+ else if (opts.limit !== void 0) queryOpts.limit = opts.limit;
30929
31253
  else if (opts.top !== void 0) queryOpts.limit = REFS_TOP_SCAN_LIMIT;
30930
- const results = applyTypedRefsTier(symName, defFileHint, queryRefs(queryOpts));
31254
+ let results = applyTypedRefsTier(symName, defFileHint, queryRefs(queryOpts));
31255
+ let suppressed = 0;
31256
+ let filteredTotal;
31257
+ if (opts.excludeTests === true) {
31258
+ const f = applyExcludeTestsFilter(results);
31259
+ suppressed = f.suppressed;
31260
+ filteredTotal = f.refs.length;
31261
+ results = opts.top !== void 0 ? f.refs : f.refs.slice(0, opts.limit ?? 100);
31262
+ }
30931
31263
  if (results.length === 0) {
31264
+ if (opts.excludeTests === true && suppressed > 0) {
31265
+ emitErr2(`No non-test references found for '${symName}' (${suppressed} in test files hidden by --exclude-tests)`);
31266
+ return 1;
31267
+ }
30932
31268
  emitErr2(`No references found for '${symName}'`);
30933
31269
  return 1;
30934
31270
  }
@@ -30939,8 +31275,8 @@ function runRefsSingle(opts) {
30939
31275
  payload = topFilesJsonPayload(results, opts.top);
30940
31276
  } else {
30941
31277
  const capped = guardJsonRows(results);
30942
- const trueTotal = countRefs(queryOpts);
30943
- payload = { items: capped.items, truncated: capped.truncated || trueTotal > results.length, totalCount: trueTotal };
31278
+ const trueTotal = opts.excludeTests === true ? filteredTotal ?? results.length : countRefs(queryOpts);
31279
+ payload = { items: withContextLines(capped.items, opts.context ?? 0), truncated: capped.truncated || trueTotal > results.length, totalCount: trueTotal };
30944
31280
  }
30945
31281
  const text2 = JSON.stringify(payload, null, 2);
30946
31282
  emit3(text2);
@@ -30948,21 +31284,26 @@ function runRefsSingle(opts) {
30948
31284
  return 0;
30949
31285
  }
30950
31286
  const displayRoot = getDisplayRoot();
30951
- 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}`);
31287
+ 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, ""))];
30952
31288
  const text = lines2.join("\n");
30953
31289
  emitGuarded(text, "symbol");
30954
31290
  recordReadStat("symbol_read", fullSourceBytes, text, symName);
30955
31291
  return 0;
30956
31292
  }
31293
+ function applyExcludeTestsFilter(refs) {
31294
+ const filtered = refs.filter((r) => !isTestFile(r.filePath));
31295
+ return { refs: filtered, suppressed: refs.length - filtered.length };
31296
+ }
30957
31297
  function groupRefsByFile(refs) {
30958
31298
  const byFile = /* @__PURE__ */ new Map();
30959
31299
  for (const ref2 of refs) byFile.set(ref2.filePath, (byFile.get(ref2.filePath) ?? 0) + 1);
30960
31300
  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));
30961
31301
  }
30962
- function renderTopFilesSummary(refs, topN, displayRoot) {
31302
+ function renderTopFilesSummary(refs, topN, displayRoot, suppressed) {
30963
31303
  const grouped = groupRefsByFile(refs);
30964
31304
  const shown = grouped.slice(0, topN);
30965
- const lines2 = [`${refs.length} references across ${grouped.length} files (showing top ${shown.length})`];
31305
+ const suppressedNote = suppressed !== void 0 && suppressed > 0 ? ` (${suppressed} in test files hidden by --exclude-tests)` : "";
31306
+ const lines2 = [`${refs.length} references across ${grouped.length} files (showing top ${shown.length})${suppressedNote}`];
30966
31307
  for (const { file: file2, count } of shown) lines2.push(` ${count} ${toDisplayPath(displayRoot, file2)}`);
30967
31308
  const omittedFiles = grouped.length - shown.length;
30968
31309
  if (omittedFiles > 0) {
@@ -30976,7 +31317,7 @@ function topFilesJsonPayload(refs, topN) {
30976
31317
  const shown = grouped.slice(0, topN);
30977
31318
  return { fileCounts: shown, totalFiles: grouped.length, totalRefs: refs.length, shown: shown.length };
30978
31319
  }
30979
- function renderCallerGroups(refs, displayRoot) {
31320
+ function renderCallerGroups(refs, displayRoot, contextLines = 0) {
30980
31321
  const byFile = /* @__PURE__ */ new Map();
30981
31322
  for (const ref2 of refs) {
30982
31323
  const bucket = byFile.get(ref2.filePath);
@@ -30988,9 +31329,12 @@ function renderCallerGroups(refs, displayRoot) {
30988
31329
  }
30989
31330
  const lines2 = [];
30990
31331
  for (const [file2, fileRefs] of byFile) {
30991
- lines2.push(`${toDisplayPath(displayRoot, file2)}:`);
31332
+ const displayPath = toDisplayPath(displayRoot, file2);
31333
+ lines2.push(`${displayPath}:`);
30992
31334
  for (const ref2 of fileRefs) {
30993
31335
  lines2.push(` :${ref2.line} ${ref2.context !== "" ? ref2.context : "(module scope)"}`);
31336
+ const window = buildContextWindow(file2, ref2.line, contextLines);
31337
+ if (window !== null) lines2.push(...renderContextWindow(displayPath, ref2.line, window, "", " "));
30994
31338
  }
30995
31339
  }
30996
31340
  return lines2;
@@ -31029,7 +31373,19 @@ function prepareSymbolListing(file2, opts) {
31029
31373
  const fullSourceBytes = sumFileSizes([resolved]);
31030
31374
  return { kind: "ok", resolved, filtered, refCounts, fullSourceBytes, symbolsTruncated, trueSymbolCount };
31031
31375
  }
31376
+ function runPerFileListing(files, run3) {
31377
+ const blocks = [];
31378
+ let anyOk = false;
31379
+ for (const file2 of files) {
31380
+ const r = run3(file2);
31381
+ if (r.code === 0) anyOk = true;
31382
+ blocks.push(r.text);
31383
+ }
31384
+ return { text: blocks.join("\n\n"), code: anyOk ? 0 : 1 };
31385
+ }
31032
31386
  function runSkeleton(opts) {
31387
+ const multiFiles = parseMultiFileSpec(opts.file);
31388
+ if (multiFiles !== null) return runPerFileListing(multiFiles, (file2) => runSkeleton({ ...opts, file: file2 }));
31033
31389
  const prep = prepareSymbolListing(opts.file, opts);
31034
31390
  if (prep.kind === "empty") {
31035
31391
  return { text: prep.text, code: 1 };
@@ -31065,6 +31421,8 @@ function runSkeleton(opts) {
31065
31421
  return { text, code: 0 };
31066
31422
  }
31067
31423
  function runOutline(opts) {
31424
+ const multiFiles = parseMultiFileSpec(opts.file);
31425
+ if (multiFiles !== null) return runPerFileListing(multiFiles, (file2) => runOutline({ ...opts, file: file2 }));
31068
31426
  const prep = prepareSymbolListing(opts.file, opts);
31069
31427
  if (prep.kind === "empty") {
31070
31428
  return { text: prep.text, code: 1 };
@@ -31639,10 +31997,13 @@ function runBriefCore(opts) {
31639
31997
  const resolution = resolveSymbolSpec(opts.spec);
31640
31998
  if (resolution.kind === "ambiguous") {
31641
31999
  return {
32000
+ // 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.
31642
32001
  text: formatAmbiguity(
31643
32002
  resolution.symbol,
31644
32003
  resolution.file,
31645
- resolution.candidates
32004
+ resolution.candidates,
32005
+ void 0,
32006
+ "brief"
31646
32007
  ),
31647
32008
  code: 1
31648
32009
  };
@@ -31665,7 +32026,7 @@ function runBriefCore(opts) {
31665
32026
  if (opts.json === true) {
31666
32027
  const result = {
31667
32028
  symbol: match2,
31668
- callers: shown,
32029
+ callers: (opts.context ?? 0) > 0 ? shown.map((c) => ({ ...c, contextLines: buildContextWindow(c.file, c.line, opts.context ?? 0) ?? [] })) : shown,
31669
32030
  totalCallers,
31670
32031
  truncated,
31671
32032
  section: section2
@@ -31684,7 +32045,10 @@ function runBriefCore(opts) {
31684
32045
  ];
31685
32046
  lines2.push(`Callers (${totalCallers}):`);
31686
32047
  for (const c of shown) {
31687
- lines2.push(` ${c.caller} ${toDisplayPath(rootDir, c.file)}:${c.line}`);
32048
+ const callerDisplayPath = toDisplayPath(rootDir, c.file);
32049
+ lines2.push(` ${c.caller} ${callerDisplayPath}:${c.line}`);
32050
+ const window = buildContextWindow(c.file, c.line, opts.context ?? 0);
32051
+ if (window !== null) lines2.push(...renderContextWindow(callerDisplayPath, c.line, window, "", " "));
31688
32052
  }
31689
32053
  if (truncated) {
31690
32054
  lines2.push(` ...(${totalCallers - shown.length} more elided)`);
@@ -31716,11 +32080,40 @@ ${sub.text}`);
31716
32080
  if (anyFound) recordReadStat("brief_view", fullSourceBytes, text, opts.spec);
31717
32081
  return { text, code: anyFound ? 0 : 1 };
31718
32082
  }
32083
+ function runBriefCrossFile(pairs, opts) {
32084
+ const distinctFiles = new Set(pairs.map((p) => p.file));
32085
+ const keyFor = (p) => distinctFiles.size === 1 ? p.symbol : `${p.file}::${p.symbol}`;
32086
+ let anyFound = false;
32087
+ const jsonOut = {};
32088
+ const textBlocks = [];
32089
+ for (const { file: file2, symbol: symbol3 } of pairs) {
32090
+ const key = keyFor({ file: file2, symbol: symbol3 });
32091
+ const sub = runBriefCore({ ...opts, spec: `${file2}::${symbol3}`, suppressStat: true });
32092
+ if (sub.code === 0) anyFound = true;
32093
+ if (opts.json === true) {
32094
+ jsonOut[key] = sub.code === 0 ? JSON.parse(sub.text) : { error: sub.text };
32095
+ continue;
32096
+ }
32097
+ textBlocks.push(`${key}:
32098
+ ${sub.text}`);
32099
+ }
32100
+ const fullSourceBytes = sumFileSizes([...distinctFiles].map((f) => resolveIndexPath(f, process.cwd())));
32101
+ const text = opts.json === true ? JSON.stringify(jsonOut, null, 2) : textBlocks.join("\n\n");
32102
+ if (anyFound) recordReadStat("brief_view", fullSourceBytes, text, opts.spec);
32103
+ return { text, code: anyFound ? 0 : 1 };
32104
+ }
31719
32105
  function runBrief(opts) {
31720
32106
  if (opts.limit !== void 0 && opts.limit <= 0) {
31721
32107
  emitErr2(`--limit must be a positive number, got: ${opts.limit}`);
31722
32108
  return 1;
31723
32109
  }
32110
+ const crossFilePairs = parseCrossFileMultiSpec(opts.spec);
32111
+ if (crossFilePairs !== null) {
32112
+ const { text: text2, code: code2 } = runBriefCrossFile(crossFilePairs, opts);
32113
+ if (code2 === 0) emit3(text2);
32114
+ else emitErr2(text2);
32115
+ return code2;
32116
+ }
31724
32117
  const { file: file2, symbol: symbol3 } = parseReadSpec(opts.spec);
31725
32118
  if (symbol3 !== void 0 && symbol3 !== "" && symbol3.includes(",")) {
31726
32119
  const multiSymbols = symbol3.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
@@ -31812,6 +32205,34 @@ function parseDiffHunks(diffText) {
31812
32205
  }
31813
32206
  return hunksByFile;
31814
32207
  }
32208
+ function buildChangedRefHint(cwd, ref2) {
32209
+ const countResult = runGit(["rev-list", "--count", "HEAD"], { cwd });
32210
+ if (countResult.exitCode !== 0) {
32211
+ return null;
32212
+ }
32213
+ const commitCount = Number.parseInt(countResult.stdout.trim(), 10);
32214
+ if (!Number.isFinite(commitCount) || commitCount < 1) {
32215
+ return null;
32216
+ }
32217
+ const refResolves = runGit(["rev-parse", "--verify", "--quiet", ref2], { cwd });
32218
+ if (refResolves.exitCode === 0) {
32219
+ return null;
32220
+ }
32221
+ let suggestedRef = null;
32222
+ for (let n = commitCount - 1; n >= 1; n--) {
32223
+ const candidate = `HEAD~${n}`;
32224
+ const candidateResolves = runGit(["rev-parse", "--verify", "--quiet", candidate], { cwd });
32225
+ if (candidateResolves.exitCode === 0) {
32226
+ suggestedRef = candidate;
32227
+ break;
32228
+ }
32229
+ }
32230
+ if (suggestedRef === null) {
32231
+ suggestedRef = EMPTY_TREE_HASH;
32232
+ }
32233
+ const commitWord = commitCount === 1 ? "1 commit" : `${commitCount} commits`;
32234
+ return `Hint: this repo has only ${commitWord}; '${ref2}' does not exist. Try: token-goat changed --since ${suggestedRef}`;
32235
+ }
31815
32236
  function runChanged(opts = {}) {
31816
32237
  const ref2 = opts.ref ?? "HEAD~5";
31817
32238
  const cwd = opts.projectRoot ?? process.cwd();
@@ -31821,6 +32242,10 @@ function runChanged(opts = {}) {
31821
32242
  const result = runGit(["diff", ref2, "--name-only"], { cwd });
31822
32243
  if (result.exitCode !== 0) {
31823
32244
  emitErr2(`git diff failed: ${result.stderr}`);
32245
+ const hint = buildChangedRefHint(cwd, ref2);
32246
+ if (hint !== null) {
32247
+ emitErr2(hint);
32248
+ }
31824
32249
  return 1;
31825
32250
  }
31826
32251
  changedFiles = result.stdout.trim().split(/\r?\n/).filter(Boolean);
@@ -31926,13 +32351,16 @@ function resolveSymbolSpecOrEmitError(commandName, spec, projectRoot) {
31926
32351
  resolution.symbol,
31927
32352
  resolution.file,
31928
32353
  resolution.candidates,
31929
- projectRoot
32354
+ projectRoot,
32355
+ commandName
31930
32356
  )
31931
32357
  );
31932
32358
  return null;
31933
32359
  }
31934
32360
  if (resolution.kind === "none") {
31935
32361
  const messages = [`Symbol '${symbol3}' not found in '${file2}'`];
32362
+ const crossFileLead = formatCrossFileLead(commandName, symbol3, file2, projectRoot);
32363
+ if (crossFileLead !== "") messages.push(crossFileLead);
31936
32364
  const resolved = resolveIndexPath(file2, projectRoot ?? process.cwd());
31937
32365
  const closes = querySymbols({ filePath: resolved, limit: DIDYOUMEAN_LIMIT }).map((s) => s.name);
31938
32366
  if (closes.length > 0) messages.push(didYouMean(closes));
@@ -32142,22 +32570,29 @@ function runGrep(opts) {
32142
32570
  return 1;
32143
32571
  }
32144
32572
  const truncated = hits.slice(0, maxLines);
32573
+ if (opts.symbol === true) {
32574
+ const symbolsByFile = /* @__PURE__ */ new Map();
32575
+ for (const hit of truncated) {
32576
+ let syms = symbolsByFile.get(hit.file);
32577
+ if (syms === void 0) {
32578
+ syms = querySymbols({ filePath: resolveIndexPath(hit.file), limit: ALL_SYMBOLS_IN_FILE_LIMIT });
32579
+ symbolsByFile.set(hit.file, syms);
32580
+ }
32581
+ const enc = enclosingSymbol(syms, hit.line);
32582
+ hit.symbol = enc === null ? null : { name: enc.name, kind: enc.kind, lineStart: enc.lineStart, lineEnd: enc.lineEnd };
32583
+ }
32584
+ }
32145
32585
  if (opts.json === true) {
32146
32586
  const payload = { items: truncated, truncated: hits.length > maxLines, totalCount: hits.length };
32147
32587
  emit3(JSON.stringify(payload, null, 2));
32148
32588
  return 0;
32149
32589
  }
32150
32590
  for (const hit of truncated) {
32591
+ const symbolTag = opts.symbol === true && hit.symbol != null ? ` [${hit.symbol.name} (${hit.symbol.kind})]` : "";
32151
32592
  if (hit.context !== void 0) {
32152
- for (const ctxLine of hit.context) {
32153
- if (ctxLine.line === hit.line) {
32154
- emit3(`${hit.file}:${ctxLine.line}: ${ctxLine.text}`);
32155
- } else {
32156
- emit3(`${hit.file}-${ctxLine.line}- ${ctxLine.text}`);
32157
- }
32158
- }
32593
+ for (const line of renderContextWindow(hit.file, hit.line, hit.context, symbolTag)) emit3(line);
32159
32594
  } else {
32160
- emit3(`${hit.file}:${hit.line}: ${hit.text}`);
32595
+ emit3(`${hit.file}:${hit.line}: ${hit.text}${symbolTag}`);
32161
32596
  }
32162
32597
  }
32163
32598
  if (hits.length > maxLines) {
@@ -32340,9 +32775,24 @@ function extractExportNames(text, ext2) {
32340
32775
  }
32341
32776
  return names;
32342
32777
  }
32778
+ function runPerFileEmitting(files, label, run3) {
32779
+ let anyOk = false;
32780
+ files.forEach((file2, i) => {
32781
+ if (i > 0) emit3("");
32782
+ emit3(`# ${label}: ${file2}`);
32783
+ if (run3(file2) === 0) anyOk = true;
32784
+ });
32785
+ return anyOk ? 0 : 1;
32786
+ }
32343
32787
  function runExports(opts) {
32788
+ const multiFiles = parseMultiFileSpec(opts.file);
32789
+ if (multiFiles !== null) return runPerFileEmitting(multiFiles, "Exports", (file2) => runExports({ ...opts, file: file2 }));
32344
32790
  const symbols = querySymbols({ filePath: resolveIndexPath(opts.file), limit: 500 });
32345
32791
  const kindOf = (name2) => symbols.find((s) => s.name === name2)?.kind ?? "export";
32792
+ const locOf = (name2) => {
32793
+ const s = symbols.find((sym) => sym.name === name2);
32794
+ return s === void 0 ? null : { lineStart: s.lineStart, lineEnd: s.lineEnd };
32795
+ };
32346
32796
  const names = [];
32347
32797
  for (const s of symbols) {
32348
32798
  if (/^(?:export|pub\b|public\b)/.test(s.body.trimStart()) && !names.includes(s.name)) {
@@ -32367,12 +32817,23 @@ function runExports(opts) {
32367
32817
  }
32368
32818
  const fullSourceBytes = sumFileSizes([opts.file]);
32369
32819
  if (opts.json === true) {
32370
- const jsonText = JSON.stringify(names.map((n) => ({ name: n, kind: kindOf(n) })), null, 2);
32820
+ const jsonText = JSON.stringify(
32821
+ names.map((n) => {
32822
+ const loc = locOf(n);
32823
+ return { name: n, kind: kindOf(n), lineStart: loc?.lineStart ?? null, lineEnd: loc?.lineEnd ?? null };
32824
+ }),
32825
+ null,
32826
+ 2
32827
+ );
32371
32828
  emit3(jsonText);
32372
32829
  recordReadStat("exports", fullSourceBytes, jsonText, opts.file);
32373
32830
  return 0;
32374
32831
  }
32375
- const outLines = names.map((n) => `${kindOf(n).padEnd(10)} ${n}`);
32832
+ const outLines = names.map((n) => {
32833
+ const loc = locOf(n);
32834
+ const locSuffix = loc === null ? "" : ` (${loc.lineStart}-${loc.lineEnd})`;
32835
+ return `${kindOf(n).padEnd(10)} ${n}${locSuffix}`;
32836
+ });
32376
32837
  for (const line of outLines) {
32377
32838
  emit3(line);
32378
32839
  }
@@ -32666,6 +33127,8 @@ function importsExtensionFor(filePath) {
32666
33127
  return path48.extname(filePath);
32667
33128
  }
32668
33129
  function runImports(opts) {
33130
+ const multiFiles = parseMultiFileSpec(opts.file);
33131
+ if (multiFiles !== null) return runPerFileEmitting(multiFiles, "Imports", (file2) => runImports({ ...opts, file: file2 }));
32669
33132
  const text = readFileText(opts.file);
32670
33133
  if (text === null) {
32671
33134
  emitErr2(`Could not read: ${opts.file}`);
@@ -32759,11 +33222,12 @@ async function runSemantic(query, opts) {
32759
33222
  );
32760
33223
  const hits = mergeNearbyHits(rawHits).slice(0, n);
32761
33224
  if (hits.length > 0) {
33225
+ const enclosing = hits.map((h) => resolveEnclosingSymbol(h.filePath, h.startLine));
32762
33226
  if (opts.json === true) {
32763
- const items = hits.map((h) => ({
33227
+ const items = hits.map((h, i) => ({
32764
33228
  filePath: h.filePath,
32765
- name: null,
32766
- kind: null,
33229
+ name: enclosing[i]?.name ?? null,
33230
+ kind: enclosing[i]?.kind ?? null,
32767
33231
  startLine: h.startLine,
32768
33232
  endLine: h.endLine,
32769
33233
  distance: h.distance,
@@ -32774,10 +33238,12 @@ async function runSemantic(query, opts) {
32774
33238
  recordReadStat("semantic_search", sumFileSizes(hits.map((h) => h.filePath)), text3, query);
32775
33239
  return { text: text3, code: 0 };
32776
33240
  }
32777
- const blocks2 = hits.map(
32778
- (h) => `# ${toDisplayPath(rootDir, h.filePath)}:${h.startLine}-${h.endLine} (distance ${h.distance.toFixed(3)})
32779
- ${previewLines(h.text, 3)}`
32780
- );
33241
+ const blocks2 = hits.map((h, i) => {
33242
+ const enc = enclosing[i] ?? null;
33243
+ const suffix = enc !== null ? ` \u2014 inside ${enc.name} (${enc.kind})` : "";
33244
+ return `# ${toDisplayPath(rootDir, h.filePath)}:${h.startLine}-${h.endLine} (distance ${h.distance.toFixed(3)})${suffix}
33245
+ ${previewLines(h.text, 3)}`;
33246
+ });
32781
33247
  const text2 = guardText(blocks2.join("\n\n"), "semantic");
32782
33248
  recordReadStat("semantic_search", sumFileSizes(hits.map((h) => h.filePath)), text2, query);
32783
33249
  return { text: text2, code: 0 };
@@ -32866,7 +33332,7 @@ function runNoteList(opts = {}) {
32866
33332
  });
32867
33333
  return { text: lines2.join("\n"), code: 0 };
32868
33334
  }
32869
- 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;
33335
+ 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;
32870
33336
  var init_read_commands = __esm({
32871
33337
  "src/read_commands.ts"() {
32872
33338
  "use strict";
@@ -32911,6 +33377,7 @@ var init_read_commands = __esm({
32911
33377
  PARENT_IDENTIFIER_RE = /^[\w$]+$/;
32912
33378
  SKELETON_SYMBOL_CAP = 5e3;
32913
33379
  HUNK_HEADER_RE = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/;
33380
+ EMPTY_TREE_HASH = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
32914
33381
  DEFAULT_LOG_MAX_COUNT = 20;
32915
33382
  }
32916
33383
  });
@@ -48701,6 +49168,7 @@ init_index_reader();
48701
49168
  init_parser_types();
48702
49169
  init_doc_embed_extract();
48703
49170
  init_paths();
49171
+ init_project();
48704
49172
  init_hooks_index();
48705
49173
 
48706
49174
  // src/relay.ts
@@ -50035,7 +50503,7 @@ function checkDbExists(dataDir2) {
50035
50503
  return {
50036
50504
  name: "Database",
50037
50505
  status: "ok",
50038
- message: `global.db exists (${toKB(sizeBytes)} KB)`
50506
+ message: `global.db exists (${toKB(sizeBytes)} KB) at ${dbPath}`
50039
50507
  };
50040
50508
  }
50041
50509
  function checkSymbolBodySize(dbPath) {
@@ -50083,6 +50551,13 @@ function checkSymbolCount(dbPath, rootDir) {
50083
50551
  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'`
50084
50552
  };
50085
50553
  }
50554
+ if (fileCount === 0 && symbolCount === 0) {
50555
+ return {
50556
+ name: "Symbols",
50557
+ status: "warn",
50558
+ 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`
50559
+ };
50560
+ }
50086
50561
  return {
50087
50562
  name: "Symbols",
50088
50563
  status: "ok",
@@ -81187,6 +81662,9 @@ function runHintStatsCommand(opts = {}) {
81187
81662
  `);
81188
81663
  return;
81189
81664
  }
81665
+ if (rows.every((r) => r.emitted === 0 && r.actedOn === 0)) {
81666
+ process.stdout.write("No hint emissions recorded yet \u2014 the zeros below are absence of data, not measured ineffectiveness.\n");
81667
+ }
81190
81668
  printSummary(rows);
81191
81669
  }
81192
81670
 
@@ -81445,7 +81923,11 @@ async function cmdIndex(pathArg, opts = {}) {
81445
81923
  function cmdMap(opts) {
81446
81924
  const map3 = buildProjectMap(process.cwd(), { compact: opts.compact === true });
81447
81925
  const text = formatProjectMap(map3, map3.compact);
81448
- out(text);
81926
+ if (opts.json === true) {
81927
+ out(JSON.stringify(map3));
81928
+ } else {
81929
+ out(text);
81930
+ }
81449
81931
  const bytesSaved = mapLookupBytesSaved(map3, text);
81450
81932
  recordStat("map_lookup", bytesSaved, Math.round(bytesSaved / 4));
81451
81933
  }
@@ -81695,6 +82177,14 @@ async function cmdDoctor(opts) {
81695
82177
  if (project !== null) {
81696
82178
  doctorOpts.rootDir = project.root;
81697
82179
  }
82180
+ if (opts.json === true) {
82181
+ const results = runDoctor(doctorOpts.dataDir, doctorOpts.configPath, doctorOpts.rootDir);
82182
+ out(JSON.stringify(results));
82183
+ if (results.some((r) => r.status === "fail")) {
82184
+ throw new CliError("doctor checks failed");
82185
+ }
82186
+ return;
82187
+ }
81698
82188
  const code = await runDoctorAndExit(doctorOpts);
81699
82189
  if (code !== 0) {
81700
82190
  throw new CliError("doctor checks failed");
@@ -82198,6 +82688,16 @@ function runExitText(fn) {
82198
82688
  process.exitCode = 1;
82199
82689
  }
82200
82690
  }
82691
+ function noteExtraFileArgs(command, first2, extras, fn) {
82692
+ const result = fn();
82693
+ if (extras === void 0 || extras.length === 0) return result;
82694
+ return { text: `${extraFileArgsNote(command, first2, extras)}
82695
+ ${result.text}`, code: result.code };
82696
+ }
82697
+ function emitExtraFileArgsNote(command, first2, extras) {
82698
+ if (extras === void 0 || extras.length === 0) return;
82699
+ out(extraFileArgsNote(command, first2, extras));
82700
+ }
82201
82701
  function cmdCompress(opts) {
82202
82702
  try {
82203
82703
  if (opts.compress === false) {
@@ -82322,6 +82822,10 @@ async function cmdSkillList(opts) {
82322
82822
  return `${s.name.padEnd(25)} ${bodyKb.padStart(6)}K ${compactKb.padStart(6)}K ${marker} ${s.hitCount.toString().padStart(3)} ${age.padStart(3)} ${staleStatus}`;
82323
82823
  });
82324
82824
  const header = `${"Name".padEnd(25)} ${"Body".padStart(6)} ${"Compact".padStart(6)} Marker Hits Age Status`;
82825
+ if (skills.length === 0) {
82826
+ out("No skills cached yet.");
82827
+ return;
82828
+ }
82325
82829
  out([header, ...lines2].join("\n"));
82326
82830
  }
82327
82831
  }
@@ -83159,19 +83663,26 @@ function buildProgram() {
83159
83663
  process.exitCode = 1;
83160
83664
  }
83161
83665
  };
83162
- 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(
83163
- (name2, opts) => runExitText(
83666
+ 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) => {
83667
+ let projectRoot;
83668
+ if (opts.project === true) {
83669
+ projectRoot = resolveProjectRoot({ project: process.cwd() });
83670
+ } else if (typeof opts.project === "string") {
83671
+ projectRoot = resolveProjectRoot({ project: opts.project });
83672
+ }
83673
+ return runExitText(
83164
83674
  () => runSymbol({
83165
83675
  name: name2,
83166
83676
  limit: opts.limit !== void 0 ? requireNonNegativeInt("--limit", opts.limit) : 20,
83167
83677
  ...opts.file !== void 0 ? { file: opts.file } : {},
83168
83678
  ...opts.kind !== void 0 ? { kind: opts.kind } : {},
83679
+ ...projectRoot !== void 0 ? { projectRoot } : {},
83169
83680
  ...opts.json === true ? { json: true } : {}
83170
83681
  })
83171
- )
83172
- );
83682
+ );
83683
+ });
83173
83684
  program2.command("read <spec>").description(
83174
- "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)"
83685
+ "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)"
83175
83686
  ).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(
83176
83687
  (spec, opts) => runExitText(
83177
83688
  () => runRead({
@@ -83183,13 +83694,14 @@ function buildProgram() {
83183
83694
  )
83184
83695
  );
83185
83696
  program2.command("brief <spec>").description(
83186
- "symbol body + callers + containing doc section in one call (spec: file::symbol; comma-separated file::a,b for a merged multi-symbol view)"
83187
- ).option("-j, --json", "output as JSON").option("--limit <n>", "max callers to show (default: 20)").action(
83697
+ "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)"
83698
+ ).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(
83188
83699
  (spec, opts) => runExit(
83189
83700
  () => runBrief({
83190
83701
  spec,
83191
83702
  ...opts.json === true ? { json: true } : {},
83192
- ...opts.limit !== void 0 ? { limit: requireNonNegativeInt("--limit", opts.limit) } : {}
83703
+ ...opts.limit !== void 0 ? { limit: requireNonNegativeInt("--limit", opts.limit) } : {},
83704
+ ...opts.context !== void 0 ? { context: requireNonNegativeInt("--context", opts.context) } : {}
83193
83705
  })
83194
83706
  )
83195
83707
  );
@@ -83199,44 +83711,56 @@ function buildProgram() {
83199
83711
  (spec, opts) => opts.list === true ? runExit(() => runListSections({ file: spec, ...opts.json === true ? { json: true } : {} })) : runExitText(() => runSection({ spec, ...opts.json === true ? { json: true } : {} }))
83200
83712
  );
83201
83713
  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));
83202
- 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(
83203
- (file2, opts) => runExitText(
83204
- () => runSkeleton({
83205
- file: file2,
83206
- ...opts.json === true ? { json: true } : {},
83207
- ...opts.minLines !== void 0 ? { minLines: requireNonNegativeInt("--min-lines", opts.minLines) } : {},
83208
- ...opts.forceRefresh === true ? { forceRefresh: true } : {},
83209
- ...opts.stats === true ? { stats: true } : {}
83210
- })
83714
+ 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(
83715
+ (file2, more, opts) => runExitText(
83716
+ () => noteExtraFileArgs(
83717
+ "skeleton",
83718
+ file2,
83719
+ more,
83720
+ () => runSkeleton({
83721
+ file: file2,
83722
+ ...opts.json === true ? { json: true } : {},
83723
+ ...opts.minLines !== void 0 ? { minLines: requireNonNegativeInt("--min-lines", opts.minLines) } : {},
83724
+ ...opts.forceRefresh === true ? { forceRefresh: true } : {},
83725
+ ...opts.stats === true ? { stats: true } : {}
83726
+ })
83727
+ )
83211
83728
  )
83212
83729
  );
83213
- 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(
83214
- (file2, opts) => runExitText(
83215
- () => runOutline({
83216
- file: file2,
83217
- ...opts.json === true ? { json: true } : {},
83218
- ...opts.minLines !== void 0 ? { minLines: requireNonNegativeInt("--min-lines", opts.minLines) } : {},
83219
- ...opts.forceRefresh === true ? { forceRefresh: true } : {},
83220
- ...opts.stats === true ? { stats: true } : {}
83221
- })
83730
+ 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(
83731
+ (file2, more, opts) => runExitText(
83732
+ () => noteExtraFileArgs(
83733
+ "outline",
83734
+ file2,
83735
+ more,
83736
+ () => runOutline({
83737
+ file: file2,
83738
+ ...opts.json === true ? { json: true } : {},
83739
+ ...opts.minLines !== void 0 ? { minLines: requireNonNegativeInt("--min-lines", opts.minLines) } : {},
83740
+ ...opts.forceRefresh === true ? { forceRefresh: true } : {},
83741
+ ...opts.stats === true ? { stats: true } : {}
83742
+ })
83743
+ )
83222
83744
  )
83223
83745
  );
83224
- 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(
83746
+ 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(
83225
83747
  "--top <n>",
83226
83748
  "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"
83227
- ).option("-j, --json", "output as JSON").action(
83749
+ ).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(
83228
83750
  (spec, opts) => runExit(
83229
83751
  () => runRefs({
83230
83752
  spec,
83231
83753
  ...opts.callers === true ? { callers: true } : {},
83232
83754
  ...opts.json === true ? { json: true } : {},
83233
83755
  ...opts.limit !== void 0 ? { limit: requireNonNegativeInt("--limit", opts.limit) } : {},
83234
- ...opts.top !== void 0 ? { top: requireNonNegativeInt("--top", opts.top) } : {}
83756
+ ...opts.top !== void 0 ? { top: requireNonNegativeInt("--top", opts.top) } : {},
83757
+ ...opts.context !== void 0 ? { context: requireNonNegativeInt("--context", opts.context) } : {},
83758
+ ...opts.excludeTests === true ? { excludeTests: true } : {}
83235
83759
  })
83236
83760
  )
83237
83761
  );
83238
83762
  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));
83239
- program2.command("map").description("project overview").option("-c, --compact", "compact, low-token summary").action(guard(cmdMap));
83763
+ 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));
83240
83764
  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));
83241
83765
  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));
83242
83766
  program2.command("mcp-serve").description("run token-goat as an MCP stdio server exposing surgical reads and local compression/handoff tools").action(guard(cmdMcpServe));
@@ -83252,7 +83776,7 @@ function buildProgram() {
83252
83776
  worker.command("stop").description("stop the background indexer").action(guard(cmdWorkerStop));
83253
83777
  worker.command("status").description("check if the indexer is running").action(guard(cmdWorkerStatus));
83254
83778
  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));
83255
- program2.command("doctor").description("diagnose token-goat health").option("--context", "include context footprint analysis").action(guard(cmdDoctor));
83779
+ 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));
83256
83780
  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));
83257
83781
  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));
83258
83782
  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));
@@ -83266,11 +83790,17 @@ function buildProgram() {
83266
83790
  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));
83267
83791
  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));
83268
83792
  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));
83269
- program2.command("exports <file>").description("list exported (public) symbols in a file").option("-j, --json", "output as JSON").action(
83270
- (file2, opts) => runExit(() => runExports({ file: file2, ...opts.json === true ? { json: true } : {} }))
83793
+ 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(
83794
+ (file2, more, opts) => runExit(() => {
83795
+ emitExtraFileArgsNote("exports", file2, more);
83796
+ return runExports({ file: file2, ...opts.json === true ? { json: true } : {} });
83797
+ })
83271
83798
  );
83272
- program2.command("imports <file>").description("list the modules a file imports").option("-j, --json", "output as JSON").action(
83273
- (file2, opts) => runExit(() => runImports({ file: file2, ...opts.json === true ? { json: true } : {} }))
83799
+ 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(
83800
+ (file2, more, opts) => runExit(() => {
83801
+ emitExtraFileArgsNote("imports", file2, more);
83802
+ return runImports({ file: file2, ...opts.json === true ? { json: true } : {} });
83803
+ })
83274
83804
  );
83275
83805
  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(
83276
83806
  (pattern, opts) => runExit(
@@ -83281,7 +83811,7 @@ function buildProgram() {
83281
83811
  })
83282
83812
  )
83283
83813
  );
83284
- 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(
83814
+ 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(
83285
83815
  (pattern, paths, opts) => runExit(
83286
83816
  () => runGrep({
83287
83817
  pattern,
@@ -83289,7 +83819,8 @@ function buildProgram() {
83289
83819
  ...opts.json === true ? { json: true } : {},
83290
83820
  ...opts.maxLines !== void 0 ? { maxLines: requirePositiveInt("--max-lines", opts.maxLines) } : {},
83291
83821
  ...opts.recursive === false ? { recursive: false } : {},
83292
- ...opts.context !== void 0 ? { context: requireNonNegativeInt("--context", opts.context) } : {}
83822
+ ...opts.context !== void 0 ? { context: requireNonNegativeInt("--context", opts.context) } : {},
83823
+ ...opts.symbol === true ? { symbol: true } : {}
83293
83824
  })
83294
83825
  )
83295
83826
  );
@@ -83300,16 +83831,18 @@ function buildProgram() {
83300
83831
  program2.command("skill-history").description("list cached skill versions newest-first").option("-j, --json", "output as JSON").action(guard(cmdSkillHistory));
83301
83832
  program2.command("skill-diff <name>").description("show diff between two cached versions of a skill").action(guard(cmdSkillDiff));
83302
83833
  program2.command("skill-section <nameHeading> [headingArg]").description("extract a named section from a skill").action(guard(cmdSkillSection));
83303
- 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(
83834
+ 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(
83304
83835
  (symbol3, opts) => runExit(
83305
83836
  () => runCallers({
83306
83837
  symbol: symbol3,
83307
83838
  ...opts.json === true ? { json: true } : {},
83308
- ...opts.limit !== void 0 ? { limit: requireNonNegativeInt("--limit", opts.limit) } : {}
83839
+ ...opts.limit !== void 0 ? { limit: requireNonNegativeInt("--limit", opts.limit) } : {},
83840
+ ...opts.context !== void 0 ? { context: requireNonNegativeInt("--context", opts.context) } : {},
83841
+ ...opts.excludeTests === true ? { excludeTests: true } : {}
83309
83842
  })
83310
83843
  )
83311
83844
  );
83312
- 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(
83845
+ 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(
83313
83846
  (symbol3, opts) => runExit(
83314
83847
  () => runCallChain({
83315
83848
  symbol: symbol3,
@@ -83318,7 +83851,7 @@ function buildProgram() {
83318
83851
  })
83319
83852
  )
83320
83853
  );
83321
- 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(
83854
+ 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(
83322
83855
  (symbol3, opts) => runExit(
83323
83856
  () => runImpact({
83324
83857
  symbol: symbol3,
@@ -83327,13 +83860,14 @@ function buildProgram() {
83327
83860
  })
83328
83861
  )
83329
83862
  );
83330
- 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(
83863
+ 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(
83331
83864
  (opts) => runExit(
83332
83865
  () => runDead({
83333
83866
  ...opts.kind !== void 0 ? { kind: opts.kind } : {},
83334
83867
  ...opts.includePrivate === true ? { includePrivate: true } : {},
83335
83868
  ...opts.top !== void 0 ? { top: requireNonNegativeInt("--top", opts.top) } : {},
83336
- ...opts.json === true ? { json: true } : {}
83869
+ ...opts.json === true ? { json: true } : {},
83870
+ ...opts.excludeTests === true ? { excludeTests: true } : {}
83337
83871
  })
83338
83872
  )
83339
83873
  );
@@ -83352,7 +83886,7 @@ function buildProgram() {
83352
83886
  program2.command("scope <fileColonLine>").description("list symbols enclosing a file:line position, innermost first").option("-j, --json", "output as JSON").action(
83353
83887
  (spec, opts) => runExit(() => runScope({ spec, ...opts.json === true ? { json: true } : {} }))
83354
83888
  );
83355
- 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(
83889
+ 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(
83356
83890
  (spec, opts) => runExit(
83357
83891
  () => runSimilar({
83358
83892
  spec,
@@ -83391,7 +83925,7 @@ function buildProgram() {
83391
83925
  })
83392
83926
  )
83393
83927
  );
83394
- program2.command("blame <spec>").description('git blame for the line range of a symbol ("file::symbol")').option("-j, --json", "output as JSON").action(
83928
+ 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(
83395
83929
  (spec, opts) => runExit(() => runBlame({ spec, ...opts.json === true ? { json: true } : {} }))
83396
83930
  );
83397
83931
  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(
@@ -83475,16 +84009,16 @@ function buildProgram() {
83475
84009
  }))());
83476
84010
  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 } : {} }))());
83477
84011
  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))());
83478
- 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(
83479
- (opts) => runExit(
84012
+ 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(
84013
+ (ref2, opts) => runExit(
83480
84014
  () => runChanged({
83481
- ref: opts.since ?? "HEAD~5",
84015
+ ref: opts.since ?? ref2 ?? "HEAD~5",
83482
84016
  ...opts.symbol === true ? { symbolMode: true } : {},
83483
84017
  ...opts.json === true ? { json: true } : {}
83484
84018
  })
83485
84019
  )
83486
84020
  );
83487
- 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(
84021
+ 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(
83488
84022
  (spec, ref2, opts) => runExit(
83489
84023
  () => runDiff({
83490
84024
  spec,
@@ -83493,7 +84027,7 @@ function buildProgram() {
83493
84027
  })
83494
84028
  )
83495
84029
  );
83496
- 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(
84030
+ 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(
83497
84031
  (spec, ref2, opts) => runExit(
83498
84032
  () => runLog({
83499
84033
  spec,
@@ -83626,6 +84160,8 @@ async function run2(argv = process.argv) {
83626
84160
  }
83627
84161
 
83628
84162
  // src/main.ts
84163
+ init_util2();
84164
+ installEpipeGuard();
83629
84165
  void run2();
83630
84166
  /*! Bundled license information:
83631
84167