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.
@@ -49,7 +49,7 @@ var init_define_import_meta_env = __esm({
49
49
  import { createRequire } from "node:module";
50
50
  function resolveVersion() {
51
51
  if (true) {
52
- return "2.6.23";
52
+ return "2.6.25";
53
53
  }
54
54
  const require2 = createRequire(import.meta.url);
55
55
  const pkg = require2("../package.json");
@@ -1464,12 +1464,12 @@ function normalizeDarwinSystemAlias(p) {
1464
1464
  }
1465
1465
  function resolveIndexPath(file2, base = process.cwd()) {
1466
1466
  const isWindowsAbsolute = (s) => /^[a-zA-Z]:[/\\]/.test(s);
1467
- const resolve24 = isWindowsAbsolute(file2) || isWindowsAbsolute(base) ? path2.win32.resolve : path2.resolve;
1468
- return normalizePath(resolve24(base, file2));
1467
+ const resolve25 = isWindowsAbsolute(file2) || isWindowsAbsolute(base) ? path2.win32.resolve : path2.resolve;
1468
+ return normalizePath(resolve25(base, file2));
1469
1469
  }
1470
1470
  function toDisplayPath(root, target) {
1471
1471
  if (root === void 0) return target;
1472
- const rel = path2.relative(root, target).replace(/\\/g, "/");
1472
+ const rel = path2.relative(normalizePath(root), normalizePath(target)).replace(/\\/g, "/");
1473
1473
  if (rel === "" || rel.startsWith("..") || path2.isAbsolute(rel)) {
1474
1474
  return rel === "" ? "." : target;
1475
1475
  }
@@ -1531,6 +1531,15 @@ function foldCase(s) {
1531
1531
  function runGit(args, opts = {}) {
1532
1532
  const subArgs = args[0] === "diff" ? [args[0], "--no-ext-diff", "--no-textconv", ...args.slice(1)] : args;
1533
1533
  const fullArgs = [
1534
+ // Never take an optional lock. Every git call here is on someone else's
1535
+ // working repo, and a `status` that refreshes the index writes
1536
+ // `.git/index.lock`; if this process is killed mid-call -- which the hint
1537
+ // paths deliberately invite, since they spawn under a short timeout -- the
1538
+ // orphaned lock blocks every subsequent commit in that repo until a human
1539
+ // deletes it. Observed doing exactly that on 2026-08-05. `--no-optional-
1540
+ // locks` suppresses only locks git considers optional, so write commands
1541
+ // that genuinely need one are unaffected.
1542
+ "--no-optional-locks",
1534
1543
  "-c",
1535
1544
  "core.fsmonitor=",
1536
1545
  "-c",
@@ -1854,6 +1863,28 @@ function writeIfDifferent(p, content, backup = false) {
1854
1863
  atomicWriteText(p, content);
1855
1864
  return true;
1856
1865
  }
1866
+ function buildContextWindow(absPath, line, contextLines) {
1867
+ if (!Number.isFinite(contextLines) || contextLines <= 0) return null;
1868
+ let text;
1869
+ try {
1870
+ text = readFileSync(absPath, "utf-8");
1871
+ } catch {
1872
+ return null;
1873
+ }
1874
+ const lines2 = text.split(/\r?\n/);
1875
+ const idx = line - 1;
1876
+ if (idx < 0 || idx >= lines2.length) return null;
1877
+ const start = Math.max(0, idx - contextLines);
1878
+ const end = Math.min(lines2.length - 1, idx + contextLines);
1879
+ const out2 = [];
1880
+ for (let i = start; i <= end; i++) out2.push({ line: i + 1, text: lines2[i] ?? "" });
1881
+ return out2;
1882
+ }
1883
+ function renderContextWindow(displayFile, matchLine3, window, matchSuffix = "", indent = "") {
1884
+ return window.map(
1885
+ (c) => c.line === matchLine3 ? `${indent}${displayFile}:${c.line}: ${c.text}${matchSuffix}` : `${indent}${displayFile}-${c.line}- ${c.text}`
1886
+ );
1887
+ }
1857
1888
  function toKB(bytes) {
1858
1889
  return Math.round(bytes / 1024);
1859
1890
  }
@@ -2653,14 +2684,23 @@ const path = require('node:path')
2653
2684
  const { pathToFileURL } = require('node:url')
2654
2685
 
2655
2686
  // Copilot event name -> token-goat internal HookEventName (src/types.ts's
2656
- // HOOK_EVENTS). Only these six have a token-goat handler; every other real
2687
+ // HOOK_EVENTS). Only these seven have a token-goat handler; every other real
2657
2688
  // Copilot event (sessionEnd, postToolUseFailure, subagentStart,
2658
2689
  // errorOccurred, notification, permissionRequest) is left unimplemented
2659
2690
  // rather than guessed at, and falls through to the default no-op below.
2660
- // 'sessionStart' is handled as a permanent no-op even though it's a real
2661
- // Copilot event, because token-goat has no internal session_start handler
2662
- // (mirrors PI_EXTENSION_SCRIPT's documented precedent).
2691
+ // 'sessionStart' was previously a permanent no-op on the stated grounds that
2692
+ // token-goat has no internal session_start handler. That was simply wrong --
2693
+ // hooks_session_start.ts has long emitted the command-routing reminder that
2694
+ // every other harness receives -- and the no-op was the reason Copilot CLI
2695
+ // sessions alone never got told token-goat exists. It is wired now: verified
2696
+ // against Copilot CLI 1.0.77 that a hooks.json sessionStart entry returning
2697
+ // {additionalContext} does reach the model. The github/copilot-cli#2142
2698
+ // fire-and-forget bug that would have made this dead wiring was fixed in a
2699
+ // pre-release months before that version, and its companion multi-extension
2700
+ // hook-overwrite bug never applied here: it hit runtime *extension* hooks,
2701
+ // while this config-file hooks.json path goes through Copilot's own merge.
2663
2702
  const COPILOT_TO_TG_EVENT = {
2703
+ sessionStart: 'session_start',
2664
2704
  preToolUse: 'pre_tool_use',
2665
2705
  postToolUse: 'post_tool_use',
2666
2706
  preCompact: 'pre_compact',
@@ -2772,11 +2812,6 @@ async function tryInProcess(entryPath, tgEvent, canonical) {
2772
2812
  async function main() {
2773
2813
  const copilotEvent = process.argv[2] || ''
2774
2814
 
2775
- if (copilotEvent === 'sessionStart') {
2776
- process.stdout.write('{}')
2777
- return
2778
- }
2779
-
2780
2815
  const tgEvent = COPILOT_TO_TG_EVENT[copilotEvent]
2781
2816
  if (!tgEvent) {
2782
2817
  process.stdout.write('{}')
@@ -2902,7 +2937,10 @@ function translate(copilotEvent, resp) {
2902
2937
  return {}
2903
2938
  }
2904
2939
 
2905
- if (copilotEvent === 'postToolUse') {
2940
+ if (copilotEvent === 'postToolUse' || copilotEvent === 'sessionStart') {
2941
+ // Both surface token-goat's context through the same field. sessionStart is
2942
+ // the one channel that reaches the model before it picks its first read
2943
+ // tool, so this is where the routing reminder has to land.
2906
2944
  const context = extractContext(resp)
2907
2945
  if (context) return { additionalContext: context }
2908
2946
  return {}
@@ -2926,9 +2964,8 @@ function translate(copilotEvent, resp) {
2926
2964
  // doc that both are notification-only -- Copilot never reads a response
2927
2965
  // body for either, so any additionalContext/systemMessage token-goat
2928
2966
  // produces has no surfacing channel here. This still routes through the
2929
- // token-goat hook call above (unlike sessionStart's early no-op) so the
2930
- // internal handler's own side effects keep running; only the response is
2931
- // discarded.
2967
+ // token-goat hook call above so the internal handler's own side effects keep
2968
+ // running; only the response is discarded.
2932
2969
  return {}
2933
2970
  }
2934
2971
 
@@ -2968,8 +3005,13 @@ main()
2968
3005
  import * as fs4 from "node:fs";
2969
3006
  import * as os4 from "node:os";
2970
3007
  import * as path6 from "node:path";
3008
+ function copilotCliUserRoot() {
3009
+ const override = process.env["COPILOT_HOME"];
3010
+ if (override !== void 0 && override.trim() !== "") return path6.resolve(override);
3011
+ return path6.join(os4.homedir(), ".copilot");
3012
+ }
2971
3013
  function copilotCliUserHooksDir() {
2972
- return path6.join(os4.homedir(), ".copilot", "hooks");
3014
+ return path6.join(copilotCliUserRoot(), "hooks");
2973
3015
  }
2974
3016
  function copilotCliProjectHooksDir() {
2975
3017
  return path6.join(process.cwd(), ".github", "hooks");
@@ -3074,6 +3116,7 @@ var init_copilot_cli_install = __esm({
3074
3116
  init_copilot_cli();
3075
3117
  init_guidance_block();
3076
3118
  COPILOT_CLI_HOOK_EVENTS = [
3119
+ "sessionStart",
3077
3120
  "preToolUse",
3078
3121
  "postToolUse",
3079
3122
  "preCompact",
@@ -3712,7 +3755,8 @@ function defaultConfig() {
3712
3755
  compression: getDefaultConfig("compression"),
3713
3756
  context: getDefaultConfig("context"),
3714
3757
  injection: getDefaultConfig("injection"),
3715
- hint_stats: getDefaultConfig("hint_stats")
3758
+ hint_stats: getDefaultConfig("hint_stats"),
3759
+ semantic: getDefaultConfig("semantic")
3716
3760
  };
3717
3761
  }
3718
3762
  function validatedBool(raw, def) {
@@ -4154,6 +4198,10 @@ function _buildConfig(raw, projectRaw = {}) {
4154
4198
  const hs = getDefaultConfig("hint_stats");
4155
4199
  hs.suppress_threshold_pct = validatedInt(hs_raw["suppress_threshold_pct"], hs.suppress_threshold_pct, ...boundsOf("hint_stats.suppress_threshold_pct"));
4156
4200
  hs.min_sample_size = validatedInt(hs_raw["min_sample_size"], hs.min_sample_size, ...boundsOf("hint_stats.min_sample_size"));
4201
+ const sem_raw = section(raw, "semantic");
4202
+ const sem = getDefaultConfig("semantic");
4203
+ sem.archive_weight = validatedFloat(sem_raw["archive_weight"], sem.archive_weight, ...boundsOf("semantic.archive_weight"));
4204
+ sem.docs_weight = validatedFloat(sem_raw["docs_weight"], sem.docs_weight, ...boundsOf("semantic.docs_weight"));
4157
4205
  return {
4158
4206
  compact_assist: ca,
4159
4207
  bash_compress: bc,
@@ -4176,7 +4224,8 @@ function _buildConfig(raw, projectRaw = {}) {
4176
4224
  compression: cpr,
4177
4225
  context: ctx,
4178
4226
  injection: inj,
4179
- hint_stats: hs
4227
+ hint_stats: hs,
4228
+ semantic: sem
4180
4229
  };
4181
4230
  }
4182
4231
  function saveConfig(config2) {
@@ -4333,6 +4382,10 @@ function saveConfig(config2) {
4333
4382
  hint_stats: {
4334
4383
  suppress_threshold_pct: config2.hint_stats.suppress_threshold_pct,
4335
4384
  min_sample_size: config2.hint_stats.min_sample_size
4385
+ },
4386
+ semantic: {
4387
+ archive_weight: config2.semantic.archive_weight,
4388
+ docs_weight: config2.semantic.docs_weight
4336
4389
  }
4337
4390
  };
4338
4391
  const toml = stringify(data);
@@ -4525,6 +4578,10 @@ var init_config = __esm({
4525
4578
  hint_stats: {
4526
4579
  suppress_threshold_pct: 15,
4527
4580
  min_sample_size: 5
4581
+ },
4582
+ semantic: {
4583
+ archive_weight: 0.7,
4584
+ docs_weight: 0.92
4528
4585
  }
4529
4586
  };
4530
4587
  NUMERIC_FIELD_BOUNDS = {
@@ -4588,7 +4645,9 @@ var init_config = __esm({
4588
4645
  "indexing.large_file_skip_kb": { min: 1, max: 1048576 },
4589
4646
  "context.model_window_tokens": { min: 1e4, max: 1e7 },
4590
4647
  "hint_stats.suppress_threshold_pct": { min: 0, max: 100 },
4591
- "hint_stats.min_sample_size": { min: 1, max: 1e4 }
4648
+ "hint_stats.min_sample_size": { min: 1, max: 1e4 },
4649
+ "semantic.archive_weight": { min: 0.05, max: 1 },
4650
+ "semantic.docs_weight": { min: 0.05, max: 1 }
4592
4651
  };
4593
4652
  ENUM_FIELD_VALUES = {
4594
4653
  "compression.profile": ["auto", "aggressive", "balanced", "minimal"],
@@ -11510,16 +11569,16 @@ var init_file_type_handler = __esm({
11510
11569
 
11511
11570
  // src/skill_cache.ts
11512
11571
  import * as fs16 from "fs/promises";
11513
- import { resolve as resolve7 } from "path";
11572
+ import { resolve as resolve8 } from "path";
11514
11573
  import { homedir as homedir7 } from "os";
11515
11574
  import { readdirSync as readdirSync8, readFileSync as readFileSync12, existsSync as existsSync14, statSync as statSync7, unlinkSync as unlinkSync9 } from "node:fs";
11516
11575
  function skillOutputsDir() {
11517
11576
  if (_skillOutputsDirOverride) return _skillOutputsDirOverride;
11518
- return resolve7(dataDir(), SKILLS_OUTPUT_SUBDIR);
11577
+ return resolve8(dataDir(), SKILLS_OUTPUT_SUBDIR);
11519
11578
  }
11520
11579
  function skillsSourceDir() {
11521
11580
  if (_skillsSourceDirOverride) return _skillsSourceDirOverride;
11522
- return resolve7(homedir7(), ".claude", "skills");
11581
+ return resolve8(homedir7(), ".claude", "skills");
11523
11582
  }
11524
11583
  async function ensureSkillsDir() {
11525
11584
  try {
@@ -11662,7 +11721,7 @@ async function listOutputs() {
11662
11721
  continue;
11663
11722
  }
11664
11723
  try {
11665
- const content = await fs16.readFile(resolve7(dir, entry.name), "utf-8");
11724
+ const content = await fs16.readFile(resolve8(dir, entry.name), "utf-8");
11666
11725
  const meta3 = JSON.parse(content);
11667
11726
  metas.push(meta3);
11668
11727
  } catch {
@@ -11696,7 +11755,7 @@ async function findCrossSessionEntry(skillName, contentSha) {
11696
11755
  continue;
11697
11756
  }
11698
11757
  const dir = skillOutputsDir();
11699
- const bodyPath = resolve7(dir, `${meta3.outputId}.txt`);
11758
+ const bodyPath = resolve8(dir, `${meta3.outputId}.txt`);
11700
11759
  try {
11701
11760
  const bodyExists = await fs16.access(bodyPath).then(() => true).catch(() => false);
11702
11761
  if (bodyExists) {
@@ -11751,7 +11810,7 @@ async function storeOutput(sessionId, skillName, body, opts) {
11751
11810
  const truncBuf = buf.slice(truncStart);
11752
11811
  storedBody = truncBuf.toString("utf-8");
11753
11812
  }
11754
- await atomicWriteText(resolve7(dir, `${outId}.txt`), storedBody);
11813
+ await atomicWriteText(resolve8(dir, `${outId}.txt`), storedBody);
11755
11814
  const meta3 = {
11756
11815
  outputId: outId,
11757
11816
  skillName: name2,
@@ -11761,7 +11820,7 @@ async function storeOutput(sessionId, skillName, body, opts) {
11761
11820
  truncated,
11762
11821
  sourcePath: opts?.sourcePath || ""
11763
11822
  };
11764
- await atomicWriteText(resolve7(dir, `${outId}.meta`), JSON.stringify(meta3, null, 2));
11823
+ await atomicWriteText(resolve8(dir, `${outId}.meta`), JSON.stringify(meta3, null, 2));
11765
11824
  pruneSkillOutputs();
11766
11825
  return meta3;
11767
11826
  } catch {
@@ -11781,7 +11840,7 @@ async function storeCompact(sessionId, skillName, compactText, sourceSha) {
11781
11840
  text = `<!-- source_sha: ${sourceSha.slice(0, 12)} -->
11782
11841
  ${text}`;
11783
11842
  }
11784
- await atomicWriteText(resolve7(dir, fileId), text);
11843
+ await atomicWriteText(resolve8(dir, fileId), text);
11785
11844
  } catch {
11786
11845
  }
11787
11846
  }
@@ -11801,7 +11860,7 @@ function getCompactAnySessionSync(skillName) {
11801
11860
  for (const entry of entries) {
11802
11861
  if (!matchesCompactSuffix(entry.name, entry.isFile(), suffix)) continue;
11803
11862
  try {
11804
- const text = readFileSync12(resolve7(dir, entry.name), "utf-8");
11863
+ const text = readFileSync12(resolve8(dir, entry.name), "utf-8");
11805
11864
  if (text.trim()) return text;
11806
11865
  } catch {
11807
11866
  continue;
@@ -11829,7 +11888,7 @@ async function readSkillHits(skillName) {
11829
11888
  const name2 = safeSkillName(skillName);
11830
11889
  if (!name2) return { count: 0, lastTs: 0 };
11831
11890
  const dir = skillOutputsDir();
11832
- const hitsFile = resolve7(dir, `${sanitizeSkillId(name2)}.hits`);
11891
+ const hitsFile = resolve8(dir, `${sanitizeSkillId(name2)}.hits`);
11833
11892
  const content = await fs16.readFile(hitsFile, "utf-8").catch(() => null);
11834
11893
  if (content) {
11835
11894
  const parsed = JSON.parse(content);
@@ -11879,7 +11938,7 @@ async function incrementSkillHit(skillName) {
11879
11938
  if (!name2) return;
11880
11939
  await ensureSkillsDir();
11881
11940
  const dir = skillOutputsDir();
11882
- const hitsFile = resolve7(dir, `${sanitizeSkillId(name2)}.hits`);
11941
+ const hitsFile = resolve8(dir, `${sanitizeSkillId(name2)}.hits`);
11883
11942
  const lockPath = `${hitsFile}.lock`;
11884
11943
  await runExclusiveInProcess(lockPath, async () => {
11885
11944
  const locked = await acquireSkillHitLock(lockPath);
@@ -11926,14 +11985,14 @@ async function listSkills(sessionId) {
11926
11985
  let compactLen = 0;
11927
11986
  let compactText = "";
11928
11987
  try {
11929
- const stat2 = await fs16.stat(resolve7(dir, compactFileId));
11988
+ const stat2 = await fs16.stat(resolve8(dir, compactFileId));
11930
11989
  compactLen = stat2.size;
11931
- compactText = await fs16.readFile(resolve7(dir, compactFileId), "utf-8").catch(() => "");
11990
+ compactText = await fs16.readFile(resolve8(dir, compactFileId), "utf-8").catch(() => "");
11932
11991
  } catch {
11933
11992
  compactLen = 0;
11934
11993
  }
11935
11994
  const hasMarker = extractCompactFromMarker(
11936
- await fs16.readFile(resolve7(dir, `${meta3.outputId}.txt`), "utf-8").catch(() => "")
11995
+ await fs16.readFile(resolve8(dir, `${meta3.outputId}.txt`), "utf-8").catch(() => "")
11937
11996
  ) !== null;
11938
11997
  const compactStale = isCompactStale(compactText, meta3.skillName, meta3.contentSha);
11939
11998
  const { count: hitCount } = await readSkillHits(meta3.skillName);
@@ -11969,7 +12028,7 @@ async function getSkillFilePath(skillName) {
11969
12028
  }
11970
12029
  }
11971
12030
  async function resolvePluginSkillPath(pluginName, skillSlug) {
11972
- const manifestPath = _pluginsManifestPathOverride ?? resolve7(homedir7(), ".claude", "plugins", "installed_plugins.json");
12031
+ const manifestPath = _pluginsManifestPathOverride ?? resolve8(homedir7(), ".claude", "plugins", "installed_plugins.json");
11973
12032
  let raw;
11974
12033
  try {
11975
12034
  raw = await fs16.readFile(manifestPath, "utf8");
@@ -11992,7 +12051,7 @@ async function resolvePluginSkillPath(pluginName, skillSlug) {
11992
12051
  if (typeof entry !== "object" || entry === null) continue;
11993
12052
  const installPath = entry["installPath"];
11994
12053
  if (typeof installPath !== "string" || installPath === "") continue;
11995
- const diskPath = resolve7(installPath, "skills", skillSlug, "SKILL.md");
12054
+ const diskPath = resolve8(installPath, "skills", skillSlug, "SKILL.md");
11996
12055
  try {
11997
12056
  await fs16.access(diskPath);
11998
12057
  return diskPath;
@@ -12011,7 +12070,7 @@ async function installedSkillPath(skillName) {
12011
12070
  const pluginPath = await resolvePluginSkillPath(name2.slice(0, colonIdx), name2.slice(colonIdx + 1));
12012
12071
  if (pluginPath !== null) return pluginPath;
12013
12072
  }
12014
- const diskPath = resolve7(skillsSourceDir(), name2, "SKILL.md");
12073
+ const diskPath = resolve8(skillsSourceDir(), name2, "SKILL.md");
12015
12074
  try {
12016
12075
  await fs16.access(diskPath);
12017
12076
  return diskPath;
@@ -12031,11 +12090,11 @@ function pruneSkillOutputs(maxCount = DEFAULT_MAX_COUNT, maxAgeMs = DEFAULT_MAX_
12031
12090
  const outputId = file2.slice(0, -".meta".length);
12032
12091
  let ts;
12033
12092
  try {
12034
- const parsed = JSON.parse(readFileSync12(resolve7(dir, file2), "utf-8"));
12093
+ const parsed = JSON.parse(readFileSync12(resolve8(dir, file2), "utf-8"));
12035
12094
  ts = parsed.ts;
12036
12095
  } catch {
12037
12096
  try {
12038
- ts = statSync7(resolve7(dir, file2)).mtimeMs;
12097
+ ts = statSync7(resolve8(dir, file2)).mtimeMs;
12039
12098
  } catch {
12040
12099
  continue;
12041
12100
  }
@@ -12045,7 +12104,7 @@ function pruneSkillOutputs(maxCount = DEFAULT_MAX_COUNT, maxAgeMs = DEFAULT_MAX_
12045
12104
  const removeEntry = (outputId) => {
12046
12105
  for (const ext2 of [".meta", ".txt"]) {
12047
12106
  try {
12048
- unlinkSync9(resolve7(dir, `${outputId}${ext2}`));
12107
+ unlinkSync9(resolve8(dir, `${outputId}${ext2}`));
12049
12108
  } catch {
12050
12109
  }
12051
12110
  }
@@ -12159,7 +12218,7 @@ async function ocrImage(input) {
12159
12218
  if (_ocrUnavailableThisProcess) return null;
12160
12219
  const entryPath = resolveTesseractEntry();
12161
12220
  if (entryPath === null) return null;
12162
- return new Promise((resolve24) => {
12221
+ return new Promise((resolve25) => {
12163
12222
  let settled = false;
12164
12223
  let child;
12165
12224
  try {
@@ -12168,7 +12227,7 @@ async function ocrImage(input) {
12168
12227
  });
12169
12228
  } catch {
12170
12229
  _ocrUnavailableThisProcess = true;
12171
- resolve24(null);
12230
+ resolve25(null);
12172
12231
  return;
12173
12232
  }
12174
12233
  const chunks = [];
@@ -12181,7 +12240,7 @@ async function ocrImage(input) {
12181
12240
  child.kill();
12182
12241
  } catch {
12183
12242
  }
12184
- resolve24(result);
12243
+ resolve25(result);
12185
12244
  };
12186
12245
  const timer = setTimeout(() => finish(null, true), _ocrTimeoutMs);
12187
12246
  child.stdout?.on("data", (c) => chunks.push(c));
@@ -13619,7 +13678,7 @@ function ensureTransformerLoaded() {
13619
13678
  }
13620
13679
  }
13621
13680
  function sleep(ms) {
13622
- return new Promise((resolve24) => setTimeout(resolve24, ms));
13681
+ return new Promise((resolve25) => setTimeout(resolve25, ms));
13623
13682
  }
13624
13683
  async function buildExtractorWithRetry(pipelineFn, modelName) {
13625
13684
  let lastError;
@@ -13959,7 +14018,8 @@ function rerankHits(hits, query, topK) {
13959
14018
  boost = Math.min(matches2 * _VERBATIM_TOKEN_BOOST, _MAX_VERBATIM_BOOST);
13960
14019
  }
13961
14020
  const penalty = _isGeneratedPath(hit.filePath) ? _GENERATED_PATH_PENALTY : 0;
13962
- return { hit, index, adjusted: hit.distance - boost + penalty };
14021
+ const pathPenalty = _pathPriorityPenalty(hit.filePath);
14022
+ return { hit, index, adjusted: hit.distance - boost + penalty + pathPenalty };
13963
14023
  });
13964
14024
  scored.sort((a, b) => a.adjusted - b.adjusted || a.index - b.index);
13965
14025
  return scored.slice(0, topK).map((entry) => ({ ...entry.hit, adjustedDistance: entry.adjusted }));
@@ -14085,11 +14145,26 @@ function _isGeneratedPath(filePath) {
14085
14145
  }
14086
14146
  return false;
14087
14147
  }
14088
- var _require3, _transformer, _transformerError, _transformerLoadAttempted, DEFAULT_MODEL, DEFAULT_DIM, QUERY_INSTRUCTION_PREFIX, _extractorCache, _pipelineFnOverride, PIPELINE_RETRY_ATTEMPTS, PIPELINE_RETRY_DELAY_MS, DEFAULT_PIPELINE_RETRY_DELAY_MS, MIN_CHUNK_CHARS, MAX_CHUNK_CHARS, DEFAULT_DISTANCE_THRESHOLD, _GENERATED_PATH_SEGMENTS, _GENERATED_PATH_PENALTY, _VERBATIM_TOKEN_BOOST, _MAX_VERBATIM_BOOST, _TOKEN_RE, _MIN_TOKEN_LEN, OVER_FETCH_FACTOR, MAX_OVER_FETCH, BACKFILL_MULTIPLIER, _chunkVectorsUsable;
14148
+ function _pathPriorityPenalty(filePath) {
14149
+ const segments = filePath.split(/[/\\]+/);
14150
+ const basename22 = segments[segments.length - 1] ?? filePath;
14151
+ const weights = loadConfig().semantic;
14152
+ const isArchive = _ARCHIVE_FILE_RE.test(basename22) || segments.some((seg) => _ARCHIVE_PATH_SEGMENTS.has(seg.toLowerCase()));
14153
+ if (isArchive) {
14154
+ return 1 - weights.archive_weight;
14155
+ }
14156
+ const isDocs = _DOCS_FILE_RE.test(basename22) || segments.some((seg) => seg.toLowerCase() === _DOCS_DIR_SEGMENT);
14157
+ if (isDocs) {
14158
+ return 1 - weights.docs_weight;
14159
+ }
14160
+ return 0;
14161
+ }
14162
+ var _require3, _transformer, _transformerError, _transformerLoadAttempted, DEFAULT_MODEL, DEFAULT_DIM, QUERY_INSTRUCTION_PREFIX, _extractorCache, _pipelineFnOverride, PIPELINE_RETRY_ATTEMPTS, PIPELINE_RETRY_DELAY_MS, DEFAULT_PIPELINE_RETRY_DELAY_MS, MIN_CHUNK_CHARS, MAX_CHUNK_CHARS, DEFAULT_DISTANCE_THRESHOLD, _GENERATED_PATH_SEGMENTS, _GENERATED_PATH_PENALTY, _ARCHIVE_PATH_SEGMENTS, _ARCHIVE_FILE_RE, _DOCS_FILE_RE, _DOCS_DIR_SEGMENT, _VERBATIM_TOKEN_BOOST, _MAX_VERBATIM_BOOST, _TOKEN_RE, _MIN_TOKEN_LEN, OVER_FETCH_FACTOR, MAX_OVER_FETCH, BACKFILL_MULTIPLIER, _chunkVectorsUsable;
14089
14163
  var init_embeddings = __esm({
14090
14164
  "src/embeddings.ts"() {
14091
14165
  "use strict";
14092
14166
  init_define_import_meta_env();
14167
+ init_config();
14093
14168
  init_sql_path();
14094
14169
  init_util2();
14095
14170
  init_reset();
@@ -14138,6 +14213,17 @@ var init_embeddings = __esm({
14138
14213
  ".ruff_cache"
14139
14214
  ]);
14140
14215
  _GENERATED_PATH_PENALTY = 0.5;
14216
+ _ARCHIVE_PATH_SEGMENTS = /* @__PURE__ */ new Set([
14217
+ "archive",
14218
+ "archived",
14219
+ "old",
14220
+ "deprecated",
14221
+ "plans",
14222
+ "drafts"
14223
+ ]);
14224
+ _ARCHIVE_FILE_RE = /(^changelog|\.bak$|\.orig$)/i;
14225
+ _DOCS_FILE_RE = /\.md$/i;
14226
+ _DOCS_DIR_SEGMENT = "docs";
14141
14227
  _VERBATIM_TOKEN_BOOST = 0.05;
14142
14228
  _MAX_VERBATIM_BOOST = 0.25;
14143
14229
  _TOKEN_RE = /\w+/g;
@@ -23932,7 +24018,7 @@ import * as readline from "node:readline";
23932
24018
  async function defaultConfirm(question) {
23933
24019
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
23934
24020
  try {
23935
- const answer = await new Promise((resolve24) => rl.question(question, resolve24));
24021
+ const answer = await new Promise((resolve25) => rl.question(question, resolve25));
23936
24022
  return /^y(es)?$/i.test(answer.trim());
23937
24023
  } finally {
23938
24024
  rl.close();
@@ -24517,14 +24603,32 @@ function fetchTopSymbols(limit, dbPath, rootDir) {
24517
24603
  try {
24518
24604
  const db = getDb(dbPath);
24519
24605
  const { clause, param } = projectScopeClause("file_path");
24606
+ const refScope = projectScopeClause("file_path");
24520
24607
  const rows = db.prepare(
24608
+ // refs carry only a bare name, so a name defined N times cannot claim all N copies' references: divide by the number of same-named definitions, and keep one representative per name so a generic helper like `apply` occupies one slot instead of seven.
24521
24609
  `SELECT file_path, name, kind, line_start, line_end, body, docstring, parent
24522
- FROM symbols
24523
- WHERE kind IN ('class', 'function', 'interface') AND ${clause}
24524
- ORDER BY CASE kind WHEN 'class' THEN 0 WHEN 'interface' THEN 1 ELSE 2 END,
24610
+ FROM (
24611
+ SELECT s.file_path, s.name, s.kind, s.line_start, s.line_end, s.body, s.docstring, s.parent,
24612
+ COALESCE(r.ref_count, 0) * 1.0 / COUNT(*) OVER (PARTITION BY s.name) AS score,
24613
+ ROW_NUMBER() OVER (
24614
+ PARTITION BY s.name
24615
+ ORDER BY LENGTH(COALESCE(s.body, '')) DESC, s.file_path
24616
+ ) AS rn
24617
+ FROM symbols s
24618
+ LEFT JOIN (
24619
+ SELECT name, COUNT(*) AS ref_count
24620
+ FROM refs
24621
+ WHERE ${refScope.clause}
24622
+ GROUP BY name
24623
+ ) r ON r.name = s.name
24624
+ WHERE s.kind IN ('class', 'function', 'interface') AND ${clause}
24625
+ )
24626
+ WHERE rn = 1
24627
+ ORDER BY score DESC,
24628
+ CASE kind WHEN 'class' THEN 0 WHEN 'interface' THEN 1 ELSE 2 END,
24525
24629
  LENGTH(COALESCE(body, '')) DESC
24526
24630
  LIMIT ?`
24527
- ).all(param(rootDir), limit);
24631
+ ).all(refScope.param(rootDir), param(rootDir), limit);
24528
24632
  return rows.map((r) => ({
24529
24633
  filePath: r.file_path,
24530
24634
  name: r.name,
@@ -24576,13 +24680,12 @@ function formatProjectMap(map3, compact = false) {
24576
24680
  lines2.push("");
24577
24681
  lines2.push("## Top symbols");
24578
24682
  for (const s of map3.topSymbols) {
24579
- if (compact) {
24580
- lines2.push(`- ${s.name} (${s.kind})`);
24581
- } else {
24582
- const loc = `${path34.basename(s.filePath)}:${s.lineStart}-${s.lineEnd}`;
24583
- lines2.push(`- ${s.name} (${s.kind}) \u2014 ${loc}`);
24584
- }
24683
+ const loc = `${toDisplayPath(map3.rootDir, s.filePath)}:${s.lineStart}-${s.lineEnd}`;
24684
+ lines2.push(`- ${s.name} (${s.kind}) \u2014 ${loc}`);
24585
24685
  }
24686
+ } else {
24687
+ lines2.push("");
24688
+ lines2.push("## Top symbols: none \u2014 no files indexed for this project; run 'token-goat index .'");
24586
24689
  }
24587
24690
  if (!compact && map3.recentFiles.length > 0) {
24588
24691
  lines2.push("");
@@ -25090,6 +25193,11 @@ function isDeadSymbol(name2, refCount) {
25090
25193
  if (ENTRY_NAMES.has(name2)) return false;
25091
25194
  return refCount === 0;
25092
25195
  }
25196
+ function parseGraphSymbolSpec(spec) {
25197
+ const colonIdx = findSpecSeparator(spec);
25198
+ if (colonIdx === -1) return { name: spec };
25199
+ return { name: spec.slice(colonIdx + 2), file: spec.slice(0, colonIdx) };
25200
+ }
25093
25201
  function buildFileSymCache() {
25094
25202
  const cache = /* @__PURE__ */ new Map();
25095
25203
  return (fp) => {
@@ -25107,9 +25215,10 @@ function fileDefinesName(fp, name2, getSyms) {
25107
25215
  function filterRefsForSymbol(refs, name2, filePath, getSyms) {
25108
25216
  return refs.filter((ref2) => ref2.filePath === filePath || !fileDefinesName(ref2.filePath, name2, getSyms));
25109
25217
  }
25110
- function resolveCallers(name2, limit, filePath, rootDir) {
25218
+ function resolveCallers(name2, limit, filePath, rootDir, excludeTests) {
25111
25219
  const resolvedRootDir = rootDir ?? resolveProjectRoot({ project: process.cwd() });
25112
- const refs = queryRefs({ name: name2, limit: limit ?? 500, rootDir: resolvedRootDir });
25220
+ const queryLimit = excludeTests === true ? UNBOUNDED_REF_LIMIT : limit ?? 500;
25221
+ const refs = queryRefs({ name: name2, limit: queryLimit, rootDir: resolvedRootDir });
25113
25222
  const getSyms = buildFileSymCache();
25114
25223
  const scoped = filePath === void 0 ? refs : filterRefsForSymbol(refs, name2, filePath, getSyms);
25115
25224
  return scoped.map((ref2) => {
@@ -25128,17 +25237,37 @@ function runCallers(opts) {
25128
25237
  return 1;
25129
25238
  }
25130
25239
  const rootDir = resolveProjectRoot({ project: process.cwd() });
25131
- const entries = resolveCallers(opts.symbol, opts.limit, void 0, rootDir);
25240
+ const { name: name2, file: file2 } = parseGraphSymbolSpec(opts.symbol);
25241
+ const fileHint = file2 !== void 0 ? resolveIndexPath(file2, rootDir) : void 0;
25242
+ if (fileHint !== void 0 && querySymbols({ name: name2, filePath: fileHint, limit: 1 }).length === 0) {
25243
+ emitErr(`Symbol '${name2}' not found in '${file2}'`);
25244
+ return 1;
25245
+ }
25246
+ const resolved = resolveCallers(name2, opts.limit, fileHint, rootDir, opts.excludeTests);
25247
+ const suppressed = opts.excludeTests === true ? resolved.filter((e) => isTestFile(e.file)).length : 0;
25248
+ const entries = opts.excludeTests === true ? resolved.filter((e) => !isTestFile(e.file)).slice(0, opts.limit ?? 500) : resolved;
25132
25249
  if (entries.length === 0) {
25250
+ if (opts.excludeTests === true && suppressed > 0) {
25251
+ emitErr(`No non-test references found for '${opts.symbol}' (${suppressed} in test files hidden by --exclude-tests)`);
25252
+ return 1;
25253
+ }
25133
25254
  emitErr(`No references found for '${opts.symbol}'`);
25134
25255
  return 1;
25135
25256
  }
25257
+ const contextLines = opts.context ?? 0;
25136
25258
  if (opts.json === true) {
25137
- emit2(JSON.stringify(entries, null, 2));
25259
+ const payload = contextLines > 0 ? entries.map((e) => ({ ...e, contextLines: buildContextWindow(e.file, e.line, contextLines) ?? [] })) : entries;
25260
+ emit2(JSON.stringify(payload, null, 2));
25138
25261
  return 0;
25139
25262
  }
25263
+ if (opts.excludeTests === true && suppressed > 0) {
25264
+ emit2(`${entries.length} callers found (${suppressed} in test files hidden by --exclude-tests)`);
25265
+ }
25140
25266
  for (const e of entries) {
25141
- emit2(`${e.caller} ${toDisplayPath(rootDir, e.file)}:${e.line}`);
25267
+ const displayPath = toDisplayPath(rootDir, e.file);
25268
+ emit2(`${e.caller} ${displayPath}:${e.line}`);
25269
+ const window = buildContextWindow(e.file, e.line, contextLines);
25270
+ if (window !== null) for (const l of renderContextWindow(displayPath, e.line, window, "", " ")) emit2(l);
25142
25271
  }
25143
25272
  return 0;
25144
25273
  }
@@ -25149,28 +25278,36 @@ function runCallChain(opts) {
25149
25278
  }
25150
25279
  const maxDepth = opts.depth ?? 8;
25151
25280
  const rootDir = resolveProjectRoot({ project: process.cwd() });
25152
- if (querySymbols({ name: opts.symbol, rootDir, limit: 1 }).length === 0) {
25281
+ const { name: name2, file: file2 } = parseGraphSymbolSpec(opts.symbol);
25282
+ const fileHint = file2 !== void 0 ? resolveIndexPath(file2, rootDir) : void 0;
25283
+ if (fileHint !== void 0) {
25284
+ if (querySymbols({ name: name2, filePath: fileHint, limit: 1 }).length === 0) {
25285
+ emitErr(`Symbol '${name2}' not found in '${file2}'`);
25286
+ return 1;
25287
+ }
25288
+ } else if (querySymbols({ name: name2, rootDir, limit: 1 }).length === 0) {
25153
25289
  emitErr(`Symbol not found: ${opts.symbol}`);
25154
25290
  return 1;
25155
25291
  }
25156
25292
  const getSyms = buildFileSymCache();
25157
- const callersOf = (name2) => {
25158
- const refs = queryRefs({ name: name2, limit: DEFAULT_REF_QUERY_LIMIT, rootDir });
25293
+ const callersOf = (n) => {
25294
+ const refs = queryRefs({ name: n, limit: DEFAULT_REF_QUERY_LIMIT, rootDir });
25159
25295
  if (refs.length === 0) return [];
25296
+ const scoped = fileHint !== void 0 && n === name2 ? filterRefsForSymbol(refs, n, fileHint, getSyms) : refs;
25160
25297
  const names = /* @__PURE__ */ new Set();
25161
- for (const ref2 of refs) {
25298
+ for (const ref2 of scoped) {
25162
25299
  const enc = enclosingSymbol(getSyms(ref2.filePath), ref2.line);
25163
25300
  if (enc !== null) names.add(enc.name);
25164
25301
  }
25165
25302
  return [...names];
25166
25303
  };
25167
- const chains = bfsCallChains(opts.symbol, callersOf, maxDepth);
25304
+ const chains = bfsCallChains(name2, callersOf, maxDepth);
25168
25305
  if (opts.json === true) {
25169
25306
  emit2(JSON.stringify({ chains }, null, 2));
25170
25307
  return 0;
25171
25308
  }
25172
- if (chains.length === 1 && chains[0]?.length === 1 && chains[0][0] === opts.symbol) {
25173
- emit2(`${opts.symbol} (no callers)`);
25309
+ if (chains.length === 1 && chains[0]?.length === 1 && chains[0][0] === name2) {
25310
+ emit2(`${name2} (no callers)`);
25174
25311
  return 0;
25175
25312
  }
25176
25313
  for (const chain2 of chains) {
@@ -25190,16 +25327,23 @@ function runImpact(opts) {
25190
25327
  const top = opts.top ?? 20;
25191
25328
  const DEPTH_CAP = 8;
25192
25329
  const rootDir = resolveProjectRoot({ project: process.cwd() });
25330
+ const { name: rootName, file: file2 } = parseGraphSymbolSpec(opts.symbol);
25331
+ const fileHint = file2 !== void 0 ? resolveIndexPath(file2, rootDir) : void 0;
25332
+ if (fileHint !== void 0 && querySymbols({ name: rootName, filePath: fileHint, limit: 1 }).length === 0) {
25333
+ emitErr(`Symbol '${rootName}' not found in '${file2}'`);
25334
+ return 1;
25335
+ }
25193
25336
  const getSyms = buildFileSymCache();
25194
- const hops = /* @__PURE__ */ new Map([[opts.symbol, 0]]);
25195
- const queue = [[opts.symbol, 0]];
25337
+ const hops = /* @__PURE__ */ new Map([[rootName, 0]]);
25338
+ const queue = [[rootName, 0]];
25196
25339
  while (queue.length > 0) {
25197
25340
  const item = queue.shift();
25198
25341
  if (item === void 0) break;
25199
25342
  const [name2, depth] = item;
25200
25343
  if (depth >= DEPTH_CAP) continue;
25201
25344
  const refs = queryRefs({ name: name2, limit: DEFAULT_REF_QUERY_LIMIT, rootDir });
25202
- for (const ref2 of refs) {
25345
+ const scoped = fileHint !== void 0 && name2 === rootName ? filterRefsForSymbol(refs, name2, fileHint, getSyms) : refs;
25346
+ for (const ref2 of scoped) {
25203
25347
  const newHop = depth + 1;
25204
25348
  const enc = enclosingSymbol(getSyms(ref2.filePath), ref2.line);
25205
25349
  if (enc === null) {
@@ -25216,7 +25360,7 @@ function runImpact(opts) {
25216
25360
  }
25217
25361
  }
25218
25362
  }
25219
- hops.delete(opts.symbol);
25363
+ hops.delete(rootName);
25220
25364
  const sorted = [...hops.entries()].sort(compareHopEntries).slice(0, top);
25221
25365
  if (sorted.length === 0) {
25222
25366
  emitErr(`No callers found for '${opts.symbol}'`);
@@ -25268,6 +25412,7 @@ function runDead(opts) {
25268
25412
  const syms = querySymbols({ kind, limit: 5e3, rootDir });
25269
25413
  const getSyms = buildFileSymCache();
25270
25414
  const results = [];
25415
+ let suppressed = 0;
25271
25416
  for (const sym of syms) {
25272
25417
  if (opts.includePrivate !== true && sym.name.startsWith("_")) continue;
25273
25418
  const refs = queryRefs({ name: sym.name, limit: DEFAULT_REF_QUERY_LIMIT, rootDir });
@@ -25277,6 +25422,10 @@ function runDead(opts) {
25277
25422
  const ownScope = enclosingNamedScope(getSyms(sym.filePath), sym.lineStart);
25278
25423
  if (ownScope !== null && hasAncestorDispatchRef(sym.name, ownScope.name, sym.filePath, rootDir)) continue;
25279
25424
  }
25425
+ if (opts.excludeTests === true && isTestFile(sym.filePath)) {
25426
+ suppressed += 1;
25427
+ continue;
25428
+ }
25280
25429
  results.push({ name: sym.name, kind: sym.kind, file: sym.filePath, line: sym.lineStart });
25281
25430
  }
25282
25431
  const sliced = results.slice(0, opts.top ?? results.length);
@@ -25285,9 +25434,16 @@ function runDead(opts) {
25285
25434
  return 0;
25286
25435
  }
25287
25436
  if (sliced.length === 0) {
25288
- emit2("No dead symbols found.");
25437
+ if (opts.excludeTests === true && suppressed > 0) {
25438
+ emit2(`No dead symbols found (${suppressed} in test files hidden by --exclude-tests).`);
25439
+ } else {
25440
+ emit2("No dead symbols found.");
25441
+ }
25289
25442
  return 0;
25290
25443
  }
25444
+ if (opts.excludeTests === true && suppressed > 0) {
25445
+ emit2(`${sliced.length} dead symbols (${suppressed} in test files hidden by --exclude-tests)`);
25446
+ }
25291
25447
  for (const r of sliced) {
25292
25448
  emit2(`${r.name} ${toDisplayPath(rootDir, r.file)}:${r.line}`);
25293
25449
  }
@@ -25537,16 +25693,9 @@ function runSimilar(opts) {
25537
25693
  emitErr(`Invalid spec - expected "file::symbol", got: ${opts.spec}`);
25538
25694
  return 1;
25539
25695
  }
25540
- const fileArg = opts.spec.slice(0, sepIdx);
25541
- const symbolArg = opts.spec.slice(sepIdx + 2);
25542
25696
  const top = opts.top ?? 10;
25543
- const filePath = resolveIndexPath(fileArg);
25544
- const anchors = querySymbols({ name: symbolArg, filePath });
25545
- if (anchors.length === 0) {
25546
- emitErr(`Symbol '${symbolArg}' not found in '${fileArg}'`);
25547
- return 1;
25548
- }
25549
- const anchor = anchors[0];
25697
+ const anchor = resolveSymbolSpecOrEmitError("similar", opts.spec, void 0);
25698
+ if (anchor === null) return 1;
25550
25699
  const words = [anchor.name, ...(anchor.docstring ?? "").split(/\s+/).filter((w) => w.length > 4)];
25551
25700
  const query = words.slice(0, 8).join(" ");
25552
25701
  const rootDir = resolveProjectRoot({ project: process.cwd() });
@@ -25578,13 +25727,13 @@ function runContextFor(opts) {
25578
25727
  const bodyTokens = estimateTokens(h.body ?? "");
25579
25728
  if (budget !== void 0 && tokensSoFar + bodyTokens > budget) continue;
25580
25729
  tokensSoFar += bodyTokens;
25581
- entries.push({ file: h.filePath, symbol: h.name, kind: h.kind, readCmd: `token-goat read "${h.filePath}::${h.name}"` });
25730
+ entries.push({ file: h.filePath, symbol: h.name, kind: h.kind, line: h.lineStart, readCmd: `token-goat read "${h.filePath}::${h.name}@${h.lineStart}"` });
25582
25731
  }
25583
25732
  if (opts.json === true) {
25584
25733
  emit2(JSON.stringify(entries, null, 2));
25585
25734
  return 0;
25586
25735
  }
25587
- for (const e of entries) emit2(`token-goat read "${toDisplayPath(rootDir, e.file)}::${e.symbol}"`);
25736
+ for (const e of entries) emit2(`token-goat read "${toDisplayPath(rootDir, e.file)}::${e.symbol}@${e.line}"`);
25588
25737
  return 0;
25589
25738
  }
25590
25739
  function runTestFor(opts) {
@@ -25729,16 +25878,10 @@ function runBlame(opts) {
25729
25878
  emitErr(`Invalid spec - expected "file::symbol", got: ${opts.spec}`);
25730
25879
  return 1;
25731
25880
  }
25732
- const fileArg = opts.spec.slice(0, sepIdx);
25733
- const symbolArg = opts.spec.slice(sepIdx + 2);
25734
25881
  const cwd = opts.cwd ?? process.cwd();
25735
- const filePath = resolveIndexPath(fileArg);
25736
- const syms = querySymbols({ name: symbolArg, filePath });
25737
- if (syms.length === 0) {
25738
- emitErr(`Symbol '${symbolArg}' not found in '${fileArg}'`);
25739
- return 1;
25740
- }
25741
- const sym = syms[0];
25882
+ const sym = resolveSymbolSpecOrEmitError("blame", opts.spec, void 0);
25883
+ if (sym === null) return 1;
25884
+ const filePath = sym.filePath;
25742
25885
  const start = sym.lineStart;
25743
25886
  const end = sym.lineEnd;
25744
25887
  let raw;
@@ -25759,10 +25902,10 @@ function runBlame(opts) {
25759
25902
  if (!m) return { raw: l };
25760
25903
  return { commit: m[1], author: (m[2] ?? "").trim(), date: (m[3] ?? "").trim(), line: Number.parseInt(m[4] ?? "0", 10), content: m[5] };
25761
25904
  });
25762
- emit2(JSON.stringify({ symbol: symbolArg, file: filePath, lines: lines2 }, null, 2));
25905
+ emit2(JSON.stringify({ symbol: sym.name, file: filePath, lines: lines2 }, null, 2));
25763
25906
  return 0;
25764
25907
  }
25765
- emit2(`${symbolArg} ${toDisplayPath(getDisplayRoot(opts.cwd), filePath)}:${start}-${end}`);
25908
+ emit2(`${sym.name} ${toDisplayPath(getDisplayRoot(opts.cwd), filePath)}:${start}-${end}`);
25766
25909
  emit2(raw.trim());
25767
25910
  return 0;
25768
25911
  }
@@ -25776,14 +25919,14 @@ function runAsk(opts) {
25776
25919
  const hits = searchSymbolsFts(opts.question, top, void 0, rootDir);
25777
25920
  const BACKEND_ENV = "TOKEN_GOAT_ASK_BACKEND";
25778
25921
  const backendLabel = process.env[BACKEND_ENV] ?? "";
25779
- const entries = hits.map((h) => ({ file: h.filePath, symbol: h.name, kind: h.kind, readCmd: `token-goat read "${h.filePath}::${h.name}"` }));
25922
+ const entries = hits.map((h) => ({ file: h.filePath, symbol: h.name, kind: h.kind, line: h.lineStart, readCmd: `token-goat read "${h.filePath}::${h.name}@${h.lineStart}"` }));
25780
25923
  const degrade = () => {
25781
25924
  if (opts.json === true) {
25782
25925
  emit2(JSON.stringify({ degraded: true, note: `Set ${BACKEND_ENV}=claude|codex for LLM synthesis`, context: entries }, null, 2));
25783
25926
  return 0;
25784
25927
  }
25785
25928
  emit2(`[degraded mode - set ${BACKEND_ENV}=claude|codex for LLM synthesis]`);
25786
- for (const e of entries) emit2(`token-goat read "${toDisplayPath(rootDir, e.file)}::${e.symbol}"`);
25929
+ for (const e of entries) emit2(`token-goat read "${toDisplayPath(rootDir, e.file)}::${e.symbol}@${e.line}"`);
25787
25930
  return 0;
25788
25931
  };
25789
25932
  if (!backendLabel) return degrade();
@@ -29951,6 +30094,42 @@ function formatBareNameSpecError(command, name2, projectRoot) {
29951
30094
  }
29952
30095
  return lines2.join("\n");
29953
30096
  }
30097
+ function formatCrossFileLead(command, name2, excludeFilePath, projectRoot) {
30098
+ const rootDir = projectRoot ?? process.cwd();
30099
+ const matches2 = querySymbols({ name: name2, limit: 50, rootDir });
30100
+ const excludeResolved = resolveIndexPath(excludeFilePath, rootDir);
30101
+ const seen = /* @__PURE__ */ new Set();
30102
+ const specs = [];
30103
+ for (const m of matches2) {
30104
+ if (foldPath(m.filePath) === foldPath(excludeResolved)) continue;
30105
+ const spec = `${toDisplayPath(rootDir, m.filePath)}::${m.name}`;
30106
+ if (seen.has(spec)) continue;
30107
+ seen.add(spec);
30108
+ specs.push(spec);
30109
+ }
30110
+ if (specs.length === 0) return "";
30111
+ const firstSpec = specs[0];
30112
+ const lines2 = [`'${name2}' is defined in ${firstSpec !== void 0 ? firstSpec.split("::")[0] : ""}`];
30113
+ for (const spec of specs.slice(0, DIDYOUMEAN_LIMIT)) {
30114
+ lines2.push(` - token-goat ${command} "${spec}"`);
30115
+ }
30116
+ if (specs.length > DIDYOUMEAN_LIMIT) {
30117
+ lines2.push(` (${specs.length - DIDYOUMEAN_LIMIT} more not shown)`);
30118
+ }
30119
+ return lines2.join("\n");
30120
+ }
30121
+ function resolveEnclosingSymbol(filePath, chunkStartLine) {
30122
+ const symbols = querySymbols({ filePath, limit: 1e5 }, globalDbPath());
30123
+ let best = null;
30124
+ for (const s of symbols) {
30125
+ if (s.lineStart <= chunkStartLine && chunkStartLine <= s.lineEnd) {
30126
+ if (best === null || s.lineEnd - s.lineStart < best.lineEnd - best.lineStart) {
30127
+ best = s;
30128
+ }
30129
+ }
30130
+ }
30131
+ return best === null ? null : { name: best.name, kind: best.kind };
30132
+ }
29954
30133
  function trimBlankLines(lines2) {
29955
30134
  let start = 0;
29956
30135
  let end = lines2.length;
@@ -30046,10 +30225,21 @@ function parseCrossFileMultiSpec(spec) {
30046
30225
  }
30047
30226
  return pairs.length > 1 ? pairs : null;
30048
30227
  }
30228
+ function parseMultiFileSpec(spec) {
30229
+ if (!spec.includes(",")) return null;
30230
+ if (fileExists(spec)) return null;
30231
+ if (findSpecSeparator(spec) !== -1) return null;
30232
+ const parts = spec.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
30233
+ return parts.length > 1 ? parts : null;
30234
+ }
30235
+ function extraFileArgsNote(command, first2, extras) {
30236
+ return `Note: ${extras.length} extra file argument(s) ignored (${extras.join(", ")}). ${command} reads one file, or a comma-separated list: token-goat ${command} "${[first2, ...extras].join(",")}"`;
30237
+ }
30049
30238
  function parseLineRange(spec) {
30050
30239
  const m = /^(.+)@(\d+)(?:-(\d+))?$/.exec(spec);
30051
30240
  if (m === null) return null;
30052
30241
  if (fileExists(spec)) return null;
30242
+ if (m[1].includes("::")) return null;
30053
30243
  const start = parseInt(m[2], 10);
30054
30244
  const end = m[3] !== void 0 ? parseInt(m[3], 10) : start;
30055
30245
  return { file: m[1], start, end };
@@ -30108,30 +30298,49 @@ function findParentName(entry, fileSymbols) {
30108
30298
  if (doc !== "" && PARENT_IDENTIFIER_RE.test(doc)) return doc;
30109
30299
  return null;
30110
30300
  }
30111
- function formatAmbiguity(symbol3, file2, candidates, explicitRoot) {
30301
+ function formatAmbiguity(symbol3, file2, candidates, explicitRoot, commandName = "read") {
30112
30302
  const multiFile = new Set(candidates.map((c) => c.filePath)).size > 1;
30113
30303
  const displayRoot = getDisplayRoot(explicitRoot);
30114
30304
  const lines2 = [
30115
30305
  `Ambiguous symbol '${symbol3}' in '${file2}': ${candidates.length} definitions match. Retry with one of the qualified commands below to pick one:`
30116
30306
  ];
30117
30307
  const fileSymCache = /* @__PURE__ */ new Map();
30118
- for (const c of candidates) {
30119
- let fileSyms = fileSymCache.get(c.filePath);
30308
+ const getFileSyms = (filePath) => {
30309
+ let fileSyms = fileSymCache.get(filePath);
30120
30310
  if (fileSyms === void 0) {
30121
- fileSyms = querySymbols({ filePath: c.filePath, limit: 1e3 });
30122
- fileSymCache.set(c.filePath, fileSyms);
30311
+ fileSyms = querySymbols({ filePath, limit: 1e3 });
30312
+ fileSymCache.set(filePath, fileSyms);
30123
30313
  }
30124
- const parent = findParentName(c, fileSyms);
30125
- const qualifier = parent !== null ? `${parent}.${symbol3}` : symbol3;
30314
+ return fileSyms;
30315
+ };
30316
+ const parents = candidates.map((c) => findParentName(c, getFileSyms(c.filePath)));
30317
+ const plainQualifiers = candidates.map((c, i) => parents[i] !== null ? `${parents[i]}.${symbol3}` : symbol3);
30318
+ const qualifierCounts = /* @__PURE__ */ new Map();
30319
+ const fileGroupSize = /* @__PURE__ */ new Map();
30320
+ for (let i = 0; i < candidates.length; i++) {
30321
+ const c = candidates[i];
30322
+ const key = `${c.filePath} ${plainQualifiers[i]}`;
30323
+ qualifierCounts.set(key, (qualifierCounts.get(key) ?? 0) + 1);
30324
+ fileGroupSize.set(c.filePath, (fileGroupSize.get(c.filePath) ?? 0) + 1);
30325
+ }
30326
+ for (let i = 0; i < candidates.length; i++) {
30327
+ const c = candidates[i];
30328
+ const parent = parents[i];
30329
+ const plainQualifier = plainQualifiers[i];
30330
+ const collides = (qualifierCounts.get(`${c.filePath} ${plainQualifier}`) ?? 0) > 1 || parent === null && (fileGroupSize.get(c.filePath) ?? 0) > 1;
30331
+ const qualifier = collides ? `${plainQualifier}@${c.lineStart}` : plainQualifier;
30126
30332
  const retryFile = multiFile ? toDisplayPath(displayRoot, c.filePath) : file2;
30127
30333
  const label = multiFile ? `${toDisplayPath(displayRoot, c.filePath)}::${qualifier}` : qualifier;
30128
- lines2.push(` - ${label} (line ${c.lineStart}) -> token-goat read "${retryFile}::${qualifier}"`);
30334
+ lines2.push(` - ${label} (line ${c.lineStart}) -> token-goat ${commandName} "${retryFile}::${qualifier}"`);
30129
30335
  }
30130
30336
  return lines2.join("\n");
30131
30337
  }
30132
30338
  function resolveSymbolSpec(spec, forceRefresh, projectRoot) {
30133
- const { file: file2, symbol: symbol3 } = parseReadSpec(spec);
30134
- if (symbol3 === void 0 || symbol3 === "") return { kind: "none" };
30339
+ const { file: file2, symbol: rawSymbol } = parseReadSpec(spec);
30340
+ if (rawSymbol === void 0 || rawSymbol === "") return { kind: "none" };
30341
+ const anchorMatch = /^(.+)@(\d+)$/.exec(rawSymbol);
30342
+ const symbol3 = anchorMatch !== null ? anchorMatch[1] : rawSymbol;
30343
+ const lineAnchor = anchorMatch !== null ? parseInt(anchorMatch[2], 10) : void 0;
30135
30344
  const resolved = resolveIndexPath(file2, projectRoot ?? process.cwd());
30136
30345
  if (forceRefresh === true) {
30137
30346
  indexFileSync(resolved, globalDbPath());
@@ -30148,9 +30357,10 @@ function resolveSymbolSpec(spec, forceRefresh, projectRoot) {
30148
30357
  seen.add(key);
30149
30358
  distinct.push(c);
30150
30359
  }
30151
- if (distinct.length === 0) return { kind: "none" };
30152
- if (distinct.length === 1) return { kind: "ok", entry: distinct[0] };
30153
- return { kind: "ambiguous", symbol: displaySymbol, file: file2, candidates: distinct };
30360
+ const anchored = lineAnchor === void 0 ? distinct : distinct.filter((c) => c.lineStart === lineAnchor);
30361
+ if (anchored.length === 0) return { kind: "none" };
30362
+ if (anchored.length === 1) return { kind: "ok", entry: anchored[0] };
30363
+ return { kind: "ambiguous", symbol: displaySymbol, file: file2, candidates: anchored };
30154
30364
  };
30155
30365
  if (symbol3.includes(".")) {
30156
30366
  const exactMatch = querySymbols({ name: symbol3, filePath: resolved, limit: 10 });
@@ -30230,6 +30440,8 @@ function runRead(opts) {
30230
30440
  return runLineRange({ file: file2, start: lineSpec.start, end: lineSpec.end }, opts);
30231
30441
  }
30232
30442
  const messages = [`Symbol '${symbol3}' not found in '${file2}'`];
30443
+ const crossFileLead = formatCrossFileLead("read", symbol3, file2, opts.projectRoot);
30444
+ if (crossFileLead !== "") messages.push(crossFileLead);
30233
30445
  const resolved = resolveIndexPath(file2, opts.projectRoot ?? process.cwd());
30234
30446
  const closes = querySymbols({ filePath: resolved, limit: DIDYOUMEAN_LIMIT }).map((s) => s.name);
30235
30447
  if (closes.length > 0) messages.push(didYouMean(closes));
@@ -30290,6 +30502,8 @@ ${sub.text}`);
30290
30502
  return { text, code: 1 };
30291
30503
  }
30292
30504
  function runSection(opts) {
30505
+ const crossFilePairs = parseCrossFileMultiSpec(opts.spec);
30506
+ if (crossFilePairs !== null) return runSectionCrossFile(crossFilePairs, opts);
30293
30507
  const colonIdx = findSpecSeparator(opts.spec);
30294
30508
  if (colonIdx === -1) {
30295
30509
  return { text: `Invalid section spec \u2014 expected "file::Heading", got: ${opts.spec}`, code: 1 };
@@ -30346,6 +30560,42 @@ ${sub.text}`);
30346
30560
  if (anyFound) recordReadStat("section_read", fullSourceBytes, text, opts.spec);
30347
30561
  return { text, code: anyFound ? 0 : 1 };
30348
30562
  }
30563
+ function runSectionCrossFile(pairs, opts) {
30564
+ let anyFound = false;
30565
+ const jsonOut = {};
30566
+ const textBlocks = [];
30567
+ const distinctFiles = new Set(pairs.map((p) => p.file));
30568
+ const keyFor = (p) => distinctFiles.size === 1 ? p.symbol : `${p.file}::${p.symbol}`;
30569
+ for (const { file: file2, symbol: heading } of pairs) {
30570
+ const sub = runSection({ ...opts, spec: `${file2}::${heading}`, suppressStat: true });
30571
+ if (sub.code === 0) anyFound = true;
30572
+ const key = keyFor({ file: file2, symbol: heading });
30573
+ if (opts.json === true) {
30574
+ jsonOut[key] = sub.code === 0 ? JSON.parse(sub.text) : { error: sub.text };
30575
+ continue;
30576
+ }
30577
+ textBlocks.push(`${key}:
30578
+ ${sub.text}`);
30579
+ }
30580
+ const resolvePath = (f) => opts.projectRoot !== void 0 && !path47.isAbsolute(f) ? path47.resolve(opts.projectRoot, f) : f;
30581
+ const text = opts.json === true ? JSON.stringify(jsonOut, null, 2) : textBlocks.join("\n\n");
30582
+ if (anyFound) {
30583
+ const fullSourceBytes = sumFileSizes(Array.from(distinctFiles, resolvePath));
30584
+ recordReadStat("section_read", fullSourceBytes, text, opts.spec);
30585
+ }
30586
+ return { text, code: anyFound ? 0 : 1 };
30587
+ }
30588
+ function renderRefLines(ref2, displayRoot, contextLines, indent = " ") {
30589
+ const displayPath = toDisplayPath(displayRoot, ref2.filePath);
30590
+ const base = `${indent}${displayPath}:${ref2.line}: ${ref2.context}`;
30591
+ const window = buildContextWindow(ref2.filePath, ref2.line, contextLines);
30592
+ if (window === null) return [base];
30593
+ return [base, ...renderContextWindow(displayPath, ref2.line, window, "", `${indent} `)];
30594
+ }
30595
+ function withContextLines(items, contextLines) {
30596
+ if (!(contextLines > 0)) return items;
30597
+ return items.map((r) => ({ ...r, contextLines: buildContextWindow(r.filePath, r.line, contextLines) ?? [] }));
30598
+ }
30349
30599
  function applyTypedRefsTier(symName, file2, results) {
30350
30600
  if (results.length === 0) return results;
30351
30601
  try {
@@ -30383,6 +30633,8 @@ function runRefs(opts) {
30383
30633
  emitErr2(`--top must be a positive number, got: ${opts.top}`);
30384
30634
  return 1;
30385
30635
  }
30636
+ const crossFilePairs = parseCrossFileMultiSpec(opts.spec);
30637
+ if (crossFilePairs !== null) return runRefsCrossFile(crossFilePairs, opts);
30386
30638
  const { file: file2, symbols } = parseMultiRefsSpec(opts.spec);
30387
30639
  if (symbols.length <= 1) return runRefsSingle(opts);
30388
30640
  const jsonOut = {};
@@ -30391,10 +30643,18 @@ function runRefs(opts) {
30391
30643
  const refFilePaths = [];
30392
30644
  for (const sym of symbols) {
30393
30645
  const queryOpts = { name: sym };
30394
- if (file2 !== void 0 && opts.callers !== true) queryOpts.filePath = resolveIndexPath(file2);
30395
- if (opts.limit !== void 0) queryOpts.limit = opts.limit;
30646
+ if (opts.excludeTests === true) queryOpts.limit = REFS_TOP_SCAN_LIMIT;
30647
+ else if (opts.limit !== void 0) queryOpts.limit = opts.limit;
30396
30648
  else if (opts.top !== void 0) queryOpts.limit = REFS_TOP_SCAN_LIMIT;
30397
- const results = applyTypedRefsTier(sym, file2, queryRefs(queryOpts));
30649
+ let results = applyTypedRefsTier(sym, file2, queryRefs(queryOpts));
30650
+ let suppressed = 0;
30651
+ let filteredTotal;
30652
+ if (opts.excludeTests === true) {
30653
+ const f = applyExcludeTestsFilter(results);
30654
+ suppressed = f.suppressed;
30655
+ filteredTotal = f.refs.length;
30656
+ results = opts.top !== void 0 ? f.refs : f.refs.slice(0, opts.limit ?? 100);
30657
+ }
30398
30658
  if (results.length > 0) anyFound = true;
30399
30659
  refFilePaths.push(...results.map((r) => r.filePath));
30400
30660
  if (opts.json === true) {
@@ -30402,22 +30662,85 @@ function runRefs(opts) {
30402
30662
  jsonOut[sym] = topFilesJsonPayload(results, opts.top);
30403
30663
  } else {
30404
30664
  const capped = guardJsonRows(results);
30405
- const trueTotal = countRefs(queryOpts);
30406
- jsonOut[sym] = { items: capped.items, truncated: capped.truncated || trueTotal > results.length, totalCount: trueTotal };
30665
+ const trueTotal = opts.excludeTests === true ? filteredTotal ?? results.length : countRefs(queryOpts);
30666
+ jsonOut[sym] = { items: withContextLines(capped.items, opts.context ?? 0), truncated: capped.truncated || trueTotal > results.length, totalCount: trueTotal };
30407
30667
  }
30408
30668
  continue;
30409
30669
  }
30410
30670
  if (results.length === 0) {
30411
- lines2.push(`${sym}: (no references found)`);
30671
+ lines2.push(opts.excludeTests === true && suppressed > 0 ? `${sym}: (no non-test references found; ${suppressed} in test files hidden by --exclude-tests)` : `${sym}: (no references found)`);
30412
30672
  continue;
30413
30673
  }
30414
30674
  lines2.push(`${sym}:`);
30415
30675
  if (opts.top !== void 0) {
30416
- lines2.push(...renderTopFilesSummary(results, opts.top));
30676
+ lines2.push(...renderTopFilesSummary(results, opts.top, void 0, suppressed));
30677
+ } else if (opts.callers === true) {
30678
+ if (opts.excludeTests === true && suppressed > 0) lines2.push(` ${results.length} references (${suppressed} in test files hidden by --exclude-tests)`);
30679
+ lines2.push(...renderCallerGroups(results, void 0, opts.context ?? 0));
30680
+ } else {
30681
+ if (opts.excludeTests === true && suppressed > 0) lines2.push(` ${results.length} references (${suppressed} in test files hidden by --exclude-tests)`);
30682
+ for (const ref2 of results) lines2.push(...renderRefLines(ref2, void 0, opts.context ?? 0));
30683
+ }
30684
+ }
30685
+ const fullSourceBytes = sumFileSizes(refFilePaths);
30686
+ if (opts.json === true) {
30687
+ const text2 = JSON.stringify(jsonOut, null, 2);
30688
+ emit3(text2);
30689
+ if (anyFound) recordReadStat("symbol_read", fullSourceBytes, text2, opts.spec);
30690
+ return anyFound ? 0 : 1;
30691
+ }
30692
+ const text = lines2.join("\n");
30693
+ emitGuarded(text, "symbol");
30694
+ if (anyFound) recordReadStat("symbol_read", fullSourceBytes, text, opts.spec);
30695
+ return anyFound ? 0 : 1;
30696
+ }
30697
+ function runRefsCrossFile(pairs, opts) {
30698
+ const distinctFiles = new Set(pairs.map((p) => p.file));
30699
+ const keyFor = (p) => distinctFiles.size === 1 ? p.symbol : `${p.file}::${p.symbol}`;
30700
+ const jsonOut = {};
30701
+ let anyFound = false;
30702
+ const lines2 = [];
30703
+ const refFilePaths = [];
30704
+ for (const { file: file2, symbol: symbol3 } of pairs) {
30705
+ const key = keyFor({ file: file2, symbol: symbol3 });
30706
+ const queryOpts = { name: symbol3 };
30707
+ if (opts.excludeTests === true) queryOpts.limit = REFS_TOP_SCAN_LIMIT;
30708
+ else if (opts.limit !== void 0) queryOpts.limit = opts.limit;
30709
+ else if (opts.top !== void 0) queryOpts.limit = REFS_TOP_SCAN_LIMIT;
30710
+ let results = applyTypedRefsTier(symbol3, file2, queryRefs(queryOpts));
30711
+ let suppressed = 0;
30712
+ let filteredTotal;
30713
+ if (opts.excludeTests === true) {
30714
+ const f = applyExcludeTestsFilter(results);
30715
+ suppressed = f.suppressed;
30716
+ filteredTotal = f.refs.length;
30717
+ results = opts.top !== void 0 ? f.refs : f.refs.slice(0, opts.limit ?? 100);
30718
+ }
30719
+ if (results.length > 0) anyFound = true;
30720
+ refFilePaths.push(...results.map((r) => r.filePath));
30721
+ if (opts.json === true) {
30722
+ if (opts.top !== void 0) {
30723
+ jsonOut[key] = topFilesJsonPayload(results, opts.top);
30724
+ } else {
30725
+ const capped = guardJsonRows(results);
30726
+ const trueTotal = opts.excludeTests === true ? filteredTotal ?? results.length : countRefs(queryOpts);
30727
+ jsonOut[key] = { items: withContextLines(capped.items, opts.context ?? 0), truncated: capped.truncated || trueTotal > results.length, totalCount: trueTotal };
30728
+ }
30729
+ continue;
30730
+ }
30731
+ if (results.length === 0) {
30732
+ lines2.push(opts.excludeTests === true && suppressed > 0 ? `${key}: (no non-test references found; ${suppressed} in test files hidden by --exclude-tests)` : `${key}: (no references found)`);
30733
+ continue;
30734
+ }
30735
+ lines2.push(`${key}:`);
30736
+ if (opts.top !== void 0) {
30737
+ lines2.push(...renderTopFilesSummary(results, opts.top, void 0, suppressed));
30417
30738
  } else if (opts.callers === true) {
30418
- lines2.push(...renderCallerGroups(results));
30739
+ if (opts.excludeTests === true && suppressed > 0) lines2.push(` ${results.length} references (${suppressed} in test files hidden by --exclude-tests)`);
30740
+ lines2.push(...renderCallerGroups(results, void 0, opts.context ?? 0));
30419
30741
  } else {
30420
- for (const ref2 of results) lines2.push(` ${ref2.filePath}:${ref2.line}: ${ref2.context}`);
30742
+ if (opts.excludeTests === true && suppressed > 0) lines2.push(` ${results.length} references (${suppressed} in test files hidden by --exclude-tests)`);
30743
+ for (const ref2 of results) lines2.push(...renderRefLines(ref2, void 0, opts.context ?? 0));
30421
30744
  }
30422
30745
  }
30423
30746
  const fullSourceBytes = sumFileSizes(refFilePaths);
@@ -30437,11 +30760,23 @@ function runRefsSingle(opts) {
30437
30760
  const symName = symbol3 ?? file2;
30438
30761
  const queryOpts = { name: symName };
30439
30762
  const defFileHint = symbol3 !== void 0 ? resolveIndexPath(file2) : void 0;
30440
- if (defFileHint !== void 0 && opts.callers !== true) queryOpts.filePath = defFileHint;
30441
- if (opts.limit !== void 0) queryOpts.limit = opts.limit;
30763
+ if (opts.excludeTests === true) queryOpts.limit = REFS_TOP_SCAN_LIMIT;
30764
+ else if (opts.limit !== void 0) queryOpts.limit = opts.limit;
30442
30765
  else if (opts.top !== void 0) queryOpts.limit = REFS_TOP_SCAN_LIMIT;
30443
- const results = applyTypedRefsTier(symName, defFileHint, queryRefs(queryOpts));
30766
+ let results = applyTypedRefsTier(symName, defFileHint, queryRefs(queryOpts));
30767
+ let suppressed = 0;
30768
+ let filteredTotal;
30769
+ if (opts.excludeTests === true) {
30770
+ const f = applyExcludeTestsFilter(results);
30771
+ suppressed = f.suppressed;
30772
+ filteredTotal = f.refs.length;
30773
+ results = opts.top !== void 0 ? f.refs : f.refs.slice(0, opts.limit ?? 100);
30774
+ }
30444
30775
  if (results.length === 0) {
30776
+ if (opts.excludeTests === true && suppressed > 0) {
30777
+ emitErr2(`No non-test references found for '${symName}' (${suppressed} in test files hidden by --exclude-tests)`);
30778
+ return 1;
30779
+ }
30445
30780
  emitErr2(`No references found for '${symName}'`);
30446
30781
  return 1;
30447
30782
  }
@@ -30452,8 +30787,8 @@ function runRefsSingle(opts) {
30452
30787
  payload = topFilesJsonPayload(results, opts.top);
30453
30788
  } else {
30454
30789
  const capped = guardJsonRows(results);
30455
- const trueTotal = countRefs(queryOpts);
30456
- payload = { items: capped.items, truncated: capped.truncated || trueTotal > results.length, totalCount: trueTotal };
30790
+ const trueTotal = opts.excludeTests === true ? filteredTotal ?? results.length : countRefs(queryOpts);
30791
+ payload = { items: withContextLines(capped.items, opts.context ?? 0), truncated: capped.truncated || trueTotal > results.length, totalCount: trueTotal };
30457
30792
  }
30458
30793
  const text2 = JSON.stringify(payload, null, 2);
30459
30794
  emit3(text2);
@@ -30461,21 +30796,26 @@ function runRefsSingle(opts) {
30461
30796
  return 0;
30462
30797
  }
30463
30798
  const displayRoot = getDisplayRoot();
30464
- const lines2 = opts.top !== void 0 ? renderTopFilesSummary(results, opts.top, displayRoot) : opts.callers === true ? renderCallerGroups(results, displayRoot) : results.map((ref2) => `${toDisplayPath(displayRoot, ref2.filePath)}:${ref2.line}: ${ref2.context}`);
30799
+ const lines2 = opts.top !== void 0 ? renderTopFilesSummary(results, opts.top, displayRoot, suppressed) : opts.callers === true ? [...opts.excludeTests === true && suppressed > 0 ? [`${results.length} references (${suppressed} in test files hidden by --exclude-tests)`] : [], ...renderCallerGroups(results, displayRoot, opts.context ?? 0)] : [...opts.excludeTests === true && suppressed > 0 ? [`${results.length} references (${suppressed} in test files hidden by --exclude-tests)`] : [], ...results.flatMap((ref2) => renderRefLines(ref2, displayRoot, opts.context ?? 0, ""))];
30465
30800
  const text = lines2.join("\n");
30466
30801
  emitGuarded(text, "symbol");
30467
30802
  recordReadStat("symbol_read", fullSourceBytes, text, symName);
30468
30803
  return 0;
30469
30804
  }
30805
+ function applyExcludeTestsFilter(refs) {
30806
+ const filtered = refs.filter((r) => !isTestFile(r.filePath));
30807
+ return { refs: filtered, suppressed: refs.length - filtered.length };
30808
+ }
30470
30809
  function groupRefsByFile(refs) {
30471
30810
  const byFile = /* @__PURE__ */ new Map();
30472
30811
  for (const ref2 of refs) byFile.set(ref2.filePath, (byFile.get(ref2.filePath) ?? 0) + 1);
30473
30812
  return [...byFile.entries()].map(([file2, count]) => ({ file: file2, count })).sort((a, b) => b.count - a.count || (a.file < b.file ? -1 : a.file > b.file ? 1 : 0));
30474
30813
  }
30475
- function renderTopFilesSummary(refs, topN, displayRoot) {
30814
+ function renderTopFilesSummary(refs, topN, displayRoot, suppressed) {
30476
30815
  const grouped = groupRefsByFile(refs);
30477
30816
  const shown = grouped.slice(0, topN);
30478
- const lines2 = [`${refs.length} references across ${grouped.length} files (showing top ${shown.length})`];
30817
+ const suppressedNote = suppressed !== void 0 && suppressed > 0 ? ` (${suppressed} in test files hidden by --exclude-tests)` : "";
30818
+ const lines2 = [`${refs.length} references across ${grouped.length} files (showing top ${shown.length})${suppressedNote}`];
30479
30819
  for (const { file: file2, count } of shown) lines2.push(` ${count} ${toDisplayPath(displayRoot, file2)}`);
30480
30820
  const omittedFiles = grouped.length - shown.length;
30481
30821
  if (omittedFiles > 0) {
@@ -30489,7 +30829,7 @@ function topFilesJsonPayload(refs, topN) {
30489
30829
  const shown = grouped.slice(0, topN);
30490
30830
  return { fileCounts: shown, totalFiles: grouped.length, totalRefs: refs.length, shown: shown.length };
30491
30831
  }
30492
- function renderCallerGroups(refs, displayRoot) {
30832
+ function renderCallerGroups(refs, displayRoot, contextLines = 0) {
30493
30833
  const byFile = /* @__PURE__ */ new Map();
30494
30834
  for (const ref2 of refs) {
30495
30835
  const bucket = byFile.get(ref2.filePath);
@@ -30501,9 +30841,12 @@ function renderCallerGroups(refs, displayRoot) {
30501
30841
  }
30502
30842
  const lines2 = [];
30503
30843
  for (const [file2, fileRefs] of byFile) {
30504
- lines2.push(`${toDisplayPath(displayRoot, file2)}:`);
30844
+ const displayPath = toDisplayPath(displayRoot, file2);
30845
+ lines2.push(`${displayPath}:`);
30505
30846
  for (const ref2 of fileRefs) {
30506
30847
  lines2.push(` :${ref2.line} ${ref2.context !== "" ? ref2.context : "(module scope)"}`);
30848
+ const window = buildContextWindow(file2, ref2.line, contextLines);
30849
+ if (window !== null) lines2.push(...renderContextWindow(displayPath, ref2.line, window, "", " "));
30507
30850
  }
30508
30851
  }
30509
30852
  return lines2;
@@ -30542,7 +30885,19 @@ function prepareSymbolListing(file2, opts) {
30542
30885
  const fullSourceBytes = sumFileSizes([resolved]);
30543
30886
  return { kind: "ok", resolved, filtered, refCounts, fullSourceBytes, symbolsTruncated, trueSymbolCount };
30544
30887
  }
30888
+ function runPerFileListing(files, run2) {
30889
+ const blocks = [];
30890
+ let anyOk = false;
30891
+ for (const file2 of files) {
30892
+ const r = run2(file2);
30893
+ if (r.code === 0) anyOk = true;
30894
+ blocks.push(r.text);
30895
+ }
30896
+ return { text: blocks.join("\n\n"), code: anyOk ? 0 : 1 };
30897
+ }
30545
30898
  function runSkeleton(opts) {
30899
+ const multiFiles = parseMultiFileSpec(opts.file);
30900
+ if (multiFiles !== null) return runPerFileListing(multiFiles, (file2) => runSkeleton({ ...opts, file: file2 }));
30546
30901
  const prep = prepareSymbolListing(opts.file, opts);
30547
30902
  if (prep.kind === "empty") {
30548
30903
  return { text: prep.text, code: 1 };
@@ -30578,6 +30933,8 @@ function runSkeleton(opts) {
30578
30933
  return { text, code: 0 };
30579
30934
  }
30580
30935
  function runOutline(opts) {
30936
+ const multiFiles = parseMultiFileSpec(opts.file);
30937
+ if (multiFiles !== null) return runPerFileListing(multiFiles, (file2) => runOutline({ ...opts, file: file2 }));
30581
30938
  const prep = prepareSymbolListing(opts.file, opts);
30582
30939
  if (prep.kind === "empty") {
30583
30940
  return { text: prep.text, code: 1 };
@@ -31152,10 +31509,13 @@ function runBriefCore(opts) {
31152
31509
  const resolution = resolveSymbolSpec(opts.spec);
31153
31510
  if (resolution.kind === "ambiguous") {
31154
31511
  return {
31512
+ // Name the command explicitly: formatAmbiguity defaults to 'read', so brief's retry lines would otherwise tell the user to run `token-goat read`, which answers a different question than the one they asked.
31155
31513
  text: formatAmbiguity(
31156
31514
  resolution.symbol,
31157
31515
  resolution.file,
31158
- resolution.candidates
31516
+ resolution.candidates,
31517
+ void 0,
31518
+ "brief"
31159
31519
  ),
31160
31520
  code: 1
31161
31521
  };
@@ -31178,7 +31538,7 @@ function runBriefCore(opts) {
31178
31538
  if (opts.json === true) {
31179
31539
  const result = {
31180
31540
  symbol: match2,
31181
- callers: shown,
31541
+ callers: (opts.context ?? 0) > 0 ? shown.map((c) => ({ ...c, contextLines: buildContextWindow(c.file, c.line, opts.context ?? 0) ?? [] })) : shown,
31182
31542
  totalCallers,
31183
31543
  truncated,
31184
31544
  section: section2
@@ -31197,7 +31557,10 @@ function runBriefCore(opts) {
31197
31557
  ];
31198
31558
  lines2.push(`Callers (${totalCallers}):`);
31199
31559
  for (const c of shown) {
31200
- lines2.push(` ${c.caller} ${toDisplayPath(rootDir, c.file)}:${c.line}`);
31560
+ const callerDisplayPath = toDisplayPath(rootDir, c.file);
31561
+ lines2.push(` ${c.caller} ${callerDisplayPath}:${c.line}`);
31562
+ const window = buildContextWindow(c.file, c.line, opts.context ?? 0);
31563
+ if (window !== null) lines2.push(...renderContextWindow(callerDisplayPath, c.line, window, "", " "));
31201
31564
  }
31202
31565
  if (truncated) {
31203
31566
  lines2.push(` ...(${totalCallers - shown.length} more elided)`);
@@ -31229,11 +31592,40 @@ ${sub.text}`);
31229
31592
  if (anyFound) recordReadStat("brief_view", fullSourceBytes, text, opts.spec);
31230
31593
  return { text, code: anyFound ? 0 : 1 };
31231
31594
  }
31595
+ function runBriefCrossFile(pairs, opts) {
31596
+ const distinctFiles = new Set(pairs.map((p) => p.file));
31597
+ const keyFor = (p) => distinctFiles.size === 1 ? p.symbol : `${p.file}::${p.symbol}`;
31598
+ let anyFound = false;
31599
+ const jsonOut = {};
31600
+ const textBlocks = [];
31601
+ for (const { file: file2, symbol: symbol3 } of pairs) {
31602
+ const key = keyFor({ file: file2, symbol: symbol3 });
31603
+ const sub = runBriefCore({ ...opts, spec: `${file2}::${symbol3}`, suppressStat: true });
31604
+ if (sub.code === 0) anyFound = true;
31605
+ if (opts.json === true) {
31606
+ jsonOut[key] = sub.code === 0 ? JSON.parse(sub.text) : { error: sub.text };
31607
+ continue;
31608
+ }
31609
+ textBlocks.push(`${key}:
31610
+ ${sub.text}`);
31611
+ }
31612
+ const fullSourceBytes = sumFileSizes([...distinctFiles].map((f) => resolveIndexPath(f, process.cwd())));
31613
+ const text = opts.json === true ? JSON.stringify(jsonOut, null, 2) : textBlocks.join("\n\n");
31614
+ if (anyFound) recordReadStat("brief_view", fullSourceBytes, text, opts.spec);
31615
+ return { text, code: anyFound ? 0 : 1 };
31616
+ }
31232
31617
  function runBrief(opts) {
31233
31618
  if (opts.limit !== void 0 && opts.limit <= 0) {
31234
31619
  emitErr2(`--limit must be a positive number, got: ${opts.limit}`);
31235
31620
  return 1;
31236
31621
  }
31622
+ const crossFilePairs = parseCrossFileMultiSpec(opts.spec);
31623
+ if (crossFilePairs !== null) {
31624
+ const { text: text2, code: code2 } = runBriefCrossFile(crossFilePairs, opts);
31625
+ if (code2 === 0) emit3(text2);
31626
+ else emitErr2(text2);
31627
+ return code2;
31628
+ }
31237
31629
  const { file: file2, symbol: symbol3 } = parseReadSpec(opts.spec);
31238
31630
  if (symbol3 !== void 0 && symbol3 !== "" && symbol3.includes(",")) {
31239
31631
  const multiSymbols = symbol3.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
@@ -31325,6 +31717,34 @@ function parseDiffHunks(diffText) {
31325
31717
  }
31326
31718
  return hunksByFile;
31327
31719
  }
31720
+ function buildChangedRefHint(cwd, ref2) {
31721
+ const countResult = runGit(["rev-list", "--count", "HEAD"], { cwd });
31722
+ if (countResult.exitCode !== 0) {
31723
+ return null;
31724
+ }
31725
+ const commitCount = Number.parseInt(countResult.stdout.trim(), 10);
31726
+ if (!Number.isFinite(commitCount) || commitCount < 1) {
31727
+ return null;
31728
+ }
31729
+ const refResolves = runGit(["rev-parse", "--verify", "--quiet", ref2], { cwd });
31730
+ if (refResolves.exitCode === 0) {
31731
+ return null;
31732
+ }
31733
+ let suggestedRef = null;
31734
+ for (let n = commitCount - 1; n >= 1; n--) {
31735
+ const candidate = `HEAD~${n}`;
31736
+ const candidateResolves = runGit(["rev-parse", "--verify", "--quiet", candidate], { cwd });
31737
+ if (candidateResolves.exitCode === 0) {
31738
+ suggestedRef = candidate;
31739
+ break;
31740
+ }
31741
+ }
31742
+ if (suggestedRef === null) {
31743
+ suggestedRef = EMPTY_TREE_HASH;
31744
+ }
31745
+ const commitWord = commitCount === 1 ? "1 commit" : `${commitCount} commits`;
31746
+ return `Hint: this repo has only ${commitWord}; '${ref2}' does not exist. Try: token-goat changed --since ${suggestedRef}`;
31747
+ }
31328
31748
  function runChanged(opts = {}) {
31329
31749
  const ref2 = opts.ref ?? "HEAD~5";
31330
31750
  const cwd = opts.projectRoot ?? process.cwd();
@@ -31334,6 +31754,10 @@ function runChanged(opts = {}) {
31334
31754
  const result = runGit(["diff", ref2, "--name-only"], { cwd });
31335
31755
  if (result.exitCode !== 0) {
31336
31756
  emitErr2(`git diff failed: ${result.stderr}`);
31757
+ const hint = buildChangedRefHint(cwd, ref2);
31758
+ if (hint !== null) {
31759
+ emitErr2(hint);
31760
+ }
31337
31761
  return 1;
31338
31762
  }
31339
31763
  changedFiles = result.stdout.trim().split(/\r?\n/).filter(Boolean);
@@ -31439,13 +31863,16 @@ function resolveSymbolSpecOrEmitError(commandName, spec, projectRoot) {
31439
31863
  resolution.symbol,
31440
31864
  resolution.file,
31441
31865
  resolution.candidates,
31442
- projectRoot
31866
+ projectRoot,
31867
+ commandName
31443
31868
  )
31444
31869
  );
31445
31870
  return null;
31446
31871
  }
31447
31872
  if (resolution.kind === "none") {
31448
31873
  const messages = [`Symbol '${symbol3}' not found in '${file2}'`];
31874
+ const crossFileLead = formatCrossFileLead(commandName, symbol3, file2, projectRoot);
31875
+ if (crossFileLead !== "") messages.push(crossFileLead);
31449
31876
  const resolved = resolveIndexPath(file2, projectRoot ?? process.cwd());
31450
31877
  const closes = querySymbols({ filePath: resolved, limit: DIDYOUMEAN_LIMIT }).map((s) => s.name);
31451
31878
  if (closes.length > 0) messages.push(didYouMean(closes));
@@ -31655,22 +32082,29 @@ function runGrep(opts) {
31655
32082
  return 1;
31656
32083
  }
31657
32084
  const truncated = hits.slice(0, maxLines);
32085
+ if (opts.symbol === true) {
32086
+ const symbolsByFile = /* @__PURE__ */ new Map();
32087
+ for (const hit of truncated) {
32088
+ let syms = symbolsByFile.get(hit.file);
32089
+ if (syms === void 0) {
32090
+ syms = querySymbols({ filePath: resolveIndexPath(hit.file), limit: ALL_SYMBOLS_IN_FILE_LIMIT });
32091
+ symbolsByFile.set(hit.file, syms);
32092
+ }
32093
+ const enc = enclosingSymbol(syms, hit.line);
32094
+ hit.symbol = enc === null ? null : { name: enc.name, kind: enc.kind, lineStart: enc.lineStart, lineEnd: enc.lineEnd };
32095
+ }
32096
+ }
31658
32097
  if (opts.json === true) {
31659
32098
  const payload = { items: truncated, truncated: hits.length > maxLines, totalCount: hits.length };
31660
32099
  emit3(JSON.stringify(payload, null, 2));
31661
32100
  return 0;
31662
32101
  }
31663
32102
  for (const hit of truncated) {
32103
+ const symbolTag = opts.symbol === true && hit.symbol != null ? ` [${hit.symbol.name} (${hit.symbol.kind})]` : "";
31664
32104
  if (hit.context !== void 0) {
31665
- for (const ctxLine of hit.context) {
31666
- if (ctxLine.line === hit.line) {
31667
- emit3(`${hit.file}:${ctxLine.line}: ${ctxLine.text}`);
31668
- } else {
31669
- emit3(`${hit.file}-${ctxLine.line}- ${ctxLine.text}`);
31670
- }
31671
- }
32105
+ for (const line of renderContextWindow(hit.file, hit.line, hit.context, symbolTag)) emit3(line);
31672
32106
  } else {
31673
- emit3(`${hit.file}:${hit.line}: ${hit.text}`);
32107
+ emit3(`${hit.file}:${hit.line}: ${hit.text}${symbolTag}`);
31674
32108
  }
31675
32109
  }
31676
32110
  if (hits.length > maxLines) {
@@ -31853,9 +32287,24 @@ function extractExportNames(text, ext2) {
31853
32287
  }
31854
32288
  return names;
31855
32289
  }
32290
+ function runPerFileEmitting(files, label, run2) {
32291
+ let anyOk = false;
32292
+ files.forEach((file2, i) => {
32293
+ if (i > 0) emit3("");
32294
+ emit3(`# ${label}: ${file2}`);
32295
+ if (run2(file2) === 0) anyOk = true;
32296
+ });
32297
+ return anyOk ? 0 : 1;
32298
+ }
31856
32299
  function runExports(opts) {
32300
+ const multiFiles = parseMultiFileSpec(opts.file);
32301
+ if (multiFiles !== null) return runPerFileEmitting(multiFiles, "Exports", (file2) => runExports({ ...opts, file: file2 }));
31857
32302
  const symbols = querySymbols({ filePath: resolveIndexPath(opts.file), limit: 500 });
31858
32303
  const kindOf = (name2) => symbols.find((s) => s.name === name2)?.kind ?? "export";
32304
+ const locOf = (name2) => {
32305
+ const s = symbols.find((sym) => sym.name === name2);
32306
+ return s === void 0 ? null : { lineStart: s.lineStart, lineEnd: s.lineEnd };
32307
+ };
31859
32308
  const names = [];
31860
32309
  for (const s of symbols) {
31861
32310
  if (/^(?:export|pub\b|public\b)/.test(s.body.trimStart()) && !names.includes(s.name)) {
@@ -31880,12 +32329,23 @@ function runExports(opts) {
31880
32329
  }
31881
32330
  const fullSourceBytes = sumFileSizes([opts.file]);
31882
32331
  if (opts.json === true) {
31883
- const jsonText = JSON.stringify(names.map((n) => ({ name: n, kind: kindOf(n) })), null, 2);
32332
+ const jsonText = JSON.stringify(
32333
+ names.map((n) => {
32334
+ const loc = locOf(n);
32335
+ return { name: n, kind: kindOf(n), lineStart: loc?.lineStart ?? null, lineEnd: loc?.lineEnd ?? null };
32336
+ }),
32337
+ null,
32338
+ 2
32339
+ );
31884
32340
  emit3(jsonText);
31885
32341
  recordReadStat("exports", fullSourceBytes, jsonText, opts.file);
31886
32342
  return 0;
31887
32343
  }
31888
- const outLines = names.map((n) => `${kindOf(n).padEnd(10)} ${n}`);
32344
+ const outLines = names.map((n) => {
32345
+ const loc = locOf(n);
32346
+ const locSuffix = loc === null ? "" : ` (${loc.lineStart}-${loc.lineEnd})`;
32347
+ return `${kindOf(n).padEnd(10)} ${n}${locSuffix}`;
32348
+ });
31889
32349
  for (const line of outLines) {
31890
32350
  emit3(line);
31891
32351
  }
@@ -32179,6 +32639,8 @@ function importsExtensionFor(filePath) {
32179
32639
  return path47.extname(filePath);
32180
32640
  }
32181
32641
  function runImports(opts) {
32642
+ const multiFiles = parseMultiFileSpec(opts.file);
32643
+ if (multiFiles !== null) return runPerFileEmitting(multiFiles, "Imports", (file2) => runImports({ ...opts, file: file2 }));
32182
32644
  const text = readFileText(opts.file);
32183
32645
  if (text === null) {
32184
32646
  emitErr2(`Could not read: ${opts.file}`);
@@ -32272,11 +32734,12 @@ async function runSemantic(query, opts) {
32272
32734
  );
32273
32735
  const hits = mergeNearbyHits(rawHits).slice(0, n);
32274
32736
  if (hits.length > 0) {
32737
+ const enclosing = hits.map((h) => resolveEnclosingSymbol(h.filePath, h.startLine));
32275
32738
  if (opts.json === true) {
32276
- const items = hits.map((h) => ({
32739
+ const items = hits.map((h, i) => ({
32277
32740
  filePath: h.filePath,
32278
- name: null,
32279
- kind: null,
32741
+ name: enclosing[i]?.name ?? null,
32742
+ kind: enclosing[i]?.kind ?? null,
32280
32743
  startLine: h.startLine,
32281
32744
  endLine: h.endLine,
32282
32745
  distance: h.distance,
@@ -32287,10 +32750,12 @@ async function runSemantic(query, opts) {
32287
32750
  recordReadStat("semantic_search", sumFileSizes(hits.map((h) => h.filePath)), text3, query);
32288
32751
  return { text: text3, code: 0 };
32289
32752
  }
32290
- const blocks2 = hits.map(
32291
- (h) => `# ${toDisplayPath(rootDir, h.filePath)}:${h.startLine}-${h.endLine} (distance ${h.distance.toFixed(3)})
32292
- ${previewLines(h.text, 3)}`
32293
- );
32753
+ const blocks2 = hits.map((h, i) => {
32754
+ const enc = enclosing[i] ?? null;
32755
+ const suffix = enc !== null ? ` \u2014 inside ${enc.name} (${enc.kind})` : "";
32756
+ return `# ${toDisplayPath(rootDir, h.filePath)}:${h.startLine}-${h.endLine} (distance ${h.distance.toFixed(3)})${suffix}
32757
+ ${previewLines(h.text, 3)}`;
32758
+ });
32294
32759
  const text2 = guardText(blocks2.join("\n\n"), "semantic");
32295
32760
  recordReadStat("semantic_search", sumFileSizes(hits.map((h) => h.filePath)), text2, query);
32296
32761
  return { text: text2, code: 0 };
@@ -32379,7 +32844,7 @@ function runNoteList(opts = {}) {
32379
32844
  });
32380
32845
  return { text: lines2.join("\n"), code: 0 };
32381
32846
  }
32382
- var DIDYOUMEAN_LIMIT, MIN_REVERSE_MATCH_LEN, GREP_MAX_LINES, FIND_SCAN_LIMIT, REFS_TOP_SCAN_LIMIT, STALE_WARNING, PARENT_IDENTIFIER_RE, SKELETON_SYMBOL_CAP, HUNK_HEADER_RE, DEFAULT_LOG_MAX_COUNT;
32847
+ var DIDYOUMEAN_LIMIT, MIN_REVERSE_MATCH_LEN, GREP_MAX_LINES, FIND_SCAN_LIMIT, REFS_TOP_SCAN_LIMIT, STALE_WARNING, PARENT_IDENTIFIER_RE, SKELETON_SYMBOL_CAP, HUNK_HEADER_RE, EMPTY_TREE_HASH, DEFAULT_LOG_MAX_COUNT;
32383
32848
  var init_read_commands = __esm({
32384
32849
  "src/read_commands.ts"() {
32385
32850
  "use strict";
@@ -32424,6 +32889,7 @@ var init_read_commands = __esm({
32424
32889
  PARENT_IDENTIFIER_RE = /^[\w$]+$/;
32425
32890
  SKELETON_SYMBOL_CAP = 5e3;
32426
32891
  HUNK_HEADER_RE = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/;
32892
+ EMPTY_TREE_HASH = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
32427
32893
  DEFAULT_LOG_MAX_COUNT = 20;
32428
32894
  }
32429
32895
  });
@@ -48520,7 +48986,7 @@ init_util2();
48520
48986
  init_disk_cache();
48521
48987
  init_lang_patterns();
48522
48988
  import { readdirSync as readdirSync11, readFileSync as readFileSync21, statSync as statSync15 } from "fs";
48523
- import { resolve as resolve9 } from "path";
48989
+ import { resolve as resolve10 } from "path";
48524
48990
 
48525
48991
  // src/recall_index.ts
48526
48992
  init_define_import_meta_env();
@@ -48753,7 +49219,7 @@ function depLockfileFingerprintSync(cmd, cwd) {
48753
49219
  if (!candidates) return null;
48754
49220
  for (const lockfile of candidates) {
48755
49221
  try {
48756
- const content = readFileSync21(resolve9(cwd, lockfile));
49222
+ const content = readFileSync21(resolve10(cwd, lockfile));
48757
49223
  return shortFingerprint(content);
48758
49224
  } catch {
48759
49225
  continue;
@@ -48906,7 +49372,7 @@ function extractFirstPathArg(cmd, cwd, fallback) {
48906
49372
  const token2 = tokens[i];
48907
49373
  if (!token2.startsWith("-")) {
48908
49374
  if (!token2.startsWith("/")) {
48909
- return resolve9(cwd, token2);
49375
+ return resolve10(cwd, token2);
48910
49376
  }
48911
49377
  return token2;
48912
49378
  }
@@ -49277,6 +49743,7 @@ init_index_reader();
49277
49743
  init_parser_types();
49278
49744
  init_doc_embed_extract();
49279
49745
  init_paths();
49746
+ init_project();
49280
49747
  init_hooks_index();
49281
49748
  init_install();
49282
49749
  init_codex_install();
@@ -64250,15 +64717,19 @@ var BRIDGE_CAPABILITY_MATRIX = [
64250
64717
  harness: "copilot_cli",
64251
64718
  label: "Copilot CLI",
64252
64719
  sourceFile: "src/bridges/copilot_cli_install.ts (COPILOT_CLI_HOOK_EVENTS), src/bridges/copilot_cli.ts (COPILOT_TO_TG_EVENT)",
64253
- implemented: /* @__PURE__ */ new Set(["pre_tool_use", "post_tool_use", "pre_compact", "stop", "subagent_stop", "user_prompt_submit"]),
64720
+ implemented: /* @__PURE__ */ new Set([
64721
+ "session_start",
64722
+ "pre_tool_use",
64723
+ "post_tool_use",
64724
+ "pre_compact",
64725
+ "stop",
64726
+ "subagent_stop",
64727
+ "user_prompt_submit"
64728
+ ]),
64254
64729
  reasons: [
64255
64730
  {
64256
64731
  events: ["notification"],
64257
64732
  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"
64258
- },
64259
- {
64260
- events: ["session_start"],
64261
- reason: "COPILOT_TO_TG_EVENT has no session-start mapping wired yet -- left unimplemented rather than guessed at"
64262
64733
  }
64263
64734
  ]
64264
64735
  },
@@ -64872,7 +65343,7 @@ function checkDbExists(dataDir2) {
64872
65343
  return {
64873
65344
  name: "Database",
64874
65345
  status: "ok",
64875
- message: `global.db exists (${toKB(sizeBytes)} KB)`
65346
+ message: `global.db exists (${toKB(sizeBytes)} KB) at ${dbPath}`
64876
65347
  };
64877
65348
  }
64878
65349
  function checkSymbolBodySize(dbPath) {
@@ -64920,6 +65391,13 @@ function checkSymbolCount(dbPath, rootDir) {
64920
65391
  message: `${fileCount} file(s) indexed but 0 symbols extracted \u2014 the parser may not be running (check the worker log); try 'token-goat index --force'`
64921
65392
  };
64922
65393
  }
65394
+ if (fileCount === 0 && symbolCount === 0) {
65395
+ return {
65396
+ name: "Symbols",
65397
+ status: "warn",
65398
+ message: `no files indexed for this project \u2014 every read command will return empty, which looks like a genuine "not found" rather than a missing index; run 'token-goat index .' here`
65399
+ };
65400
+ }
64923
65401
  return {
64924
65402
  name: "Symbols",
64925
65403
  status: "ok",
@@ -69571,7 +70049,7 @@ import { existsSync as existsSync33, readdirSync as readdirSync20, statSync as s
69571
70049
  import * as http from "http";
69572
70050
  import * as https from "https";
69573
70051
  import { isIPv4, isIPv6 } from "net";
69574
- import { resolve as resolve17, join as join41 } from "path";
70052
+ import { resolve as resolve18, join as join41 } from "path";
69575
70053
  import { URL as URL2 } from "url";
69576
70054
  import { promisify } from "util";
69577
70055
  import { lookup as dnsLookup } from "dns";
@@ -69743,7 +70221,7 @@ function cleanupStaleDownloads() {
69743
70221
  const files = readdirSync20(cacheDir);
69744
70222
  for (const file2 of files) {
69745
70223
  if (file2.endsWith(".tmp")) {
69746
- const filePath = resolve17(cacheDir, file2);
70224
+ const filePath = resolve18(cacheDir, file2);
69747
70225
  try {
69748
70226
  const stat2 = statSync26(filePath);
69749
70227
  if (Date.now() - stat2.mtimeMs < STALE_DOWNLOAD_AGE_MS) continue;
@@ -72020,6 +72498,9 @@ function runHintStatsCommand(opts = {}) {
72020
72498
  `);
72021
72499
  return;
72022
72500
  }
72501
+ if (rows.every((r) => r.emitted === 0 && r.actedOn === 0)) {
72502
+ process.stdout.write("No hint emissions recorded yet \u2014 the zeros below are absence of data, not measured ineffectiveness.\n");
72503
+ }
72023
72504
  printSummary(rows);
72024
72505
  }
72025
72506
 
@@ -72278,7 +72759,11 @@ async function cmdIndex(pathArg, opts = {}) {
72278
72759
  function cmdMap(opts) {
72279
72760
  const map3 = buildProjectMap(process.cwd(), { compact: opts.compact === true });
72280
72761
  const text = formatProjectMap(map3, map3.compact);
72281
- out(text);
72762
+ if (opts.json === true) {
72763
+ out(JSON.stringify(map3));
72764
+ } else {
72765
+ out(text);
72766
+ }
72282
72767
  const bytesSaved = mapLookupBytesSaved(map3, text);
72283
72768
  recordStat("map_lookup", bytesSaved, Math.round(bytesSaved / 4));
72284
72769
  }
@@ -72320,8 +72805,8 @@ async function cmdMcpServe() {
72320
72805
  const server = createMcpServer2();
72321
72806
  const transport = new StdioServerTransport();
72322
72807
  await server.connect(transport);
72323
- await new Promise((resolve24) => {
72324
- server.server.onclose = resolve24;
72808
+ await new Promise((resolve25) => {
72809
+ server.server.onclose = resolve25;
72325
72810
  });
72326
72811
  }
72327
72812
  async function cmdHook(event, opts) {
@@ -72528,6 +73013,14 @@ async function cmdDoctor(opts) {
72528
73013
  if (project !== null) {
72529
73014
  doctorOpts.rootDir = project.root;
72530
73015
  }
73016
+ if (opts.json === true) {
73017
+ const results = runDoctor(doctorOpts.dataDir, doctorOpts.configPath, doctorOpts.rootDir);
73018
+ out(JSON.stringify(results));
73019
+ if (results.some((r) => r.status === "fail")) {
73020
+ throw new CliError("doctor checks failed");
73021
+ }
73022
+ return;
73023
+ }
72531
73024
  const code = await runDoctorAndExit(doctorOpts);
72532
73025
  if (code !== 0) {
72533
73026
  throw new CliError("doctor checks failed");
@@ -73031,6 +73524,16 @@ function runExitText(fn) {
73031
73524
  process.exitCode = 1;
73032
73525
  }
73033
73526
  }
73527
+ function noteExtraFileArgs(command, first2, extras, fn) {
73528
+ const result = fn();
73529
+ if (extras === void 0 || extras.length === 0) return result;
73530
+ return { text: `${extraFileArgsNote(command, first2, extras)}
73531
+ ${result.text}`, code: result.code };
73532
+ }
73533
+ function emitExtraFileArgsNote(command, first2, extras) {
73534
+ if (extras === void 0 || extras.length === 0) return;
73535
+ out(extraFileArgsNote(command, first2, extras));
73536
+ }
73034
73537
  function cmdCompress(opts) {
73035
73538
  try {
73036
73539
  if (opts.compress === false) {
@@ -73155,6 +73658,10 @@ async function cmdSkillList(opts) {
73155
73658
  return `${s.name.padEnd(25)} ${bodyKb.padStart(6)}K ${compactKb.padStart(6)}K ${marker} ${s.hitCount.toString().padStart(3)} ${age.padStart(3)} ${staleStatus}`;
73156
73659
  });
73157
73660
  const header = `${"Name".padEnd(25)} ${"Body".padStart(6)} ${"Compact".padStart(6)} Marker Hits Age Status`;
73661
+ if (skills.length === 0) {
73662
+ out("No skills cached yet.");
73663
+ return;
73664
+ }
73158
73665
  out([header, ...lines2].join("\n"));
73159
73666
  }
73160
73667
  }
@@ -73533,7 +74040,7 @@ function cmdWriteFile(dest, opts) {
73533
74040
  throw new CliError(`TOKEN_GOAT_MAX_STDIN_MB must be a positive integer; got '${process.env["TOKEN_GOAT_MAX_STDIN_MB"] ?? ""}'`);
73534
74041
  }
73535
74042
  const maxBytes = maxMB * 1024 * 1024;
73536
- return new Promise((resolve24, reject) => {
74043
+ return new Promise((resolve25, reject) => {
73537
74044
  const chunks = [];
73538
74045
  let totalBytes = 0;
73539
74046
  let settled = false;
@@ -73557,7 +74064,7 @@ function cmdWriteFile(dest, opts) {
73557
74064
  try {
73558
74065
  atomicWriteBuffer(dest, Buffer.concat(chunks));
73559
74066
  enqueueDirtyPathSafe(dest);
73560
- resolve24();
74067
+ resolve25();
73561
74068
  } catch (e) {
73562
74069
  try {
73563
74070
  mapFsError(e, void 0, dest);
@@ -73992,19 +74499,26 @@ function buildProgram() {
73992
74499
  process.exitCode = 1;
73993
74500
  }
73994
74501
  };
73995
- 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(
73996
- (name2, opts) => runExitText(
74502
+ program2.command("symbol <name>").description("search for a symbol by name").option("-l, --limit <n>", "max results").option("-f, --file <path>", "restrict to one file").option("-k, --kind <kind>", "restrict to one kind (function, class, ...)").option("-p, --project [path]", "scope search to one project root instead of the global index (defaults to cwd)").option("-j, --json", "output as JSON").action((name2, opts) => {
74503
+ let projectRoot;
74504
+ if (opts.project === true) {
74505
+ projectRoot = resolveProjectRoot({ project: process.cwd() });
74506
+ } else if (typeof opts.project === "string") {
74507
+ projectRoot = resolveProjectRoot({ project: opts.project });
74508
+ }
74509
+ return runExitText(
73997
74510
  () => runSymbol({
73998
74511
  name: name2,
73999
74512
  limit: opts.limit !== void 0 ? requireNonNegativeInt("--limit", opts.limit) : 20,
74000
74513
  ...opts.file !== void 0 ? { file: opts.file } : {},
74001
74514
  ...opts.kind !== void 0 ? { kind: opts.kind } : {},
74515
+ ...projectRoot !== void 0 ? { projectRoot } : {},
74002
74516
  ...opts.json === true ? { json: true } : {}
74003
74517
  })
74004
- )
74005
- );
74518
+ );
74519
+ });
74006
74520
  program2.command("read <spec>").description(
74007
- "read one symbol's full body (spec: file::symbol; disambiguate a name shared by several classes with file::Parent.symbol; comma-separated file::a,b for a merged multi-symbol view, or a::x,b::y to merge symbols across several files)"
74521
+ "read one symbol's full body (spec: file::symbol; disambiguate a name shared by several classes with file::Parent.symbol; a trailing @LINE anchor -- file::symbol@LINE, or combined as file::Parent.symbol@LINE -- picks out a specific candidate by its exact starting line, for the case a Parent qualifier can't reach (e.g. a top-level definition); comma-separated file::a,b for a merged multi-symbol view, or a::x,b::y to merge symbols across several files)"
74008
74522
  ).option("-j, --json", "output as JSON").option("--force-refresh", "reparse file from disk before querying (ignore stale index)").option("--stats", "add per-symbol reference count and doc-coverage flag").action(
74009
74523
  (spec, opts) => runExitText(
74010
74524
  () => runRead({
@@ -74016,13 +74530,14 @@ function buildProgram() {
74016
74530
  )
74017
74531
  );
74018
74532
  program2.command("brief <spec>").description(
74019
- "symbol body + callers + containing doc section in one call (spec: file::symbol; comma-separated file::a,b for a merged multi-symbol view)"
74020
- ).option("-j, --json", "output as JSON").option("--limit <n>", "max callers to show (default: 20)").action(
74533
+ "symbol body + callers + containing doc section in one call (spec: file::symbol; also accepts the file::symbol@LINE anchor form documented under `read`; comma-separated file::a,b for a merged multi-symbol view; cross-file a.ts::x,b.ts::y is also supported)"
74534
+ ).option("-j, --json", "output as JSON").option("--limit <n>", "max callers to show (default: 20)").option("-C, --context <n>", "lines of call-site source to show before and after each caller (default 0)").action(
74021
74535
  (spec, opts) => runExit(
74022
74536
  () => runBrief({
74023
74537
  spec,
74024
74538
  ...opts.json === true ? { json: true } : {},
74025
- ...opts.limit !== void 0 ? { limit: requireNonNegativeInt("--limit", opts.limit) } : {}
74539
+ ...opts.limit !== void 0 ? { limit: requireNonNegativeInt("--limit", opts.limit) } : {},
74540
+ ...opts.context !== void 0 ? { context: requireNonNegativeInt("--context", opts.context) } : {}
74026
74541
  })
74027
74542
  )
74028
74543
  );
@@ -74032,44 +74547,56 @@ function buildProgram() {
74032
74547
  (spec, opts) => opts.list === true ? runExit(() => runListSections({ file: spec, ...opts.json === true ? { json: true } : {} })) : runExitText(() => runSection({ spec, ...opts.json === true ? { json: true } : {} }))
74033
74548
  );
74034
74549
  program2.command("semantic <query>").description("semantic search (falls back to full-text search)").option("-l, --limit <n>", "max results").option("-j, --json", "output as JSON").action(guard(cmdSemantic));
74035
- 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(
74036
- (file2, opts) => runExitText(
74037
- () => runSkeleton({
74038
- file: file2,
74039
- ...opts.json === true ? { json: true } : {},
74040
- ...opts.minLines !== void 0 ? { minLines: requireNonNegativeInt("--min-lines", opts.minLines) } : {},
74041
- ...opts.forceRefresh === true ? { forceRefresh: true } : {},
74042
- ...opts.stats === true ? { stats: true } : {}
74043
- })
74550
+ program2.command("skeleton <file> [more...]").description('list all symbols in a file without bodies (also accepts a comma-separated file list "a,b,c" for one headed block per file)').option("-j, --json", "output as JSON").option("--min-lines <n>", "only show symbols at least N lines long").option("--force-refresh", "reparse file from disk before querying (ignore stale index)").option("--stats", "add per-symbol reference count and doc-coverage flag").action(
74551
+ (file2, more, opts) => runExitText(
74552
+ () => noteExtraFileArgs(
74553
+ "skeleton",
74554
+ file2,
74555
+ more,
74556
+ () => runSkeleton({
74557
+ file: file2,
74558
+ ...opts.json === true ? { json: true } : {},
74559
+ ...opts.minLines !== void 0 ? { minLines: requireNonNegativeInt("--min-lines", opts.minLines) } : {},
74560
+ ...opts.forceRefresh === true ? { forceRefresh: true } : {},
74561
+ ...opts.stats === true ? { stats: true } : {}
74562
+ })
74563
+ )
74044
74564
  )
74045
74565
  );
74046
- 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(
74047
- (file2, opts) => runExitText(
74048
- () => runOutline({
74049
- file: file2,
74050
- ...opts.json === true ? { json: true } : {},
74051
- ...opts.minLines !== void 0 ? { minLines: requireNonNegativeInt("--min-lines", opts.minLines) } : {},
74052
- ...opts.forceRefresh === true ? { forceRefresh: true } : {},
74053
- ...opts.stats === true ? { stats: true } : {}
74054
- })
74566
+ program2.command("outline <file> [more...]").description('list symbols with line ranges and docstrings (also accepts a comma-separated file list "a,b,c" for one headed block per file)').option("-j, --json", "output as JSON").option("--min-lines <n>", "only show symbols at least N lines long").option("--force-refresh", "reparse file from disk before querying (ignore stale index)").option("--stats", "add per-symbol reference count and doc-coverage flag").action(
74567
+ (file2, more, opts) => runExitText(
74568
+ () => noteExtraFileArgs(
74569
+ "outline",
74570
+ file2,
74571
+ more,
74572
+ () => runOutline({
74573
+ file: file2,
74574
+ ...opts.json === true ? { json: true } : {},
74575
+ ...opts.minLines !== void 0 ? { minLines: requireNonNegativeInt("--min-lines", opts.minLines) } : {},
74576
+ ...opts.forceRefresh === true ? { forceRefresh: true } : {},
74577
+ ...opts.stats === true ? { stats: true } : {}
74578
+ })
74579
+ )
74055
74580
  )
74056
74581
  );
74057
- program2.command("refs <spec>").description("find references to one or more symbols (spec: file::symbol, symbol, or comma-separated a,b,c / file::a,b for a merged multi-symbol view). For an unambiguous TypeScript symbol, automatically type-resolves candidates via the TypeScript compiler API to drop same-named-different-symbol false positives; falls back to name-based matching when that is not possible.").option("--callers", "group references by their enclosing caller symbol").option("-l, --limit <n>", "max results").option(
74582
+ program2.command("refs <spec>").description("find references to one or more symbols (spec: file::symbol, symbol, or comma-separated a,b,c / file::a,b for a merged multi-symbol view; cross-file a.ts::x,b.ts::y is also supported). For an unambiguous TypeScript symbol, automatically type-resolves candidates via the TypeScript compiler API to drop same-named-different-symbol false positives; falls back to name-based matching when that is not possible.").option("--callers", "group references by their enclosing caller symbol").option("-l, --limit <n>", "max results").option(
74058
74583
  "--top <n>",
74059
74584
  "for a high-fanout symbol, group references by file (count only) and show only the top N files by reference count instead of a per-line dump"
74060
- ).option("-j, --json", "output as JSON").action(
74585
+ ).option("-C, --context <n>", "lines of call-site source to show before and after each reference (default 0)").option("-j, --json", "output as JSON").option("--exclude-tests", "hide references whose call site lives in a test file (opt-in; default output is unchanged)").action(
74061
74586
  (spec, opts) => runExit(
74062
74587
  () => runRefs({
74063
74588
  spec,
74064
74589
  ...opts.callers === true ? { callers: true } : {},
74065
74590
  ...opts.json === true ? { json: true } : {},
74066
74591
  ...opts.limit !== void 0 ? { limit: requireNonNegativeInt("--limit", opts.limit) } : {},
74067
- ...opts.top !== void 0 ? { top: requireNonNegativeInt("--top", opts.top) } : {}
74592
+ ...opts.top !== void 0 ? { top: requireNonNegativeInt("--top", opts.top) } : {},
74593
+ ...opts.context !== void 0 ? { context: requireNonNegativeInt("--context", opts.context) } : {},
74594
+ ...opts.excludeTests === true ? { excludeTests: true } : {}
74068
74595
  })
74069
74596
  )
74070
74597
  );
74071
74598
  program2.command("index [path]").description("parse all git-tracked files and (re)build the symbol index").option("--walk", "if not a git repo, index a bounded directory walk instead (skips .env / generated / oversized trees)").option("--force", "bypass the SHA-freshness cache and reindex every tracked file, even byte-identical ones (e.g. after a parser upgrade changes what gets extracted)").option("--force-walk", `index a non-git folder via --walk and raise its ${MAX_FILES_SCANNED} source-file refusal to ${MAX_FILES_SCANNED_FORCED} (slow; produces a large index)`).action(guard(cmdIndex));
74072
- program2.command("map").description("project overview").option("-c, --compact", "compact, low-token summary").action(guard(cmdMap));
74599
+ program2.command("map").description("project overview").option("-c, --compact", "compact, low-token summary").option("--json", "emit the project map as JSON instead of text").action(guard(cmdMap));
74073
74600
  program2.command("bridges-status").description("hook-event parity matrix across every AI-harness bridge (read-only static analysis, never invokes a real harness binary)").option("--json", "emit the matrix as JSON instead of text").action(guard(cmdBridgesStatus));
74074
74601
  program2.command("commands").description("machine-readable manifest of every registered command, its options, and its arguments").option("--json", "emit the manifest as JSON instead of text").option("--grep <pattern>", "filter to commands whose name, description, or aliases match this regex").action(guard(cmdCommands));
74075
74602
  program2.command("mcp-serve").description("run token-goat as an MCP stdio server exposing surgical reads and local compression/handoff tools").action(guard(cmdMcpServe));
@@ -74085,7 +74612,7 @@ function buildProgram() {
74085
74612
  worker.command("stop").description("stop the background indexer").action(guard(cmdWorkerStop));
74086
74613
  worker.command("status").description("check if the indexer is running").action(guard(cmdWorkerStatus));
74087
74614
  program2.command("stats").description("show session statistics (bare = totals only; --full for the breakdown)").option("-j, --json", "output as JSON").option("--full", "show the full breakdown (by source, by command, by day)").option("--short", "force the rich short KPI view even when stdout is not a TTY (e.g. piped)").option("--window-days <days>", "days to include (0 = all time)", "30").option("--home-dir <path>", "home directory (for testing)").action(guard(cmdStats));
74088
- program2.command("doctor").description("diagnose token-goat health").option("--context", "include context footprint analysis").action(guard(cmdDoctor));
74615
+ program2.command("doctor").description("diagnose token-goat health").option("--context", "include context footprint analysis").option("--json", "emit check results as JSON instead of text").action(guard(cmdDoctor));
74089
74616
  program2.command("context-stats").description("show context statistics").option("--project <path>", "project root to analyze").option("-j, --json", "output as JSON").option("--fix", "apply automatic fixes (confirm-gated; shows a diff before writing)").option("-y, --yes", "with --fix, apply without prompting (non-interactive / scripted use)").action(guard(cmdContextStats));
74090
74617
  program2.command("bootstrap-audit").description("audit Claude Code startup-context contributors without reading prompt bodies").option("--project <path>", "project root to analyze").option("--home <path>", "home directory override (for CI/testing)").option("--follow-links", "follow external symlink/junction roots and direct children").option("-j, --json", "output as JSON").option("--top <n>", "largest metadata entries to show (default 10)", "10").option("--warn-tokens <n>", "warn when total estimated startup tokens exceed n").option("--fail-tokens <n>", "fail when total estimated startup tokens exceed n").option("--warn-bytes <n>", "warn when agent/skill metadata bytes exceed n").option("--fail-bytes <n>", "fail when agent/skill metadata bytes exceed n").action(guard(cmdBootstrapAudit));
74091
74618
  program2.command("memory").description("analyze CLAUDE.md files for duplicate/overlapping content (--fix to apply safe mechanical fixes)").option("--project <path>", "project root to analyze").option("--analyze", "report-only analysis (default)").option("--fix", "remove exact-duplicate lines (confirm-gated; shows a diff before writing)").option("--yes", "apply --fix changes without prompting (non-interactive)").action(guard(cmdMemory));
@@ -74099,11 +74626,17 @@ function buildProgram() {
74099
74626
  program2.command("bash-output [id]").description("retrieve cached bash output by ID or file").option("--head <n>", "show first N lines").option("--tail <n>", "show last N lines").option("--grep <pattern>", "filter lines matching regex").option("--max-matches <n>", "cap --grep output to the first N matching lines").option("--section <heading>", "extract a specific section from the output").option("--full", "print the entire cached entry with no head/tail elision").option("--file <path>", "read from raw output file instead of cache").option("--transcript", "parse the --file as a JSONL agent transcript: keep assistant text blocks in order before filtering").action(guard(cmdBashOutput));
74100
74627
  program2.command("web-output [id]").description("retrieve a cached WebFetch response body by ID").option("--head <n>", "show first N lines").option("--tail <n>", "show last N lines").option("--grep <pattern>", "filter lines matching regex").option("--max-matches <n>", "cap --grep output to the first N matching lines").option("--section <heading>", "extract a specific section from the response").option("--full", "print the entire cached entry with no head/tail elision").action(guard(cmdWebOutput));
74101
74628
  program2.command("mcp-output [id]").description("retrieve a cached MCP tool result by ID (the id an MCP post_tool_use hook cached, or a `[token-goat: compressed, full via mcp-output <id>]` label points here)").option("--head <n>", "show first N lines").option("--tail <n>", "show last N lines").option("--grep <pattern>", "filter lines matching regex").option("--max-matches <n>", "cap --grep output to the first N matching lines").option("--section <heading>", "extract a specific section from the result").option("--full", "print the entire cached entry with no head/tail elision").action(guard(cmdMcpOutput));
74102
- program2.command("exports <file>").description("list exported (public) symbols in a file").option("-j, --json", "output as JSON").action(
74103
- (file2, opts) => runExit(() => runExports({ file: file2, ...opts.json === true ? { json: true } : {} }))
74629
+ program2.command("exports <file> [more...]").description('list exported (public) symbols in a file (also accepts a comma-separated file list "a,b,c" for one headed block per file)').option("-j, --json", "output as JSON").action(
74630
+ (file2, more, opts) => runExit(() => {
74631
+ emitExtraFileArgsNote("exports", file2, more);
74632
+ return runExports({ file: file2, ...opts.json === true ? { json: true } : {} });
74633
+ })
74104
74634
  );
74105
- program2.command("imports <file>").description("list the modules a file imports").option("-j, --json", "output as JSON").action(
74106
- (file2, opts) => runExit(() => runImports({ file: file2, ...opts.json === true ? { json: true } : {} }))
74635
+ program2.command("imports <file> [more...]").description('list the modules a file imports (also accepts a comma-separated file list "a,b,c" for one headed block per file)').option("-j, --json", "output as JSON").action(
74636
+ (file2, more, opts) => runExit(() => {
74637
+ emitExtraFileArgsNote("imports", file2, more);
74638
+ return runImports({ file: file2, ...opts.json === true ? { json: true } : {} });
74639
+ })
74107
74640
  );
74108
74641
  program2.command("find <pattern>").description("find files containing a symbol matching a pattern").option("-j, --json", "output as JSON").option("-l, --limit <n>", "max results").action(
74109
74642
  (pattern, opts) => runExit(
@@ -74114,7 +74647,7 @@ function buildProgram() {
74114
74647
  })
74115
74648
  )
74116
74649
  );
74117
- program2.command("grep <pattern> [paths...]").description("regex search over files, caching nothing (session-aware grep)").option("-j, --json", "output as JSON").option("--max-lines <n>", "max matching lines to print").option("--no-recursive", "do not descend into subdirectories").option("-C, --context <n>", "lines of context to show before and after each match").action(
74650
+ program2.command("grep <pattern> [paths...]").description("regex search over files, caching nothing (session-aware grep)").option("-j, --json", "output as JSON").option("--max-lines <n>", "max matching lines to print").option("--no-recursive", "do not descend into subdirectories").option("-C, --context <n>", "lines of context to show before and after each match").option("--symbol", "annotate each hit with its enclosing symbol (name and kind)").action(
74118
74651
  (pattern, paths, opts) => runExit(
74119
74652
  () => runGrep({
74120
74653
  pattern,
@@ -74122,7 +74655,8 @@ function buildProgram() {
74122
74655
  ...opts.json === true ? { json: true } : {},
74123
74656
  ...opts.maxLines !== void 0 ? { maxLines: requirePositiveInt("--max-lines", opts.maxLines) } : {},
74124
74657
  ...opts.recursive === false ? { recursive: false } : {},
74125
- ...opts.context !== void 0 ? { context: requireNonNegativeInt("--context", opts.context) } : {}
74658
+ ...opts.context !== void 0 ? { context: requireNonNegativeInt("--context", opts.context) } : {},
74659
+ ...opts.symbol === true ? { symbol: true } : {}
74126
74660
  })
74127
74661
  )
74128
74662
  );
@@ -74133,16 +74667,18 @@ function buildProgram() {
74133
74667
  program2.command("skill-history").description("list cached skill versions newest-first").option("-j, --json", "output as JSON").action(guard(cmdSkillHistory));
74134
74668
  program2.command("skill-diff <name>").description("show diff between two cached versions of a skill").action(guard(cmdSkillDiff));
74135
74669
  program2.command("skill-section <nameHeading> [headingArg]").description("extract a named section from a skill").action(guard(cmdSkillSection));
74136
- program2.command("callers <symbol>").description("find all callers of a symbol, resolved to their enclosing function").option("-j, --json", "output as JSON").option("-l, --limit <n>", "max references to scan").action(
74670
+ program2.command("callers <symbol>").description("find all callers of a symbol, resolved to their enclosing function (accepts file::symbol to disambiguate which same-named definition is meant)").option("-j, --json", "output as JSON").option("-l, --limit <n>", "max references to scan").option("-C, --context <n>", "lines of call-site source to show before and after each caller (default 0)").option("--exclude-tests", "hide callers whose call site lives in a test file (opt-in; default output is unchanged)").action(
74137
74671
  (symbol3, opts) => runExit(
74138
74672
  () => runCallers({
74139
74673
  symbol: symbol3,
74140
74674
  ...opts.json === true ? { json: true } : {},
74141
- ...opts.limit !== void 0 ? { limit: requireNonNegativeInt("--limit", opts.limit) } : {}
74675
+ ...opts.limit !== void 0 ? { limit: requireNonNegativeInt("--limit", opts.limit) } : {},
74676
+ ...opts.context !== void 0 ? { context: requireNonNegativeInt("--context", opts.context) } : {},
74677
+ ...opts.excludeTests === true ? { excludeTests: true } : {}
74142
74678
  })
74143
74679
  )
74144
74680
  );
74145
- program2.command("call-chain <symbol>").description("transitive callers up toward entry points (BFS, cycle-safe)").option("-d, --depth <n>", "max BFS depth (default 8)").option("-j, --json", "output as JSON").action(
74681
+ program2.command("call-chain <symbol>").description("transitive callers up toward entry points (BFS, cycle-safe; accepts file::symbol to disambiguate which same-named definition is meant)").option("-d, --depth <n>", "max BFS depth (default 8)").option("-j, --json", "output as JSON").action(
74146
74682
  (symbol3, opts) => runExit(
74147
74683
  () => runCallChain({
74148
74684
  symbol: symbol3,
@@ -74151,7 +74687,7 @@ function buildProgram() {
74151
74687
  })
74152
74688
  )
74153
74689
  );
74154
- program2.command("impact <symbol>").description("transitive set of callers impacted by a change (with hop depth)").option("--top <n>", "limit output to top N results").option("-j, --json", "output as JSON").action(
74690
+ program2.command("impact <symbol>").description("transitive set of callers impacted by a change (with hop depth; accepts file::symbol to disambiguate which same-named definition is meant)").option("--top <n>", "limit output to top N results").option("-j, --json", "output as JSON").action(
74155
74691
  (symbol3, opts) => runExit(
74156
74692
  () => runImpact({
74157
74693
  symbol: symbol3,
@@ -74160,13 +74696,14 @@ function buildProgram() {
74160
74696
  })
74161
74697
  )
74162
74698
  );
74163
- program2.command("dead").description("symbols with zero references (default kind: function)").option("-k, --kind <kind>", "symbol kind to check (function, method, class, ...)").option("--include-private", "include _-prefixed names").option("--top <n>", "limit output to top N results").option("-j, --json", "output as JSON").action(
74699
+ program2.command("dead").description("symbols with zero references (default kind: function)").option("-k, --kind <kind>", "symbol kind to check (function, method, class, ...)").option("--include-private", "include _-prefixed names").option("--top <n>", "limit output to top N results").option("-j, --json", "output as JSON").option("--exclude-tests", "hide dead symbols defined in a test file (opt-in; default output is unchanged)").action(
74164
74700
  (opts) => runExit(
74165
74701
  () => runDead({
74166
74702
  ...opts.kind !== void 0 ? { kind: opts.kind } : {},
74167
74703
  ...opts.includePrivate === true ? { includePrivate: true } : {},
74168
74704
  ...opts.top !== void 0 ? { top: requireNonNegativeInt("--top", opts.top) } : {},
74169
- ...opts.json === true ? { json: true } : {}
74705
+ ...opts.json === true ? { json: true } : {},
74706
+ ...opts.excludeTests === true ? { excludeTests: true } : {}
74170
74707
  })
74171
74708
  )
74172
74709
  );
@@ -74185,7 +74722,7 @@ function buildProgram() {
74185
74722
  program2.command("scope <fileColonLine>").description("list symbols enclosing a file:line position, innermost first").option("-j, --json", "output as JSON").action(
74186
74723
  (spec, opts) => runExit(() => runScope({ spec, ...opts.json === true ? { json: true } : {} }))
74187
74724
  );
74188
- program2.command("similar <spec>").description('find symbols similar to a given "file::symbol" anchor using FTS').option("--top <n>", "max results (default 10)").option("-j, --json", "output as JSON").action(
74725
+ program2.command("similar <spec>").description('find symbols similar to a given "file::symbol" anchor using FTS (also accepts the file::symbol@LINE anchor form documented under `read`)').option("--top <n>", "max results (default 10)").option("-j, --json", "output as JSON").action(
74189
74726
  (spec, opts) => runExit(
74190
74727
  () => runSimilar({
74191
74728
  spec,
@@ -74224,7 +74761,7 @@ function buildProgram() {
74224
74761
  })
74225
74762
  )
74226
74763
  );
74227
- program2.command("blame <spec>").description('git blame for the line range of a symbol ("file::symbol")').option("-j, --json", "output as JSON").action(
74764
+ program2.command("blame <spec>").description('git blame for the line range of a symbol ("file::symbol"; also accepts the file::symbol@LINE anchor form documented under `read`)').option("-j, --json", "output as JSON").action(
74228
74765
  (spec, opts) => runExit(() => runBlame({ spec, ...opts.json === true ? { json: true } : {} }))
74229
74766
  );
74230
74767
  program2.command("ask <question>").description("(experimental) find relevant code context; synthesize with an LLM if TOKEN_GOAT_ASK_BACKEND is set").option("--top <n>", "max FTS hits to surface (default 8)").option("-j, --json", "output as JSON").action(
@@ -74308,16 +74845,16 @@ function buildProgram() {
74308
74845
  }))());
74309
74846
  program2.command("fetch-image <url>").description("fetch an image URL and shrink it (saves to --out path or a temp file)").option("--out <path>", "output file path").option("-j, --json", "output as JSON").action((url2, opts) => guard(() => cmdFetchImage({ url: url2, ...opts.out !== void 0 ? { out: opts.out } : {}, ...opts.json === true ? { json: true } : {} }))());
74310
74847
  program2.command("history").description("show recent session history: bash commands and web fetches (current-session or recent cache)").option("--limit <n>", "max entries to show (default: 30)").option("-j, --json", "output as JSON").action((opts) => guard(() => cmdHistory(opts))());
74311
- 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(
74312
- (opts) => runExit(
74848
+ program2.command("changed [ref]").description("list files or symbols changed since a git ref").option("--since <ref>", "git ref to compare against (default: HEAD~5)").option("--symbol", "list symbols instead of files").option("-j, --json", "output as JSON").action(
74849
+ (ref2, opts) => runExit(
74313
74850
  () => runChanged({
74314
- ref: opts.since ?? "HEAD~5",
74851
+ ref: opts.since ?? ref2 ?? "HEAD~5",
74315
74852
  ...opts.symbol === true ? { symbolMode: true } : {},
74316
74853
  ...opts.json === true ? { json: true } : {}
74317
74854
  })
74318
74855
  )
74319
74856
  );
74320
- program2.command("diff <spec> [ref]").description('show only the git diff hunk(s) that fall within one symbol\'s line range, e.g. `token-goat diff "file.ts::myFn" HEAD~3..HEAD`').option("-j, --json", "output as JSON").action(
74857
+ program2.command("diff <spec> [ref]").description('show only the git diff hunk(s) that fall within one symbol\'s line range, e.g. `token-goat diff "file.ts::myFn" HEAD~3..HEAD` (also accepts the file::symbol@LINE anchor form documented under `read`)').option("-j, --json", "output as JSON").action(
74321
74858
  (spec, ref2, opts) => runExit(
74322
74859
  () => runDiff({
74323
74860
  spec,
@@ -74326,7 +74863,7 @@ function buildProgram() {
74326
74863
  })
74327
74864
  )
74328
74865
  );
74329
- program2.command("log <spec> [ref]").description('show git commit history scoped to one symbol\'s line range, e.g. `token-goat log "file.ts::myFn" HEAD~10`').option("--max-count <n>", "maximum number of commits to show (default 20)").option("-j, --json", "output as JSON").action(
74866
+ program2.command("log <spec> [ref]").description('show git commit history scoped to one symbol\'s line range, e.g. `token-goat log "file.ts::myFn" HEAD~10` (also accepts the file::symbol@LINE anchor form documented under `read`)').option("--max-count <n>", "maximum number of commits to show (default 20)").option("-j, --json", "output as JSON").action(
74330
74867
  (spec, ref2, opts) => runExit(
74331
74868
  () => runLog({
74332
74869
  spec,
@@ -82989,7 +83526,7 @@ init_image_shrink();
82989
83526
  var DEFAULT_STDIN_TIMEOUT_MS = 5e3;
82990
83527
  var MAX_STDIN_BYTES = 64 * 1024 * 1024;
82991
83528
  function readStdinJson(timeoutMs = DEFAULT_STDIN_TIMEOUT_MS, maxBytes = MAX_STDIN_BYTES) {
82992
- return new Promise((resolve24, reject) => {
83529
+ return new Promise((resolve25, reject) => {
82993
83530
  const chunks = [];
82994
83531
  let totalBytes = 0;
82995
83532
  let settled = false;
@@ -83024,7 +83561,7 @@ function readStdinJson(timeoutMs = DEFAULT_STDIN_TIMEOUT_MS, maxBytes = MAX_STDI
83024
83561
  return;
83025
83562
  }
83026
83563
  try {
83027
- resolve24(JSON.parse(text));
83564
+ resolve25(JSON.parse(text));
83028
83565
  } catch (err2) {
83029
83566
  reject(err2 instanceof Error ? err2 : new Error(String(err2)));
83030
83567
  }