token-goat 2.6.23 → 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.23";
3053
+ return "2.6.25";
3054
3054
  }
3055
3055
  const require2 = createRequire(import.meta.url);
3056
3056
  const pkg = require2("../package.json");
@@ -4283,12 +4283,12 @@ function normalizeDarwinSystemAlias(p) {
4283
4283
  }
4284
4284
  function resolveIndexPath(file2, base = process.cwd()) {
4285
4285
  const isWindowsAbsolute = (s) => /^[a-zA-Z]:[/\\]/.test(s);
4286
- const resolve24 = isWindowsAbsolute(file2) || isWindowsAbsolute(base) ? path2.win32.resolve : path2.resolve;
4287
- return normalizePath(resolve24(base, file2));
4286
+ const resolve25 = isWindowsAbsolute(file2) || isWindowsAbsolute(base) ? path2.win32.resolve : path2.resolve;
4287
+ return normalizePath(resolve25(base, file2));
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"],
@@ -12084,14 +12144,23 @@ const path = require('node:path')
12084
12144
  const { pathToFileURL } = require('node:url')
12085
12145
 
12086
12146
  // Copilot event name -> token-goat internal HookEventName (src/types.ts's
12087
- // HOOK_EVENTS). Only these six have a token-goat handler; every other real
12147
+ // HOOK_EVENTS). Only these seven have a token-goat handler; every other real
12088
12148
  // Copilot event (sessionEnd, postToolUseFailure, subagentStart,
12089
12149
  // errorOccurred, notification, permissionRequest) is left unimplemented
12090
12150
  // rather than guessed at, and falls through to the default no-op below.
12091
- // 'sessionStart' is handled as a permanent no-op even though it's a real
12092
- // Copilot event, because token-goat has no internal session_start handler
12093
- // (mirrors PI_EXTENSION_SCRIPT's documented precedent).
12151
+ // 'sessionStart' was previously a permanent no-op on the stated grounds that
12152
+ // token-goat has no internal session_start handler. That was simply wrong --
12153
+ // hooks_session_start.ts has long emitted the command-routing reminder that
12154
+ // every other harness receives -- and the no-op was the reason Copilot CLI
12155
+ // sessions alone never got told token-goat exists. It is wired now: verified
12156
+ // against Copilot CLI 1.0.77 that a hooks.json sessionStart entry returning
12157
+ // {additionalContext} does reach the model. The github/copilot-cli#2142
12158
+ // fire-and-forget bug that would have made this dead wiring was fixed in a
12159
+ // pre-release months before that version, and its companion multi-extension
12160
+ // hook-overwrite bug never applied here: it hit runtime *extension* hooks,
12161
+ // while this config-file hooks.json path goes through Copilot's own merge.
12094
12162
  const COPILOT_TO_TG_EVENT = {
12163
+ sessionStart: 'session_start',
12095
12164
  preToolUse: 'pre_tool_use',
12096
12165
  postToolUse: 'post_tool_use',
12097
12166
  preCompact: 'pre_compact',
@@ -12203,11 +12272,6 @@ async function tryInProcess(entryPath, tgEvent, canonical) {
12203
12272
  async function main() {
12204
12273
  const copilotEvent = process.argv[2] || ''
12205
12274
 
12206
- if (copilotEvent === 'sessionStart') {
12207
- process.stdout.write('{}')
12208
- return
12209
- }
12210
-
12211
12275
  const tgEvent = COPILOT_TO_TG_EVENT[copilotEvent]
12212
12276
  if (!tgEvent) {
12213
12277
  process.stdout.write('{}')
@@ -12333,7 +12397,10 @@ function translate(copilotEvent, resp) {
12333
12397
  return {}
12334
12398
  }
12335
12399
 
12336
- if (copilotEvent === 'postToolUse') {
12400
+ if (copilotEvent === 'postToolUse' || copilotEvent === 'sessionStart') {
12401
+ // Both surface token-goat's context through the same field. sessionStart is
12402
+ // the one channel that reaches the model before it picks its first read
12403
+ // tool, so this is where the routing reminder has to land.
12337
12404
  const context = extractContext(resp)
12338
12405
  if (context) return { additionalContext: context }
12339
12406
  return {}
@@ -12357,9 +12424,8 @@ function translate(copilotEvent, resp) {
12357
12424
  // doc that both are notification-only -- Copilot never reads a response
12358
12425
  // body for either, so any additionalContext/systemMessage token-goat
12359
12426
  // produces has no surfacing channel here. This still routes through the
12360
- // token-goat hook call above (unlike sessionStart's early no-op) so the
12361
- // internal handler's own side effects keep running; only the response is
12362
- // discarded.
12427
+ // token-goat hook call above so the internal handler's own side effects keep
12428
+ // running; only the response is discarded.
12363
12429
  return {}
12364
12430
  }
12365
12431
 
@@ -12399,8 +12465,13 @@ main()
12399
12465
  import * as fs11 from "node:fs";
12400
12466
  import * as os5 from "node:os";
12401
12467
  import * as path11 from "node:path";
12468
+ function copilotCliUserRoot() {
12469
+ const override = process.env["COPILOT_HOME"];
12470
+ if (override !== void 0 && override.trim() !== "") return path11.resolve(override);
12471
+ return path11.join(os5.homedir(), ".copilot");
12472
+ }
12402
12473
  function copilotCliUserHooksDir() {
12403
- return path11.join(os5.homedir(), ".copilot", "hooks");
12474
+ return path11.join(copilotCliUserRoot(), "hooks");
12404
12475
  }
12405
12476
  function copilotCliProjectHooksDir() {
12406
12477
  return path11.join(process.cwd(), ".github", "hooks");
@@ -12505,6 +12576,7 @@ var init_copilot_cli_install = __esm({
12505
12576
  init_copilot_cli();
12506
12577
  init_guidance_block();
12507
12578
  COPILOT_CLI_HOOK_EVENTS = [
12579
+ "sessionStart",
12508
12580
  "preToolUse",
12509
12581
  "postToolUse",
12510
12582
  "preCompact",
@@ -15646,16 +15718,16 @@ var init_file_type_handler = __esm({
15646
15718
 
15647
15719
  // src/skill_cache.ts
15648
15720
  import * as fs18 from "fs/promises";
15649
- import { resolve as resolve7 } from "path";
15721
+ import { resolve as resolve8 } from "path";
15650
15722
  import { homedir as homedir7 } from "os";
15651
15723
  import { readdirSync as readdirSync8, readFileSync as readFileSync13, existsSync as existsSync14, statSync as statSync8, unlinkSync as unlinkSync9 } from "node:fs";
15652
15724
  function skillOutputsDir() {
15653
15725
  if (_skillOutputsDirOverride) return _skillOutputsDirOverride;
15654
- return resolve7(dataDir(), SKILLS_OUTPUT_SUBDIR);
15726
+ return resolve8(dataDir(), SKILLS_OUTPUT_SUBDIR);
15655
15727
  }
15656
15728
  function skillsSourceDir() {
15657
15729
  if (_skillsSourceDirOverride) return _skillsSourceDirOverride;
15658
- return resolve7(homedir7(), ".claude", "skills");
15730
+ return resolve8(homedir7(), ".claude", "skills");
15659
15731
  }
15660
15732
  async function ensureSkillsDir() {
15661
15733
  try {
@@ -15798,7 +15870,7 @@ async function listOutputs() {
15798
15870
  continue;
15799
15871
  }
15800
15872
  try {
15801
- const content = await fs18.readFile(resolve7(dir, entry.name), "utf-8");
15873
+ const content = await fs18.readFile(resolve8(dir, entry.name), "utf-8");
15802
15874
  const meta3 = JSON.parse(content);
15803
15875
  metas.push(meta3);
15804
15876
  } catch {
@@ -15832,7 +15904,7 @@ async function findCrossSessionEntry(skillName, contentSha) {
15832
15904
  continue;
15833
15905
  }
15834
15906
  const dir = skillOutputsDir();
15835
- const bodyPath = resolve7(dir, `${meta3.outputId}.txt`);
15907
+ const bodyPath = resolve8(dir, `${meta3.outputId}.txt`);
15836
15908
  try {
15837
15909
  const bodyExists = await fs18.access(bodyPath).then(() => true).catch(() => false);
15838
15910
  if (bodyExists) {
@@ -15887,7 +15959,7 @@ async function storeOutput(sessionId, skillName, body, opts) {
15887
15959
  const truncBuf = buf.slice(truncStart);
15888
15960
  storedBody = truncBuf.toString("utf-8");
15889
15961
  }
15890
- await atomicWriteText(resolve7(dir, `${outId}.txt`), storedBody);
15962
+ await atomicWriteText(resolve8(dir, `${outId}.txt`), storedBody);
15891
15963
  const meta3 = {
15892
15964
  outputId: outId,
15893
15965
  skillName: name2,
@@ -15897,7 +15969,7 @@ async function storeOutput(sessionId, skillName, body, opts) {
15897
15969
  truncated,
15898
15970
  sourcePath: opts?.sourcePath || ""
15899
15971
  };
15900
- await atomicWriteText(resolve7(dir, `${outId}.meta`), JSON.stringify(meta3, null, 2));
15972
+ await atomicWriteText(resolve8(dir, `${outId}.meta`), JSON.stringify(meta3, null, 2));
15901
15973
  pruneSkillOutputs();
15902
15974
  return meta3;
15903
15975
  } catch {
@@ -15917,7 +15989,7 @@ async function storeCompact(sessionId, skillName, compactText, sourceSha) {
15917
15989
  text = `<!-- source_sha: ${sourceSha.slice(0, 12)} -->
15918
15990
  ${text}`;
15919
15991
  }
15920
- await atomicWriteText(resolve7(dir, fileId), text);
15992
+ await atomicWriteText(resolve8(dir, fileId), text);
15921
15993
  } catch {
15922
15994
  }
15923
15995
  }
@@ -15937,7 +16009,7 @@ function getCompactAnySessionSync(skillName) {
15937
16009
  for (const entry of entries) {
15938
16010
  if (!matchesCompactSuffix(entry.name, entry.isFile(), suffix)) continue;
15939
16011
  try {
15940
- const text = readFileSync13(resolve7(dir, entry.name), "utf-8");
16012
+ const text = readFileSync13(resolve8(dir, entry.name), "utf-8");
15941
16013
  if (text.trim()) return text;
15942
16014
  } catch {
15943
16015
  continue;
@@ -15965,7 +16037,7 @@ async function readSkillHits(skillName) {
15965
16037
  const name2 = safeSkillName(skillName);
15966
16038
  if (!name2) return { count: 0, lastTs: 0 };
15967
16039
  const dir = skillOutputsDir();
15968
- const hitsFile = resolve7(dir, `${sanitizeSkillId(name2)}.hits`);
16040
+ const hitsFile = resolve8(dir, `${sanitizeSkillId(name2)}.hits`);
15969
16041
  const content = await fs18.readFile(hitsFile, "utf-8").catch(() => null);
15970
16042
  if (content) {
15971
16043
  const parsed = JSON.parse(content);
@@ -16015,7 +16087,7 @@ async function incrementSkillHit(skillName) {
16015
16087
  if (!name2) return;
16016
16088
  await ensureSkillsDir();
16017
16089
  const dir = skillOutputsDir();
16018
- const hitsFile = resolve7(dir, `${sanitizeSkillId(name2)}.hits`);
16090
+ const hitsFile = resolve8(dir, `${sanitizeSkillId(name2)}.hits`);
16019
16091
  const lockPath = `${hitsFile}.lock`;
16020
16092
  await runExclusiveInProcess(lockPath, async () => {
16021
16093
  const locked = await acquireSkillHitLock(lockPath);
@@ -16062,14 +16134,14 @@ async function listSkills(sessionId) {
16062
16134
  let compactLen = 0;
16063
16135
  let compactText = "";
16064
16136
  try {
16065
- const stat2 = await fs18.stat(resolve7(dir, compactFileId));
16137
+ const stat2 = await fs18.stat(resolve8(dir, compactFileId));
16066
16138
  compactLen = stat2.size;
16067
- compactText = await fs18.readFile(resolve7(dir, compactFileId), "utf-8").catch(() => "");
16139
+ compactText = await fs18.readFile(resolve8(dir, compactFileId), "utf-8").catch(() => "");
16068
16140
  } catch {
16069
16141
  compactLen = 0;
16070
16142
  }
16071
16143
  const hasMarker = extractCompactFromMarker(
16072
- await fs18.readFile(resolve7(dir, `${meta3.outputId}.txt`), "utf-8").catch(() => "")
16144
+ await fs18.readFile(resolve8(dir, `${meta3.outputId}.txt`), "utf-8").catch(() => "")
16073
16145
  ) !== null;
16074
16146
  const compactStale = isCompactStale(compactText, meta3.skillName, meta3.contentSha);
16075
16147
  const { count: hitCount } = await readSkillHits(meta3.skillName);
@@ -16105,7 +16177,7 @@ async function getSkillFilePath(skillName) {
16105
16177
  }
16106
16178
  }
16107
16179
  async function resolvePluginSkillPath(pluginName, skillSlug) {
16108
- const manifestPath = _pluginsManifestPathOverride ?? resolve7(homedir7(), ".claude", "plugins", "installed_plugins.json");
16180
+ const manifestPath = _pluginsManifestPathOverride ?? resolve8(homedir7(), ".claude", "plugins", "installed_plugins.json");
16109
16181
  let raw;
16110
16182
  try {
16111
16183
  raw = await fs18.readFile(manifestPath, "utf8");
@@ -16128,7 +16200,7 @@ async function resolvePluginSkillPath(pluginName, skillSlug) {
16128
16200
  if (typeof entry !== "object" || entry === null) continue;
16129
16201
  const installPath = entry["installPath"];
16130
16202
  if (typeof installPath !== "string" || installPath === "") continue;
16131
- const diskPath = resolve7(installPath, "skills", skillSlug, "SKILL.md");
16203
+ const diskPath = resolve8(installPath, "skills", skillSlug, "SKILL.md");
16132
16204
  try {
16133
16205
  await fs18.access(diskPath);
16134
16206
  return diskPath;
@@ -16147,7 +16219,7 @@ async function installedSkillPath(skillName) {
16147
16219
  const pluginPath = await resolvePluginSkillPath(name2.slice(0, colonIdx), name2.slice(colonIdx + 1));
16148
16220
  if (pluginPath !== null) return pluginPath;
16149
16221
  }
16150
- const diskPath = resolve7(skillsSourceDir(), name2, "SKILL.md");
16222
+ const diskPath = resolve8(skillsSourceDir(), name2, "SKILL.md");
16151
16223
  try {
16152
16224
  await fs18.access(diskPath);
16153
16225
  return diskPath;
@@ -16167,11 +16239,11 @@ function pruneSkillOutputs(maxCount = DEFAULT_MAX_COUNT, maxAgeMs = DEFAULT_MAX_
16167
16239
  const outputId = file2.slice(0, -".meta".length);
16168
16240
  let ts;
16169
16241
  try {
16170
- const parsed = JSON.parse(readFileSync13(resolve7(dir, file2), "utf-8"));
16242
+ const parsed = JSON.parse(readFileSync13(resolve8(dir, file2), "utf-8"));
16171
16243
  ts = parsed.ts;
16172
16244
  } catch {
16173
16245
  try {
16174
- ts = statSync8(resolve7(dir, file2)).mtimeMs;
16246
+ ts = statSync8(resolve8(dir, file2)).mtimeMs;
16175
16247
  } catch {
16176
16248
  continue;
16177
16249
  }
@@ -16181,7 +16253,7 @@ function pruneSkillOutputs(maxCount = DEFAULT_MAX_COUNT, maxAgeMs = DEFAULT_MAX_
16181
16253
  const removeEntry = (outputId) => {
16182
16254
  for (const ext2 of [".meta", ".txt"]) {
16183
16255
  try {
16184
- unlinkSync9(resolve7(dir, `${outputId}${ext2}`));
16256
+ unlinkSync9(resolve8(dir, `${outputId}${ext2}`));
16185
16257
  } catch {
16186
16258
  }
16187
16259
  }
@@ -16273,7 +16345,7 @@ async function ocrImage(input) {
16273
16345
  if (_ocrUnavailableThisProcess) return null;
16274
16346
  const entryPath = resolveTesseractEntry();
16275
16347
  if (entryPath === null) return null;
16276
- return new Promise((resolve24) => {
16348
+ return new Promise((resolve25) => {
16277
16349
  let settled = false;
16278
16350
  let child;
16279
16351
  try {
@@ -16282,7 +16354,7 @@ async function ocrImage(input) {
16282
16354
  });
16283
16355
  } catch {
16284
16356
  _ocrUnavailableThisProcess = true;
16285
- resolve24(null);
16357
+ resolve25(null);
16286
16358
  return;
16287
16359
  }
16288
16360
  const chunks = [];
@@ -16295,7 +16367,7 @@ async function ocrImage(input) {
16295
16367
  child.kill();
16296
16368
  } catch {
16297
16369
  }
16298
- resolve24(result);
16370
+ resolve25(result);
16299
16371
  };
16300
16372
  const timer = setTimeout(() => finish(null, true), _ocrTimeoutMs);
16301
16373
  child.stdout?.on("data", (c) => chunks.push(c));
@@ -17537,7 +17609,7 @@ import * as readline from "node:readline";
17537
17609
  async function defaultConfirm(question) {
17538
17610
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
17539
17611
  try {
17540
- const answer = await new Promise((resolve24) => rl.question(question, resolve24));
17612
+ const answer = await new Promise((resolve25) => rl.question(question, resolve25));
17541
17613
  return /^y(es)?$/i.test(answer.trim());
17542
17614
  } finally {
17543
17615
  rl.close();
@@ -17603,7 +17675,7 @@ function ensureTransformerLoaded() {
17603
17675
  }
17604
17676
  }
17605
17677
  function sleep(ms) {
17606
- return new Promise((resolve24) => setTimeout(resolve24, ms));
17678
+ return new Promise((resolve25) => setTimeout(resolve25, ms));
17607
17679
  }
17608
17680
  async function buildExtractorWithRetry(pipelineFn, modelName) {
17609
17681
  let lastError;
@@ -17943,7 +18015,8 @@ function rerankHits(hits, query, topK) {
17943
18015
  boost = Math.min(matches2 * _VERBATIM_TOKEN_BOOST, _MAX_VERBATIM_BOOST);
17944
18016
  }
17945
18017
  const penalty = _isGeneratedPath(hit.filePath) ? _GENERATED_PATH_PENALTY : 0;
17946
- return { hit, index, adjusted: hit.distance - boost + penalty };
18018
+ const pathPenalty = _pathPriorityPenalty(hit.filePath);
18019
+ return { hit, index, adjusted: hit.distance - boost + penalty + pathPenalty };
17947
18020
  });
17948
18021
  scored.sort((a, b) => a.adjusted - b.adjusted || a.index - b.index);
17949
18022
  return scored.slice(0, topK).map((entry) => ({ ...entry.hit, adjustedDistance: entry.adjusted }));
@@ -18069,11 +18142,26 @@ function _isGeneratedPath(filePath) {
18069
18142
  }
18070
18143
  return false;
18071
18144
  }
18072
- 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;
18073
18160
  var init_embeddings = __esm({
18074
18161
  "src/embeddings.ts"() {
18075
18162
  "use strict";
18076
18163
  init_define_import_meta_env();
18164
+ init_config();
18077
18165
  init_sql_path();
18078
18166
  init_util2();
18079
18167
  init_reset();
@@ -18122,6 +18210,17 @@ var init_embeddings = __esm({
18122
18210
  ".ruff_cache"
18123
18211
  ]);
18124
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";
18125
18224
  _VERBATIM_TOKEN_BOOST = 0.05;
18126
18225
  _MAX_VERBATIM_BOOST = 0.25;
18127
18226
  _TOKEN_RE = /\w+/g;
@@ -18664,14 +18763,32 @@ function fetchTopSymbols(limit, dbPath, rootDir) {
18664
18763
  try {
18665
18764
  const db = getDb(dbPath);
18666
18765
  const { clause, param } = projectScopeClause("file_path");
18766
+ const refScope = projectScopeClause("file_path");
18667
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.
18668
18769
  `SELECT file_path, name, kind, line_start, line_end, body, docstring, parent
18669
- FROM symbols
18670
- WHERE kind IN ('class', 'function', 'interface') AND ${clause}
18671
- 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,
18672
18789
  LENGTH(COALESCE(body, '')) DESC
18673
18790
  LIMIT ?`
18674
- ).all(param(rootDir), limit);
18791
+ ).all(refScope.param(rootDir), param(rootDir), limit);
18675
18792
  return rows.map((r) => ({
18676
18793
  filePath: r.file_path,
18677
18794
  name: r.name,
@@ -18723,13 +18840,12 @@ function formatProjectMap(map3, compact = false) {
18723
18840
  lines2.push("");
18724
18841
  lines2.push("## Top symbols");
18725
18842
  for (const s of map3.topSymbols) {
18726
- if (compact) {
18727
- lines2.push(`- ${s.name} (${s.kind})`);
18728
- } else {
18729
- const loc = `${path25.basename(s.filePath)}:${s.lineStart}-${s.lineEnd}`;
18730
- lines2.push(`- ${s.name} (${s.kind}) \u2014 ${loc}`);
18731
- }
18843
+ const loc = `${toDisplayPath(map3.rootDir, s.filePath)}:${s.lineStart}-${s.lineEnd}`;
18844
+ lines2.push(`- ${s.name} (${s.kind}) \u2014 ${loc}`);
18732
18845
  }
18846
+ } else {
18847
+ lines2.push("");
18848
+ lines2.push("## Top symbols: none \u2014 no files indexed for this project; run 'token-goat index .'");
18733
18849
  }
18734
18850
  if (!compact && map3.recentFiles.length > 0) {
18735
18851
  lines2.push("");
@@ -24655,10 +24771,10 @@ function embedFileSerialized(absPath, dbPath, sha) {
24655
24771
  activeEmbedSlots += 1;
24656
24772
  return dispatchEmbed();
24657
24773
  }
24658
- return new Promise((resolve24) => {
24774
+ return new Promise((resolve25) => {
24659
24775
  embedSlotWaiters.push(() => {
24660
24776
  activeEmbedSlots += 1;
24661
- resolve24(dispatchEmbed());
24777
+ resolve25(dispatchEmbed());
24662
24778
  });
24663
24779
  });
24664
24780
  };
@@ -25064,7 +25180,7 @@ async function runWorkerLoop(dir, pollIntervalMs, shouldStop = () => false) {
25064
25180
  lastKnownRootsSweepMs = Date.now();
25065
25181
  }
25066
25182
  if (shouldStop()) break;
25067
- await new Promise((resolve24) => setTimeout(resolve24, pollIntervalMs));
25183
+ await new Promise((resolve25) => setTimeout(resolve25, pollIntervalMs));
25068
25184
  }
25069
25185
  }
25070
25186
  function runDetachedWorkerDaemon() {
@@ -25750,6 +25866,11 @@ function isDeadSymbol(name2, refCount) {
25750
25866
  if (ENTRY_NAMES.has(name2)) return false;
25751
25867
  return refCount === 0;
25752
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
+ }
25753
25874
  function buildFileSymCache() {
25754
25875
  const cache = /* @__PURE__ */ new Map();
25755
25876
  return (fp) => {
@@ -25767,9 +25888,10 @@ function fileDefinesName(fp, name2, getSyms) {
25767
25888
  function filterRefsForSymbol(refs, name2, filePath, getSyms) {
25768
25889
  return refs.filter((ref2) => ref2.filePath === filePath || !fileDefinesName(ref2.filePath, name2, getSyms));
25769
25890
  }
25770
- function resolveCallers(name2, limit, filePath, rootDir) {
25891
+ function resolveCallers(name2, limit, filePath, rootDir, excludeTests) {
25771
25892
  const resolvedRootDir = rootDir ?? resolveProjectRoot({ project: process.cwd() });
25772
- 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 });
25773
25895
  const getSyms = buildFileSymCache();
25774
25896
  const scoped = filePath === void 0 ? refs : filterRefsForSymbol(refs, name2, filePath, getSyms);
25775
25897
  return scoped.map((ref2) => {
@@ -25788,17 +25910,37 @@ function runCallers(opts) {
25788
25910
  return 1;
25789
25911
  }
25790
25912
  const rootDir = resolveProjectRoot({ project: process.cwd() });
25791
- 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;
25792
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
+ }
25793
25927
  emitErr(`No references found for '${opts.symbol}'`);
25794
25928
  return 1;
25795
25929
  }
25930
+ const contextLines = opts.context ?? 0;
25796
25931
  if (opts.json === true) {
25797
- 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));
25798
25934
  return 0;
25799
25935
  }
25936
+ if (opts.excludeTests === true && suppressed > 0) {
25937
+ emit2(`${entries.length} callers found (${suppressed} in test files hidden by --exclude-tests)`);
25938
+ }
25800
25939
  for (const e of entries) {
25801
- 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);
25802
25944
  }
25803
25945
  return 0;
25804
25946
  }
@@ -25809,28 +25951,36 @@ function runCallChain(opts) {
25809
25951
  }
25810
25952
  const maxDepth = opts.depth ?? 8;
25811
25953
  const rootDir = resolveProjectRoot({ project: process.cwd() });
25812
- 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) {
25813
25962
  emitErr(`Symbol not found: ${opts.symbol}`);
25814
25963
  return 1;
25815
25964
  }
25816
25965
  const getSyms = buildFileSymCache();
25817
- const callersOf = (name2) => {
25818
- 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 });
25819
25968
  if (refs.length === 0) return [];
25969
+ const scoped = fileHint !== void 0 && n === name2 ? filterRefsForSymbol(refs, n, fileHint, getSyms) : refs;
25820
25970
  const names = /* @__PURE__ */ new Set();
25821
- for (const ref2 of refs) {
25971
+ for (const ref2 of scoped) {
25822
25972
  const enc = enclosingSymbol(getSyms(ref2.filePath), ref2.line);
25823
25973
  if (enc !== null) names.add(enc.name);
25824
25974
  }
25825
25975
  return [...names];
25826
25976
  };
25827
- const chains = bfsCallChains(opts.symbol, callersOf, maxDepth);
25977
+ const chains = bfsCallChains(name2, callersOf, maxDepth);
25828
25978
  if (opts.json === true) {
25829
25979
  emit2(JSON.stringify({ chains }, null, 2));
25830
25980
  return 0;
25831
25981
  }
25832
- if (chains.length === 1 && chains[0]?.length === 1 && chains[0][0] === opts.symbol) {
25833
- emit2(`${opts.symbol} (no callers)`);
25982
+ if (chains.length === 1 && chains[0]?.length === 1 && chains[0][0] === name2) {
25983
+ emit2(`${name2} (no callers)`);
25834
25984
  return 0;
25835
25985
  }
25836
25986
  for (const chain2 of chains) {
@@ -25850,16 +26000,23 @@ function runImpact(opts) {
25850
26000
  const top = opts.top ?? 20;
25851
26001
  const DEPTH_CAP = 8;
25852
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
+ }
25853
26009
  const getSyms = buildFileSymCache();
25854
- const hops = /* @__PURE__ */ new Map([[opts.symbol, 0]]);
25855
- const queue = [[opts.symbol, 0]];
26010
+ const hops = /* @__PURE__ */ new Map([[rootName, 0]]);
26011
+ const queue = [[rootName, 0]];
25856
26012
  while (queue.length > 0) {
25857
26013
  const item = queue.shift();
25858
26014
  if (item === void 0) break;
25859
26015
  const [name2, depth] = item;
25860
26016
  if (depth >= DEPTH_CAP) continue;
25861
26017
  const refs = queryRefs({ name: name2, limit: DEFAULT_REF_QUERY_LIMIT, rootDir });
25862
- 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) {
25863
26020
  const newHop = depth + 1;
25864
26021
  const enc = enclosingSymbol(getSyms(ref2.filePath), ref2.line);
25865
26022
  if (enc === null) {
@@ -25876,7 +26033,7 @@ function runImpact(opts) {
25876
26033
  }
25877
26034
  }
25878
26035
  }
25879
- hops.delete(opts.symbol);
26036
+ hops.delete(rootName);
25880
26037
  const sorted = [...hops.entries()].sort(compareHopEntries).slice(0, top);
25881
26038
  if (sorted.length === 0) {
25882
26039
  emitErr(`No callers found for '${opts.symbol}'`);
@@ -25928,6 +26085,7 @@ function runDead(opts) {
25928
26085
  const syms = querySymbols({ kind, limit: 5e3, rootDir });
25929
26086
  const getSyms = buildFileSymCache();
25930
26087
  const results = [];
26088
+ let suppressed = 0;
25931
26089
  for (const sym of syms) {
25932
26090
  if (opts.includePrivate !== true && sym.name.startsWith("_")) continue;
25933
26091
  const refs = queryRefs({ name: sym.name, limit: DEFAULT_REF_QUERY_LIMIT, rootDir });
@@ -25937,6 +26095,10 @@ function runDead(opts) {
25937
26095
  const ownScope = enclosingNamedScope(getSyms(sym.filePath), sym.lineStart);
25938
26096
  if (ownScope !== null && hasAncestorDispatchRef(sym.name, ownScope.name, sym.filePath, rootDir)) continue;
25939
26097
  }
26098
+ if (opts.excludeTests === true && isTestFile(sym.filePath)) {
26099
+ suppressed += 1;
26100
+ continue;
26101
+ }
25940
26102
  results.push({ name: sym.name, kind: sym.kind, file: sym.filePath, line: sym.lineStart });
25941
26103
  }
25942
26104
  const sliced = results.slice(0, opts.top ?? results.length);
@@ -25945,9 +26107,16 @@ function runDead(opts) {
25945
26107
  return 0;
25946
26108
  }
25947
26109
  if (sliced.length === 0) {
25948
- 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
+ }
25949
26115
  return 0;
25950
26116
  }
26117
+ if (opts.excludeTests === true && suppressed > 0) {
26118
+ emit2(`${sliced.length} dead symbols (${suppressed} in test files hidden by --exclude-tests)`);
26119
+ }
25951
26120
  for (const r of sliced) {
25952
26121
  emit2(`${r.name} ${toDisplayPath(rootDir, r.file)}:${r.line}`);
25953
26122
  }
@@ -26197,16 +26366,9 @@ function runSimilar(opts) {
26197
26366
  emitErr(`Invalid spec - expected "file::symbol", got: ${opts.spec}`);
26198
26367
  return 1;
26199
26368
  }
26200
- const fileArg = opts.spec.slice(0, sepIdx);
26201
- const symbolArg = opts.spec.slice(sepIdx + 2);
26202
26369
  const top = opts.top ?? 10;
26203
- const filePath = resolveIndexPath(fileArg);
26204
- const anchors = querySymbols({ name: symbolArg, filePath });
26205
- if (anchors.length === 0) {
26206
- emitErr(`Symbol '${symbolArg}' not found in '${fileArg}'`);
26207
- return 1;
26208
- }
26209
- const anchor = anchors[0];
26370
+ const anchor = resolveSymbolSpecOrEmitError("similar", opts.spec, void 0);
26371
+ if (anchor === null) return 1;
26210
26372
  const words = [anchor.name, ...(anchor.docstring ?? "").split(/\s+/).filter((w) => w.length > 4)];
26211
26373
  const query = words.slice(0, 8).join(" ");
26212
26374
  const rootDir = resolveProjectRoot({ project: process.cwd() });
@@ -26238,13 +26400,13 @@ function runContextFor(opts) {
26238
26400
  const bodyTokens = estimateTokens(h.body ?? "");
26239
26401
  if (budget !== void 0 && tokensSoFar + bodyTokens > budget) continue;
26240
26402
  tokensSoFar += bodyTokens;
26241
- 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}"` });
26242
26404
  }
26243
26405
  if (opts.json === true) {
26244
26406
  emit2(JSON.stringify(entries, null, 2));
26245
26407
  return 0;
26246
26408
  }
26247
- 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}"`);
26248
26410
  return 0;
26249
26411
  }
26250
26412
  function runTestFor(opts) {
@@ -26389,16 +26551,10 @@ function runBlame(opts) {
26389
26551
  emitErr(`Invalid spec - expected "file::symbol", got: ${opts.spec}`);
26390
26552
  return 1;
26391
26553
  }
26392
- const fileArg = opts.spec.slice(0, sepIdx);
26393
- const symbolArg = opts.spec.slice(sepIdx + 2);
26394
26554
  const cwd = opts.cwd ?? process.cwd();
26395
- const filePath = resolveIndexPath(fileArg);
26396
- const syms = querySymbols({ name: symbolArg, filePath });
26397
- if (syms.length === 0) {
26398
- emitErr(`Symbol '${symbolArg}' not found in '${fileArg}'`);
26399
- return 1;
26400
- }
26401
- const sym = syms[0];
26555
+ const sym = resolveSymbolSpecOrEmitError("blame", opts.spec, void 0);
26556
+ if (sym === null) return 1;
26557
+ const filePath = sym.filePath;
26402
26558
  const start = sym.lineStart;
26403
26559
  const end = sym.lineEnd;
26404
26560
  let raw;
@@ -26419,10 +26575,10 @@ function runBlame(opts) {
26419
26575
  if (!m) return { raw: l };
26420
26576
  return { commit: m[1], author: (m[2] ?? "").trim(), date: (m[3] ?? "").trim(), line: Number.parseInt(m[4] ?? "0", 10), content: m[5] };
26421
26577
  });
26422
- emit2(JSON.stringify({ symbol: symbolArg, file: filePath, lines: lines2 }, null, 2));
26578
+ emit2(JSON.stringify({ symbol: sym.name, file: filePath, lines: lines2 }, null, 2));
26423
26579
  return 0;
26424
26580
  }
26425
- emit2(`${symbolArg} ${toDisplayPath(getDisplayRoot(opts.cwd), filePath)}:${start}-${end}`);
26581
+ emit2(`${sym.name} ${toDisplayPath(getDisplayRoot(opts.cwd), filePath)}:${start}-${end}`);
26426
26582
  emit2(raw.trim());
26427
26583
  return 0;
26428
26584
  }
@@ -26436,14 +26592,14 @@ function runAsk(opts) {
26436
26592
  const hits = searchSymbolsFts(opts.question, top, void 0, rootDir);
26437
26593
  const BACKEND_ENV = "TOKEN_GOAT_ASK_BACKEND";
26438
26594
  const backendLabel = process.env[BACKEND_ENV] ?? "";
26439
- 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}"` }));
26440
26596
  const degrade = () => {
26441
26597
  if (opts.json === true) {
26442
26598
  emit2(JSON.stringify({ degraded: true, note: `Set ${BACKEND_ENV}=claude|codex for LLM synthesis`, context: entries }, null, 2));
26443
26599
  return 0;
26444
26600
  }
26445
26601
  emit2(`[degraded mode - set ${BACKEND_ENV}=claude|codex for LLM synthesis]`);
26446
- 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}"`);
26447
26603
  return 0;
26448
26604
  };
26449
26605
  if (!backendLabel) return degrade();
@@ -30426,6 +30582,42 @@ function formatBareNameSpecError(command, name2, projectRoot) {
30426
30582
  }
30427
30583
  return lines2.join("\n");
30428
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
+ }
30429
30621
  function trimBlankLines(lines2) {
30430
30622
  let start = 0;
30431
30623
  let end = lines2.length;
@@ -30521,10 +30713,21 @@ function parseCrossFileMultiSpec(spec) {
30521
30713
  }
30522
30714
  return pairs.length > 1 ? pairs : null;
30523
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
+ }
30524
30726
  function parseLineRange(spec) {
30525
30727
  const m = /^(.+)@(\d+)(?:-(\d+))?$/.exec(spec);
30526
30728
  if (m === null) return null;
30527
30729
  if (fileExists(spec)) return null;
30730
+ if (m[1].includes("::")) return null;
30528
30731
  const start = parseInt(m[2], 10);
30529
30732
  const end = m[3] !== void 0 ? parseInt(m[3], 10) : start;
30530
30733
  return { file: m[1], start, end };
@@ -30583,30 +30786,49 @@ function findParentName(entry, fileSymbols) {
30583
30786
  if (doc !== "" && PARENT_IDENTIFIER_RE.test(doc)) return doc;
30584
30787
  return null;
30585
30788
  }
30586
- function formatAmbiguity(symbol3, file2, candidates, explicitRoot) {
30789
+ function formatAmbiguity(symbol3, file2, candidates, explicitRoot, commandName = "read") {
30587
30790
  const multiFile = new Set(candidates.map((c) => c.filePath)).size > 1;
30588
30791
  const displayRoot = getDisplayRoot(explicitRoot);
30589
30792
  const lines2 = [
30590
30793
  `Ambiguous symbol '${symbol3}' in '${file2}': ${candidates.length} definitions match. Retry with one of the qualified commands below to pick one:`
30591
30794
  ];
30592
30795
  const fileSymCache = /* @__PURE__ */ new Map();
30593
- for (const c of candidates) {
30594
- let fileSyms = fileSymCache.get(c.filePath);
30796
+ const getFileSyms = (filePath) => {
30797
+ let fileSyms = fileSymCache.get(filePath);
30595
30798
  if (fileSyms === void 0) {
30596
- fileSyms = querySymbols({ filePath: c.filePath, limit: 1e3 });
30597
- fileSymCache.set(c.filePath, fileSyms);
30799
+ fileSyms = querySymbols({ filePath, limit: 1e3 });
30800
+ fileSymCache.set(filePath, fileSyms);
30598
30801
  }
30599
- const parent = findParentName(c, fileSyms);
30600
- 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;
30601
30820
  const retryFile = multiFile ? toDisplayPath(displayRoot, c.filePath) : file2;
30602
30821
  const label = multiFile ? `${toDisplayPath(displayRoot, c.filePath)}::${qualifier}` : qualifier;
30603
- lines2.push(` - ${label} (line ${c.lineStart}) -> token-goat read "${retryFile}::${qualifier}"`);
30822
+ lines2.push(` - ${label} (line ${c.lineStart}) -> token-goat ${commandName} "${retryFile}::${qualifier}"`);
30604
30823
  }
30605
30824
  return lines2.join("\n");
30606
30825
  }
30607
30826
  function resolveSymbolSpec(spec, forceRefresh, projectRoot) {
30608
- const { file: file2, symbol: symbol3 } = parseReadSpec(spec);
30609
- 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;
30610
30832
  const resolved = resolveIndexPath(file2, projectRoot ?? process.cwd());
30611
30833
  if (forceRefresh === true) {
30612
30834
  indexFileSync(resolved, globalDbPath());
@@ -30623,9 +30845,10 @@ function resolveSymbolSpec(spec, forceRefresh, projectRoot) {
30623
30845
  seen.add(key);
30624
30846
  distinct.push(c);
30625
30847
  }
30626
- if (distinct.length === 0) return { kind: "none" };
30627
- if (distinct.length === 1) return { kind: "ok", entry: distinct[0] };
30628
- 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 };
30629
30852
  };
30630
30853
  if (symbol3.includes(".")) {
30631
30854
  const exactMatch = querySymbols({ name: symbol3, filePath: resolved, limit: 10 });
@@ -30705,6 +30928,8 @@ function runRead(opts) {
30705
30928
  return runLineRange({ file: file2, start: lineSpec.start, end: lineSpec.end }, opts);
30706
30929
  }
30707
30930
  const messages = [`Symbol '${symbol3}' not found in '${file2}'`];
30931
+ const crossFileLead = formatCrossFileLead("read", symbol3, file2, opts.projectRoot);
30932
+ if (crossFileLead !== "") messages.push(crossFileLead);
30708
30933
  const resolved = resolveIndexPath(file2, opts.projectRoot ?? process.cwd());
30709
30934
  const closes = querySymbols({ filePath: resolved, limit: DIDYOUMEAN_LIMIT }).map((s) => s.name);
30710
30935
  if (closes.length > 0) messages.push(didYouMean(closes));
@@ -30765,6 +30990,8 @@ ${sub.text}`);
30765
30990
  return { text, code: 1 };
30766
30991
  }
30767
30992
  function runSection(opts) {
30993
+ const crossFilePairs = parseCrossFileMultiSpec(opts.spec);
30994
+ if (crossFilePairs !== null) return runSectionCrossFile(crossFilePairs, opts);
30768
30995
  const colonIdx = findSpecSeparator(opts.spec);
30769
30996
  if (colonIdx === -1) {
30770
30997
  return { text: `Invalid section spec \u2014 expected "file::Heading", got: ${opts.spec}`, code: 1 };
@@ -30821,6 +31048,42 @@ ${sub.text}`);
30821
31048
  if (anyFound) recordReadStat("section_read", fullSourceBytes, text, opts.spec);
30822
31049
  return { text, code: anyFound ? 0 : 1 };
30823
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
+ }
30824
31087
  function applyTypedRefsTier(symName, file2, results) {
30825
31088
  if (results.length === 0) return results;
30826
31089
  try {
@@ -30858,6 +31121,8 @@ function runRefs(opts) {
30858
31121
  emitErr2(`--top must be a positive number, got: ${opts.top}`);
30859
31122
  return 1;
30860
31123
  }
31124
+ const crossFilePairs = parseCrossFileMultiSpec(opts.spec);
31125
+ if (crossFilePairs !== null) return runRefsCrossFile(crossFilePairs, opts);
30861
31126
  const { file: file2, symbols } = parseMultiRefsSpec(opts.spec);
30862
31127
  if (symbols.length <= 1) return runRefsSingle(opts);
30863
31128
  const jsonOut = {};
@@ -30866,10 +31131,18 @@ function runRefs(opts) {
30866
31131
  const refFilePaths = [];
30867
31132
  for (const sym of symbols) {
30868
31133
  const queryOpts = { name: sym };
30869
- if (file2 !== void 0 && opts.callers !== true) queryOpts.filePath = resolveIndexPath(file2);
30870
- 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;
30871
31136
  else if (opts.top !== void 0) queryOpts.limit = REFS_TOP_SCAN_LIMIT;
30872
- 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
+ }
30873
31146
  if (results.length > 0) anyFound = true;
30874
31147
  refFilePaths.push(...results.map((r) => r.filePath));
30875
31148
  if (opts.json === true) {
@@ -30877,22 +31150,85 @@ function runRefs(opts) {
30877
31150
  jsonOut[sym] = topFilesJsonPayload(results, opts.top);
30878
31151
  } else {
30879
31152
  const capped = guardJsonRows(results);
30880
- const trueTotal = countRefs(queryOpts);
30881
- 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 };
30882
31155
  }
30883
31156
  continue;
30884
31157
  }
30885
31158
  if (results.length === 0) {
30886
- 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)`);
30887
31160
  continue;
30888
31161
  }
30889
31162
  lines2.push(`${sym}:`);
30890
31163
  if (opts.top !== void 0) {
30891
- lines2.push(...renderTopFilesSummary(results, opts.top));
31164
+ lines2.push(...renderTopFilesSummary(results, opts.top, void 0, suppressed));
31165
+ } else if (opts.callers === true) {
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));
31168
+ } else {
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));
30892
31226
  } else if (opts.callers === true) {
30893
- lines2.push(...renderCallerGroups(results));
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));
30894
31229
  } else {
30895
- for (const ref2 of results) lines2.push(` ${ref2.filePath}:${ref2.line}: ${ref2.context}`);
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));
30896
31232
  }
30897
31233
  }
30898
31234
  const fullSourceBytes = sumFileSizes(refFilePaths);
@@ -30912,11 +31248,23 @@ function runRefsSingle(opts) {
30912
31248
  const symName = symbol3 ?? file2;
30913
31249
  const queryOpts = { name: symName };
30914
31250
  const defFileHint = symbol3 !== void 0 ? resolveIndexPath(file2) : void 0;
30915
- if (defFileHint !== void 0 && opts.callers !== true) queryOpts.filePath = defFileHint;
30916
- 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;
30917
31253
  else if (opts.top !== void 0) queryOpts.limit = REFS_TOP_SCAN_LIMIT;
30918
- 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
+ }
30919
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
+ }
30920
31268
  emitErr2(`No references found for '${symName}'`);
30921
31269
  return 1;
30922
31270
  }
@@ -30927,8 +31275,8 @@ function runRefsSingle(opts) {
30927
31275
  payload = topFilesJsonPayload(results, opts.top);
30928
31276
  } else {
30929
31277
  const capped = guardJsonRows(results);
30930
- const trueTotal = countRefs(queryOpts);
30931
- 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 };
30932
31280
  }
30933
31281
  const text2 = JSON.stringify(payload, null, 2);
30934
31282
  emit3(text2);
@@ -30936,21 +31284,26 @@ function runRefsSingle(opts) {
30936
31284
  return 0;
30937
31285
  }
30938
31286
  const displayRoot = getDisplayRoot();
30939
- 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, ""))];
30940
31288
  const text = lines2.join("\n");
30941
31289
  emitGuarded(text, "symbol");
30942
31290
  recordReadStat("symbol_read", fullSourceBytes, text, symName);
30943
31291
  return 0;
30944
31292
  }
31293
+ function applyExcludeTestsFilter(refs) {
31294
+ const filtered = refs.filter((r) => !isTestFile(r.filePath));
31295
+ return { refs: filtered, suppressed: refs.length - filtered.length };
31296
+ }
30945
31297
  function groupRefsByFile(refs) {
30946
31298
  const byFile = /* @__PURE__ */ new Map();
30947
31299
  for (const ref2 of refs) byFile.set(ref2.filePath, (byFile.get(ref2.filePath) ?? 0) + 1);
30948
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));
30949
31301
  }
30950
- function renderTopFilesSummary(refs, topN, displayRoot) {
31302
+ function renderTopFilesSummary(refs, topN, displayRoot, suppressed) {
30951
31303
  const grouped = groupRefsByFile(refs);
30952
31304
  const shown = grouped.slice(0, topN);
30953
- 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}`];
30954
31307
  for (const { file: file2, count } of shown) lines2.push(` ${count} ${toDisplayPath(displayRoot, file2)}`);
30955
31308
  const omittedFiles = grouped.length - shown.length;
30956
31309
  if (omittedFiles > 0) {
@@ -30964,7 +31317,7 @@ function topFilesJsonPayload(refs, topN) {
30964
31317
  const shown = grouped.slice(0, topN);
30965
31318
  return { fileCounts: shown, totalFiles: grouped.length, totalRefs: refs.length, shown: shown.length };
30966
31319
  }
30967
- function renderCallerGroups(refs, displayRoot) {
31320
+ function renderCallerGroups(refs, displayRoot, contextLines = 0) {
30968
31321
  const byFile = /* @__PURE__ */ new Map();
30969
31322
  for (const ref2 of refs) {
30970
31323
  const bucket = byFile.get(ref2.filePath);
@@ -30976,9 +31329,12 @@ function renderCallerGroups(refs, displayRoot) {
30976
31329
  }
30977
31330
  const lines2 = [];
30978
31331
  for (const [file2, fileRefs] of byFile) {
30979
- lines2.push(`${toDisplayPath(displayRoot, file2)}:`);
31332
+ const displayPath = toDisplayPath(displayRoot, file2);
31333
+ lines2.push(`${displayPath}:`);
30980
31334
  for (const ref2 of fileRefs) {
30981
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, "", " "));
30982
31338
  }
30983
31339
  }
30984
31340
  return lines2;
@@ -31017,7 +31373,19 @@ function prepareSymbolListing(file2, opts) {
31017
31373
  const fullSourceBytes = sumFileSizes([resolved]);
31018
31374
  return { kind: "ok", resolved, filtered, refCounts, fullSourceBytes, symbolsTruncated, trueSymbolCount };
31019
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
+ }
31020
31386
  function runSkeleton(opts) {
31387
+ const multiFiles = parseMultiFileSpec(opts.file);
31388
+ if (multiFiles !== null) return runPerFileListing(multiFiles, (file2) => runSkeleton({ ...opts, file: file2 }));
31021
31389
  const prep = prepareSymbolListing(opts.file, opts);
31022
31390
  if (prep.kind === "empty") {
31023
31391
  return { text: prep.text, code: 1 };
@@ -31053,6 +31421,8 @@ function runSkeleton(opts) {
31053
31421
  return { text, code: 0 };
31054
31422
  }
31055
31423
  function runOutline(opts) {
31424
+ const multiFiles = parseMultiFileSpec(opts.file);
31425
+ if (multiFiles !== null) return runPerFileListing(multiFiles, (file2) => runOutline({ ...opts, file: file2 }));
31056
31426
  const prep = prepareSymbolListing(opts.file, opts);
31057
31427
  if (prep.kind === "empty") {
31058
31428
  return { text: prep.text, code: 1 };
@@ -31627,10 +31997,13 @@ function runBriefCore(opts) {
31627
31997
  const resolution = resolveSymbolSpec(opts.spec);
31628
31998
  if (resolution.kind === "ambiguous") {
31629
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.
31630
32001
  text: formatAmbiguity(
31631
32002
  resolution.symbol,
31632
32003
  resolution.file,
31633
- resolution.candidates
32004
+ resolution.candidates,
32005
+ void 0,
32006
+ "brief"
31634
32007
  ),
31635
32008
  code: 1
31636
32009
  };
@@ -31653,7 +32026,7 @@ function runBriefCore(opts) {
31653
32026
  if (opts.json === true) {
31654
32027
  const result = {
31655
32028
  symbol: match2,
31656
- callers: shown,
32029
+ callers: (opts.context ?? 0) > 0 ? shown.map((c) => ({ ...c, contextLines: buildContextWindow(c.file, c.line, opts.context ?? 0) ?? [] })) : shown,
31657
32030
  totalCallers,
31658
32031
  truncated,
31659
32032
  section: section2
@@ -31672,7 +32045,10 @@ function runBriefCore(opts) {
31672
32045
  ];
31673
32046
  lines2.push(`Callers (${totalCallers}):`);
31674
32047
  for (const c of shown) {
31675
- 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, "", " "));
31676
32052
  }
31677
32053
  if (truncated) {
31678
32054
  lines2.push(` ...(${totalCallers - shown.length} more elided)`);
@@ -31704,11 +32080,40 @@ ${sub.text}`);
31704
32080
  if (anyFound) recordReadStat("brief_view", fullSourceBytes, text, opts.spec);
31705
32081
  return { text, code: anyFound ? 0 : 1 };
31706
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
+ }
31707
32105
  function runBrief(opts) {
31708
32106
  if (opts.limit !== void 0 && opts.limit <= 0) {
31709
32107
  emitErr2(`--limit must be a positive number, got: ${opts.limit}`);
31710
32108
  return 1;
31711
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
+ }
31712
32117
  const { file: file2, symbol: symbol3 } = parseReadSpec(opts.spec);
31713
32118
  if (symbol3 !== void 0 && symbol3 !== "" && symbol3.includes(",")) {
31714
32119
  const multiSymbols = symbol3.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
@@ -31800,6 +32205,34 @@ function parseDiffHunks(diffText) {
31800
32205
  }
31801
32206
  return hunksByFile;
31802
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
+ }
31803
32236
  function runChanged(opts = {}) {
31804
32237
  const ref2 = opts.ref ?? "HEAD~5";
31805
32238
  const cwd = opts.projectRoot ?? process.cwd();
@@ -31809,6 +32242,10 @@ function runChanged(opts = {}) {
31809
32242
  const result = runGit(["diff", ref2, "--name-only"], { cwd });
31810
32243
  if (result.exitCode !== 0) {
31811
32244
  emitErr2(`git diff failed: ${result.stderr}`);
32245
+ const hint = buildChangedRefHint(cwd, ref2);
32246
+ if (hint !== null) {
32247
+ emitErr2(hint);
32248
+ }
31812
32249
  return 1;
31813
32250
  }
31814
32251
  changedFiles = result.stdout.trim().split(/\r?\n/).filter(Boolean);
@@ -31914,13 +32351,16 @@ function resolveSymbolSpecOrEmitError(commandName, spec, projectRoot) {
31914
32351
  resolution.symbol,
31915
32352
  resolution.file,
31916
32353
  resolution.candidates,
31917
- projectRoot
32354
+ projectRoot,
32355
+ commandName
31918
32356
  )
31919
32357
  );
31920
32358
  return null;
31921
32359
  }
31922
32360
  if (resolution.kind === "none") {
31923
32361
  const messages = [`Symbol '${symbol3}' not found in '${file2}'`];
32362
+ const crossFileLead = formatCrossFileLead(commandName, symbol3, file2, projectRoot);
32363
+ if (crossFileLead !== "") messages.push(crossFileLead);
31924
32364
  const resolved = resolveIndexPath(file2, projectRoot ?? process.cwd());
31925
32365
  const closes = querySymbols({ filePath: resolved, limit: DIDYOUMEAN_LIMIT }).map((s) => s.name);
31926
32366
  if (closes.length > 0) messages.push(didYouMean(closes));
@@ -32130,22 +32570,29 @@ function runGrep(opts) {
32130
32570
  return 1;
32131
32571
  }
32132
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
+ }
32133
32585
  if (opts.json === true) {
32134
32586
  const payload = { items: truncated, truncated: hits.length > maxLines, totalCount: hits.length };
32135
32587
  emit3(JSON.stringify(payload, null, 2));
32136
32588
  return 0;
32137
32589
  }
32138
32590
  for (const hit of truncated) {
32591
+ const symbolTag = opts.symbol === true && hit.symbol != null ? ` [${hit.symbol.name} (${hit.symbol.kind})]` : "";
32139
32592
  if (hit.context !== void 0) {
32140
- for (const ctxLine of hit.context) {
32141
- if (ctxLine.line === hit.line) {
32142
- emit3(`${hit.file}:${ctxLine.line}: ${ctxLine.text}`);
32143
- } else {
32144
- emit3(`${hit.file}-${ctxLine.line}- ${ctxLine.text}`);
32145
- }
32146
- }
32593
+ for (const line of renderContextWindow(hit.file, hit.line, hit.context, symbolTag)) emit3(line);
32147
32594
  } else {
32148
- emit3(`${hit.file}:${hit.line}: ${hit.text}`);
32595
+ emit3(`${hit.file}:${hit.line}: ${hit.text}${symbolTag}`);
32149
32596
  }
32150
32597
  }
32151
32598
  if (hits.length > maxLines) {
@@ -32328,9 +32775,24 @@ function extractExportNames(text, ext2) {
32328
32775
  }
32329
32776
  return names;
32330
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
+ }
32331
32787
  function runExports(opts) {
32788
+ const multiFiles = parseMultiFileSpec(opts.file);
32789
+ if (multiFiles !== null) return runPerFileEmitting(multiFiles, "Exports", (file2) => runExports({ ...opts, file: file2 }));
32332
32790
  const symbols = querySymbols({ filePath: resolveIndexPath(opts.file), limit: 500 });
32333
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
+ };
32334
32796
  const names = [];
32335
32797
  for (const s of symbols) {
32336
32798
  if (/^(?:export|pub\b|public\b)/.test(s.body.trimStart()) && !names.includes(s.name)) {
@@ -32355,12 +32817,23 @@ function runExports(opts) {
32355
32817
  }
32356
32818
  const fullSourceBytes = sumFileSizes([opts.file]);
32357
32819
  if (opts.json === true) {
32358
- 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
+ );
32359
32828
  emit3(jsonText);
32360
32829
  recordReadStat("exports", fullSourceBytes, jsonText, opts.file);
32361
32830
  return 0;
32362
32831
  }
32363
- 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
+ });
32364
32837
  for (const line of outLines) {
32365
32838
  emit3(line);
32366
32839
  }
@@ -32654,6 +33127,8 @@ function importsExtensionFor(filePath) {
32654
33127
  return path48.extname(filePath);
32655
33128
  }
32656
33129
  function runImports(opts) {
33130
+ const multiFiles = parseMultiFileSpec(opts.file);
33131
+ if (multiFiles !== null) return runPerFileEmitting(multiFiles, "Imports", (file2) => runImports({ ...opts, file: file2 }));
32657
33132
  const text = readFileText(opts.file);
32658
33133
  if (text === null) {
32659
33134
  emitErr2(`Could not read: ${opts.file}`);
@@ -32747,11 +33222,12 @@ async function runSemantic(query, opts) {
32747
33222
  );
32748
33223
  const hits = mergeNearbyHits(rawHits).slice(0, n);
32749
33224
  if (hits.length > 0) {
33225
+ const enclosing = hits.map((h) => resolveEnclosingSymbol(h.filePath, h.startLine));
32750
33226
  if (opts.json === true) {
32751
- const items = hits.map((h) => ({
33227
+ const items = hits.map((h, i) => ({
32752
33228
  filePath: h.filePath,
32753
- name: null,
32754
- kind: null,
33229
+ name: enclosing[i]?.name ?? null,
33230
+ kind: enclosing[i]?.kind ?? null,
32755
33231
  startLine: h.startLine,
32756
33232
  endLine: h.endLine,
32757
33233
  distance: h.distance,
@@ -32762,10 +33238,12 @@ async function runSemantic(query, opts) {
32762
33238
  recordReadStat("semantic_search", sumFileSizes(hits.map((h) => h.filePath)), text3, query);
32763
33239
  return { text: text3, code: 0 };
32764
33240
  }
32765
- const blocks2 = hits.map(
32766
- (h) => `# ${toDisplayPath(rootDir, h.filePath)}:${h.startLine}-${h.endLine} (distance ${h.distance.toFixed(3)})
32767
- ${previewLines(h.text, 3)}`
32768
- );
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
+ });
32769
33247
  const text2 = guardText(blocks2.join("\n\n"), "semantic");
32770
33248
  recordReadStat("semantic_search", sumFileSizes(hits.map((h) => h.filePath)), text2, query);
32771
33249
  return { text: text2, code: 0 };
@@ -32854,7 +33332,7 @@ function runNoteList(opts = {}) {
32854
33332
  });
32855
33333
  return { text: lines2.join("\n"), code: 0 };
32856
33334
  }
32857
- 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;
32858
33336
  var init_read_commands = __esm({
32859
33337
  "src/read_commands.ts"() {
32860
33338
  "use strict";
@@ -32899,6 +33377,7 @@ var init_read_commands = __esm({
32899
33377
  PARENT_IDENTIFIER_RE = /^[\w$]+$/;
32900
33378
  SKELETON_SYMBOL_CAP = 5e3;
32901
33379
  HUNK_HEADER_RE = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/;
33380
+ EMPTY_TREE_HASH = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
32902
33381
  DEFAULT_LOG_MAX_COUNT = 20;
32903
33382
  }
32904
33383
  });
@@ -48689,6 +49168,7 @@ init_index_reader();
48689
49168
  init_parser_types();
48690
49169
  init_doc_embed_extract();
48691
49170
  init_paths();
49171
+ init_project();
48692
49172
  init_hooks_index();
48693
49173
 
48694
49174
  // src/relay.ts
@@ -49089,7 +49569,7 @@ init_util2();
49089
49569
  init_disk_cache();
49090
49570
  init_lang_patterns();
49091
49571
  import { readdirSync as readdirSync13, readFileSync as readFileSync24, statSync as statSync17 } from "fs";
49092
- import { resolve as resolve12 } from "path";
49572
+ import { resolve as resolve13 } from "path";
49093
49573
 
49094
49574
  // src/recall_index.ts
49095
49575
  init_define_import_meta_env();
@@ -49322,7 +49802,7 @@ function depLockfileFingerprintSync(cmd, cwd) {
49322
49802
  if (!candidates) return null;
49323
49803
  for (const lockfile of candidates) {
49324
49804
  try {
49325
- const content = readFileSync24(resolve12(cwd, lockfile));
49805
+ const content = readFileSync24(resolve13(cwd, lockfile));
49326
49806
  return shortFingerprint(content);
49327
49807
  } catch {
49328
49808
  continue;
@@ -49475,7 +49955,7 @@ function extractFirstPathArg(cmd, cwd, fallback) {
49475
49955
  const token2 = tokens[i];
49476
49956
  if (!token2.startsWith("-")) {
49477
49957
  if (!token2.startsWith("/")) {
49478
- return resolve12(cwd, token2);
49958
+ return resolve13(cwd, token2);
49479
49959
  }
49480
49960
  return token2;
49481
49961
  }
@@ -50023,7 +50503,7 @@ function checkDbExists(dataDir2) {
50023
50503
  return {
50024
50504
  name: "Database",
50025
50505
  status: "ok",
50026
- message: `global.db exists (${toKB(sizeBytes)} KB)`
50506
+ message: `global.db exists (${toKB(sizeBytes)} KB) at ${dbPath}`
50027
50507
  };
50028
50508
  }
50029
50509
  function checkSymbolBodySize(dbPath) {
@@ -50071,6 +50551,13 @@ function checkSymbolCount(dbPath, rootDir) {
50071
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'`
50072
50552
  };
50073
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
+ }
50074
50561
  return {
50075
50562
  name: "Symbols",
50076
50563
  status: "ok",
@@ -72393,7 +72880,7 @@ init_image_shrink();
72393
72880
  var DEFAULT_STDIN_TIMEOUT_MS = 5e3;
72394
72881
  var MAX_STDIN_BYTES = 64 * 1024 * 1024;
72395
72882
  function readStdinJson(timeoutMs = DEFAULT_STDIN_TIMEOUT_MS, maxBytes = MAX_STDIN_BYTES) {
72396
- return new Promise((resolve24, reject) => {
72883
+ return new Promise((resolve25, reject) => {
72397
72884
  const chunks = [];
72398
72885
  let totalBytes = 0;
72399
72886
  let settled = false;
@@ -72428,7 +72915,7 @@ function readStdinJson(timeoutMs = DEFAULT_STDIN_TIMEOUT_MS, maxBytes = MAX_STDI
72428
72915
  return;
72429
72916
  }
72430
72917
  try {
72431
- resolve24(JSON.parse(text));
72918
+ resolve25(JSON.parse(text));
72432
72919
  } catch (err2) {
72433
72920
  reject(err2 instanceof Error ? err2 : new Error(String(err2)));
72434
72921
  }
@@ -73878,15 +74365,19 @@ var BRIDGE_CAPABILITY_MATRIX = [
73878
74365
  harness: "copilot_cli",
73879
74366
  label: "Copilot CLI",
73880
74367
  sourceFile: "src/bridges/copilot_cli_install.ts (COPILOT_CLI_HOOK_EVENTS), src/bridges/copilot_cli.ts (COPILOT_TO_TG_EVENT)",
73881
- implemented: /* @__PURE__ */ new Set(["pre_tool_use", "post_tool_use", "pre_compact", "stop", "subagent_stop", "user_prompt_submit"]),
74368
+ implemented: /* @__PURE__ */ new Set([
74369
+ "session_start",
74370
+ "pre_tool_use",
74371
+ "post_tool_use",
74372
+ "pre_compact",
74373
+ "stop",
74374
+ "subagent_stop",
74375
+ "user_prompt_submit"
74376
+ ]),
73882
74377
  reasons: [
73883
74378
  {
73884
74379
  events: ["notification"],
73885
74380
  reason: "Copilot CLI has a real 'notification' hook event, but copilot_cli.ts's COPILOT_TO_TG_EVENT deliberately leaves it (and sessionEnd/postToolUseFailure/subagentStart/errorOccurred/permissionRequest) unimplemented rather than guessed at"
73886
- },
73887
- {
73888
- events: ["session_start"],
73889
- reason: "COPILOT_TO_TG_EVENT has no session-start mapping wired yet -- left unimplemented rather than guessed at"
73890
74381
  }
73891
74382
  ]
73892
74383
  },
@@ -78722,7 +79213,7 @@ import { existsSync as existsSync34, readdirSync as readdirSync20, statSync as s
78722
79213
  import * as http from "http";
78723
79214
  import * as https from "https";
78724
79215
  import { isIPv4, isIPv6 } from "net";
78725
- import { resolve as resolve17, join as join41 } from "path";
79216
+ import { resolve as resolve18, join as join41 } from "path";
78726
79217
  import { URL as URL2 } from "url";
78727
79218
  import { promisify } from "util";
78728
79219
  import { lookup as dnsLookup } from "dns";
@@ -78894,7 +79385,7 @@ function cleanupStaleDownloads() {
78894
79385
  const files = readdirSync20(cacheDir);
78895
79386
  for (const file2 of files) {
78896
79387
  if (file2.endsWith(".tmp")) {
78897
- const filePath = resolve17(cacheDir, file2);
79388
+ const filePath = resolve18(cacheDir, file2);
78898
79389
  try {
78899
79390
  const stat2 = statSync27(filePath);
78900
79391
  if (Date.now() - stat2.mtimeMs < STALE_DOWNLOAD_AGE_MS) continue;
@@ -81171,6 +81662,9 @@ function runHintStatsCommand(opts = {}) {
81171
81662
  `);
81172
81663
  return;
81173
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
+ }
81174
81668
  printSummary(rows);
81175
81669
  }
81176
81670
 
@@ -81429,7 +81923,11 @@ async function cmdIndex(pathArg, opts = {}) {
81429
81923
  function cmdMap(opts) {
81430
81924
  const map3 = buildProjectMap(process.cwd(), { compact: opts.compact === true });
81431
81925
  const text = formatProjectMap(map3, map3.compact);
81432
- out(text);
81926
+ if (opts.json === true) {
81927
+ out(JSON.stringify(map3));
81928
+ } else {
81929
+ out(text);
81930
+ }
81433
81931
  const bytesSaved = mapLookupBytesSaved(map3, text);
81434
81932
  recordStat("map_lookup", bytesSaved, Math.round(bytesSaved / 4));
81435
81933
  }
@@ -81471,8 +81969,8 @@ async function cmdMcpServe() {
81471
81969
  const server = createMcpServer2();
81472
81970
  const transport = new StdioServerTransport();
81473
81971
  await server.connect(transport);
81474
- await new Promise((resolve24) => {
81475
- server.server.onclose = resolve24;
81972
+ await new Promise((resolve25) => {
81973
+ server.server.onclose = resolve25;
81476
81974
  });
81477
81975
  }
81478
81976
  async function cmdHook(event, opts) {
@@ -81679,6 +82177,14 @@ async function cmdDoctor(opts) {
81679
82177
  if (project !== null) {
81680
82178
  doctorOpts.rootDir = project.root;
81681
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
+ }
81682
82188
  const code = await runDoctorAndExit(doctorOpts);
81683
82189
  if (code !== 0) {
81684
82190
  throw new CliError("doctor checks failed");
@@ -82182,6 +82688,16 @@ function runExitText(fn) {
82182
82688
  process.exitCode = 1;
82183
82689
  }
82184
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
+ }
82185
82701
  function cmdCompress(opts) {
82186
82702
  try {
82187
82703
  if (opts.compress === false) {
@@ -82306,6 +82822,10 @@ async function cmdSkillList(opts) {
82306
82822
  return `${s.name.padEnd(25)} ${bodyKb.padStart(6)}K ${compactKb.padStart(6)}K ${marker} ${s.hitCount.toString().padStart(3)} ${age.padStart(3)} ${staleStatus}`;
82307
82823
  });
82308
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
+ }
82309
82829
  out([header, ...lines2].join("\n"));
82310
82830
  }
82311
82831
  }
@@ -82684,7 +83204,7 @@ function cmdWriteFile(dest, opts) {
82684
83204
  throw new CliError(`TOKEN_GOAT_MAX_STDIN_MB must be a positive integer; got '${process.env["TOKEN_GOAT_MAX_STDIN_MB"] ?? ""}'`);
82685
83205
  }
82686
83206
  const maxBytes = maxMB * 1024 * 1024;
82687
- return new Promise((resolve24, reject) => {
83207
+ return new Promise((resolve25, reject) => {
82688
83208
  const chunks = [];
82689
83209
  let totalBytes = 0;
82690
83210
  let settled = false;
@@ -82708,7 +83228,7 @@ function cmdWriteFile(dest, opts) {
82708
83228
  try {
82709
83229
  atomicWriteBuffer(dest, Buffer.concat(chunks));
82710
83230
  enqueueDirtyPathSafe(dest);
82711
- resolve24();
83231
+ resolve25();
82712
83232
  } catch (e) {
82713
83233
  try {
82714
83234
  mapFsError(e, void 0, dest);
@@ -83143,19 +83663,26 @@ function buildProgram() {
83143
83663
  process.exitCode = 1;
83144
83664
  }
83145
83665
  };
83146
- 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(
83147
- (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(
83148
83674
  () => runSymbol({
83149
83675
  name: name2,
83150
83676
  limit: opts.limit !== void 0 ? requireNonNegativeInt("--limit", opts.limit) : 20,
83151
83677
  ...opts.file !== void 0 ? { file: opts.file } : {},
83152
83678
  ...opts.kind !== void 0 ? { kind: opts.kind } : {},
83679
+ ...projectRoot !== void 0 ? { projectRoot } : {},
83153
83680
  ...opts.json === true ? { json: true } : {}
83154
83681
  })
83155
- )
83156
- );
83682
+ );
83683
+ });
83157
83684
  program2.command("read <spec>").description(
83158
- "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)"
83159
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(
83160
83687
  (spec, opts) => runExitText(
83161
83688
  () => runRead({
@@ -83167,13 +83694,14 @@ function buildProgram() {
83167
83694
  )
83168
83695
  );
83169
83696
  program2.command("brief <spec>").description(
83170
- "symbol body + callers + containing doc section in one call (spec: file::symbol; comma-separated file::a,b for a merged multi-symbol view)"
83171
- ).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(
83172
83699
  (spec, opts) => runExit(
83173
83700
  () => runBrief({
83174
83701
  spec,
83175
83702
  ...opts.json === true ? { json: true } : {},
83176
- ...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) } : {}
83177
83705
  })
83178
83706
  )
83179
83707
  );
@@ -83183,44 +83711,56 @@ function buildProgram() {
83183
83711
  (spec, opts) => opts.list === true ? runExit(() => runListSections({ file: spec, ...opts.json === true ? { json: true } : {} })) : runExitText(() => runSection({ spec, ...opts.json === true ? { json: true } : {} }))
83184
83712
  );
83185
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));
83186
- 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(
83187
- (file2, opts) => runExitText(
83188
- () => runSkeleton({
83189
- file: file2,
83190
- ...opts.json === true ? { json: true } : {},
83191
- ...opts.minLines !== void 0 ? { minLines: requireNonNegativeInt("--min-lines", opts.minLines) } : {},
83192
- ...opts.forceRefresh === true ? { forceRefresh: true } : {},
83193
- ...opts.stats === true ? { stats: true } : {}
83194
- })
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
+ )
83195
83728
  )
83196
83729
  );
83197
- 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(
83198
- (file2, opts) => runExitText(
83199
- () => runOutline({
83200
- file: file2,
83201
- ...opts.json === true ? { json: true } : {},
83202
- ...opts.minLines !== void 0 ? { minLines: requireNonNegativeInt("--min-lines", opts.minLines) } : {},
83203
- ...opts.forceRefresh === true ? { forceRefresh: true } : {},
83204
- ...opts.stats === true ? { stats: true } : {}
83205
- })
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
+ )
83206
83744
  )
83207
83745
  );
83208
- 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(
83209
83747
  "--top <n>",
83210
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"
83211
- ).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(
83212
83750
  (spec, opts) => runExit(
83213
83751
  () => runRefs({
83214
83752
  spec,
83215
83753
  ...opts.callers === true ? { callers: true } : {},
83216
83754
  ...opts.json === true ? { json: true } : {},
83217
83755
  ...opts.limit !== void 0 ? { limit: requireNonNegativeInt("--limit", opts.limit) } : {},
83218
- ...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 } : {}
83219
83759
  })
83220
83760
  )
83221
83761
  );
83222
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));
83223
- 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));
83224
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));
83225
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));
83226
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));
@@ -83236,7 +83776,7 @@ function buildProgram() {
83236
83776
  worker.command("stop").description("stop the background indexer").action(guard(cmdWorkerStop));
83237
83777
  worker.command("status").description("check if the indexer is running").action(guard(cmdWorkerStatus));
83238
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));
83239
- 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));
83240
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));
83241
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));
83242
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));
@@ -83250,11 +83790,17 @@ function buildProgram() {
83250
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));
83251
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));
83252
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));
83253
- program2.command("exports <file>").description("list exported (public) symbols in a file").option("-j, --json", "output as JSON").action(
83254
- (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
+ })
83255
83798
  );
83256
- program2.command("imports <file>").description("list the modules a file imports").option("-j, --json", "output as JSON").action(
83257
- (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
+ })
83258
83804
  );
83259
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(
83260
83806
  (pattern, opts) => runExit(
@@ -83265,7 +83811,7 @@ function buildProgram() {
83265
83811
  })
83266
83812
  )
83267
83813
  );
83268
- 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(
83269
83815
  (pattern, paths, opts) => runExit(
83270
83816
  () => runGrep({
83271
83817
  pattern,
@@ -83273,7 +83819,8 @@ function buildProgram() {
83273
83819
  ...opts.json === true ? { json: true } : {},
83274
83820
  ...opts.maxLines !== void 0 ? { maxLines: requirePositiveInt("--max-lines", opts.maxLines) } : {},
83275
83821
  ...opts.recursive === false ? { recursive: false } : {},
83276
- ...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 } : {}
83277
83824
  })
83278
83825
  )
83279
83826
  );
@@ -83284,16 +83831,18 @@ function buildProgram() {
83284
83831
  program2.command("skill-history").description("list cached skill versions newest-first").option("-j, --json", "output as JSON").action(guard(cmdSkillHistory));
83285
83832
  program2.command("skill-diff <name>").description("show diff between two cached versions of a skill").action(guard(cmdSkillDiff));
83286
83833
  program2.command("skill-section <nameHeading> [headingArg]").description("extract a named section from a skill").action(guard(cmdSkillSection));
83287
- 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(
83288
83835
  (symbol3, opts) => runExit(
83289
83836
  () => runCallers({
83290
83837
  symbol: symbol3,
83291
83838
  ...opts.json === true ? { json: true } : {},
83292
- ...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 } : {}
83293
83842
  })
83294
83843
  )
83295
83844
  );
83296
- 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(
83297
83846
  (symbol3, opts) => runExit(
83298
83847
  () => runCallChain({
83299
83848
  symbol: symbol3,
@@ -83302,7 +83851,7 @@ function buildProgram() {
83302
83851
  })
83303
83852
  )
83304
83853
  );
83305
- 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(
83306
83855
  (symbol3, opts) => runExit(
83307
83856
  () => runImpact({
83308
83857
  symbol: symbol3,
@@ -83311,13 +83860,14 @@ function buildProgram() {
83311
83860
  })
83312
83861
  )
83313
83862
  );
83314
- 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(
83315
83864
  (opts) => runExit(
83316
83865
  () => runDead({
83317
83866
  ...opts.kind !== void 0 ? { kind: opts.kind } : {},
83318
83867
  ...opts.includePrivate === true ? { includePrivate: true } : {},
83319
83868
  ...opts.top !== void 0 ? { top: requireNonNegativeInt("--top", opts.top) } : {},
83320
- ...opts.json === true ? { json: true } : {}
83869
+ ...opts.json === true ? { json: true } : {},
83870
+ ...opts.excludeTests === true ? { excludeTests: true } : {}
83321
83871
  })
83322
83872
  )
83323
83873
  );
@@ -83336,7 +83886,7 @@ function buildProgram() {
83336
83886
  program2.command("scope <fileColonLine>").description("list symbols enclosing a file:line position, innermost first").option("-j, --json", "output as JSON").action(
83337
83887
  (spec, opts) => runExit(() => runScope({ spec, ...opts.json === true ? { json: true } : {} }))
83338
83888
  );
83339
- 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(
83340
83890
  (spec, opts) => runExit(
83341
83891
  () => runSimilar({
83342
83892
  spec,
@@ -83375,7 +83925,7 @@ function buildProgram() {
83375
83925
  })
83376
83926
  )
83377
83927
  );
83378
- 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(
83379
83929
  (spec, opts) => runExit(() => runBlame({ spec, ...opts.json === true ? { json: true } : {} }))
83380
83930
  );
83381
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(
@@ -83459,16 +84009,16 @@ function buildProgram() {
83459
84009
  }))());
83460
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 } : {} }))());
83461
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))());
83462
- 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(
83463
- (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(
83464
84014
  () => runChanged({
83465
- ref: opts.since ?? "HEAD~5",
84015
+ ref: opts.since ?? ref2 ?? "HEAD~5",
83466
84016
  ...opts.symbol === true ? { symbolMode: true } : {},
83467
84017
  ...opts.json === true ? { json: true } : {}
83468
84018
  })
83469
84019
  )
83470
84020
  );
83471
- 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(
83472
84022
  (spec, ref2, opts) => runExit(
83473
84023
  () => runDiff({
83474
84024
  spec,
@@ -83477,7 +84027,7 @@ function buildProgram() {
83477
84027
  })
83478
84028
  )
83479
84029
  );
83480
- 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(
83481
84031
  (spec, ref2, opts) => runExit(
83482
84032
  () => runLog({
83483
84033
  spec,
@@ -83610,6 +84160,8 @@ async function run2(argv = process.argv) {
83610
84160
  }
83611
84161
 
83612
84162
  // src/main.ts
84163
+ init_util2();
84164
+ installEpipeGuard();
83613
84165
  void run2();
83614
84166
  /*! Bundled license information:
83615
84167