token-goat 2.6.26 → 2.6.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3050,7 +3050,7 @@ var require_commander = __commonJS({
3050
3050
  import { createRequire } from "node:module";
3051
3051
  function resolveVersion() {
3052
3052
  if (true) {
3053
- return "2.6.26";
3053
+ return "2.6.27";
3054
3054
  }
3055
3055
  const require2 = createRequire(import.meta.url);
3056
3056
  const pkg = require2("../package.json");
@@ -4720,6 +4720,12 @@ function grepFilteredToEmptyNotice(preFilterCount, grep, nounSingular, nounPlura
4720
4720
  const verb = preFilterCount === 1 ? "was" : "were";
4721
4721
  return ` (all ${preFilterCount} ${noun} ${verb} filtered out by --grep ${grep} -- widen or drop the filter to see them)`;
4722
4722
  }
4723
+ function countNoun(count, singular, plural2 = `${singular}s`) {
4724
+ return `${count} ${count === 1 ? singular : plural2}`;
4725
+ }
4726
+ function excludeTestsHiddenNote(count) {
4727
+ return `${count} in test ${count === 1 ? "file" : "files"} hidden by --exclude-tests`;
4728
+ }
4723
4729
  function escapeRegExp(s) {
4724
4730
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4725
4731
  }
@@ -20677,6 +20683,33 @@ function dirtyQueuePathFor(dir) {
20677
20683
  function drainHeartbeatPathFor(dir) {
20678
20684
  return path17.join(dir, "queue", "drain-heartbeat");
20679
20685
  }
20686
+ function writeDrainHeartbeat(dir, force = false) {
20687
+ const now = Date.now();
20688
+ if (!force && now - (heartbeatWriteTimes.get(dir) ?? 0) < WORKER_HEARTBEAT_REFRESH_MS) return;
20689
+ try {
20690
+ fs12.mkdirSync(path17.dirname(drainHeartbeatPathFor(dir)), { recursive: true });
20691
+ fs12.writeFileSync(drainHeartbeatPathFor(dir), `${process.pid}
20692
+ `);
20693
+ heartbeatWriteTimes.set(dir, now);
20694
+ } catch {
20695
+ }
20696
+ }
20697
+ function hasFreshWorkerHeartbeat(dir, pid) {
20698
+ try {
20699
+ const heartbeatPath = drainHeartbeatPathFor(dir);
20700
+ if (Date.now() - fs12.statSync(heartbeatPath).mtimeMs > WORKER_HEARTBEAT_STALE_MS) return false;
20701
+ return fs12.readFileSync(heartbeatPath, "utf8").trim() === String(pid);
20702
+ } catch {
20703
+ return false;
20704
+ }
20705
+ }
20706
+ function pidFileIsWithinStartupGrace(dir) {
20707
+ try {
20708
+ return Date.now() - fs12.statSync(workerPidPath(dir)).mtimeMs < WORKER_STARTUP_GRACE_MS;
20709
+ } catch {
20710
+ return false;
20711
+ }
20712
+ }
20680
20713
  function parseDirtyQueueLines(raw) {
20681
20714
  const seen = /* @__PURE__ */ new Set();
20682
20715
  const out2 = [];
@@ -20864,6 +20897,7 @@ function processDirtyBatch(paths, index = makeIndexer(globalDbPath()), remove =
20864
20897
  let indexed = 0;
20865
20898
  for (const p of paths) {
20866
20899
  if (!p) continue;
20900
+ writeDrainHeartbeat(dir);
20867
20901
  if (isUnderBlockedRoot(p, blockedRoots)) continue;
20868
20902
  if (!fs12.existsSync(p)) {
20869
20903
  remove(p);
@@ -20991,11 +21025,7 @@ function drainOnce(dir, index, remove) {
20991
21025
  }
20992
21026
  }
20993
21027
  for (const p of deferredRequeues) appendToDirtyQueue(dir, p);
20994
- try {
20995
- fs12.mkdirSync(path17.dirname(drainHeartbeatPathFor(dir)), { recursive: true });
20996
- fs12.writeFileSync(drainHeartbeatPathFor(dir), "");
20997
- } catch {
20998
- }
21028
+ writeDrainHeartbeat(dir, true);
20999
21029
  return processed;
21000
21030
  }
21001
21031
  function pidAlive(pid) {
@@ -21018,7 +21048,7 @@ function readPidFile(dir) {
21018
21048
  function isWorkerRunning(dir = dataDir()) {
21019
21049
  const pid = readPidFile(dir);
21020
21050
  if (pid === null) return false;
21021
- return pidAlive(pid);
21051
+ return pidAlive(pid) && hasFreshWorkerHeartbeat(dir, pid);
21022
21052
  }
21023
21053
  function workerHealthCheckMarkerPath(dir) {
21024
21054
  return path17.join(dir, "worker-healthcheck.marker");
@@ -21054,8 +21084,8 @@ function ensureWorkerAlive(dir = dataDir()) {
21054
21084
  function stopWorker(dir = dataDir()) {
21055
21085
  const pid = readPidFile(dir);
21056
21086
  if (pid === null) return false;
21057
- const alive = pidAlive(pid);
21058
- if (alive) {
21087
+ const running = isWorkerRunning(dir);
21088
+ if (running) {
21059
21089
  try {
21060
21090
  process.kill(pid);
21061
21091
  } catch {
@@ -21067,7 +21097,7 @@ function stopWorker(dir = dataDir()) {
21067
21097
  } catch {
21068
21098
  }
21069
21099
  }
21070
- return alive;
21100
+ return running;
21071
21101
  }
21072
21102
  function claimWorkerPidFile(dir, pid) {
21073
21103
  const pidPath = workerPidPath(dir);
@@ -21079,7 +21109,7 @@ function claimWorkerPidFile(dir, pid) {
21079
21109
  if (e.code !== "EEXIST") throw e;
21080
21110
  }
21081
21111
  const existingPid = readPidFile(dir);
21082
- if (existingPid !== null && pidAlive(existingPid)) {
21112
+ if (existingPid !== null && pidAlive(existingPid) && (hasFreshWorkerHeartbeat(dir, existingPid) || pidFileIsWithinStartupGrace(dir))) {
21083
21113
  return false;
21084
21114
  }
21085
21115
  try {
@@ -21183,9 +21213,10 @@ function runDetachedWorkerDaemon() {
21183
21213
  }
21184
21214
  }
21185
21215
  });
21216
+ writeDrainHeartbeat(dir, true);
21186
21217
  void runWorkerLoop(dir, safeInterval);
21187
21218
  }
21188
- var DEFAULT_POLL_INTERVAL_MS, SNAPSHOT_CLEANUP_INTERVAL_MS, PRUNE_EVERY_N_DRAINS, drainCycleCounts, lastKnownProjectRoots, unclearedDrainingSnapshots, MAX_TRANSIENT_RETRIES, WORKER_ERROR_LOG_MAX_BYTES, CORRUPT_QUARANTINE_MAX_AGE_MS, INDEX_FAILED, inFlightEmbeddings, activeEmbedSlots, embedSlotWaiters, WORKER_HEALTHCHECK_MIN_INTERVAL_MS, WorkerAlreadyRunningError, KNOWN_ROOTS_SWEEP_INTERVAL_MS;
21219
+ var DEFAULT_POLL_INTERVAL_MS, WORKER_HEARTBEAT_STALE_MS, WORKER_HEARTBEAT_REFRESH_MS, WORKER_STARTUP_GRACE_MS, SNAPSHOT_CLEANUP_INTERVAL_MS, PRUNE_EVERY_N_DRAINS, drainCycleCounts, heartbeatWriteTimes, lastKnownProjectRoots, unclearedDrainingSnapshots, MAX_TRANSIENT_RETRIES, WORKER_ERROR_LOG_MAX_BYTES, CORRUPT_QUARANTINE_MAX_AGE_MS, INDEX_FAILED, inFlightEmbeddings, activeEmbedSlots, embedSlotWaiters, WORKER_HEALTHCHECK_MIN_INTERVAL_MS, WorkerAlreadyRunningError, KNOWN_ROOTS_SWEEP_INTERVAL_MS;
21189
21220
  var init_worker = __esm({
21190
21221
  "src/worker.ts"() {
21191
21222
  "use strict";
@@ -21205,9 +21236,13 @@ var init_worker = __esm({
21205
21236
  init_project();
21206
21237
  init_reset();
21207
21238
  DEFAULT_POLL_INTERVAL_MS = 2e3;
21239
+ WORKER_HEARTBEAT_STALE_MS = 6e4;
21240
+ WORKER_HEARTBEAT_REFRESH_MS = 5e3;
21241
+ WORKER_STARTUP_GRACE_MS = 1e4;
21208
21242
  SNAPSHOT_CLEANUP_INTERVAL_MS = 60 * 60 * 1e3;
21209
21243
  PRUNE_EVERY_N_DRAINS = 30;
21210
21244
  drainCycleCounts = /* @__PURE__ */ new Map();
21245
+ heartbeatWriteTimes = /* @__PURE__ */ new Map();
21211
21246
  lastKnownProjectRoots = /* @__PURE__ */ new Map();
21212
21247
  unclearedDrainingSnapshots = /* @__PURE__ */ new Map();
21213
21248
  MAX_TRANSIENT_RETRIES = 5;
@@ -23722,6 +23757,12 @@ function didYouMean(candidates) {
23722
23757
  }
23723
23758
  return lines2.join("\n");
23724
23759
  }
23760
+ function unknownSymbolSuggestion(name2, rootDir) {
23761
+ const rawSymbols = querySymbols({ limit: FIND_SCAN_LIMIT, rootDir });
23762
+ const candidates = rankSimilarNames(rawSymbols.map((s) => s.name), name2);
23763
+ return candidates.length > 0 ? `
23764
+ ${didYouMean(candidates)}` : "";
23765
+ }
23725
23766
  function formatBareNameSpecError(command, name2, projectRoot) {
23726
23767
  const rootDir = projectRoot ?? resolveProjectRoot({ project: process.cwd() });
23727
23768
  const matches2 = querySymbols({ name: name2, limit: 50, rootDir });
@@ -23795,6 +23836,17 @@ function runSymbol(opts) {
23795
23836
  if (opts.limit !== void 0 && opts.limit <= 0) {
23796
23837
  return { text: `--limit must be a positive number, got: ${opts.limit}`, code: 1 };
23797
23838
  }
23839
+ if (opts.name !== void 0 && opts.grep !== void 0) {
23840
+ return {
23841
+ text: "symbol: --grep cannot be combined with a name; drop the name to search by pattern, or drop --grep to search by exact name",
23842
+ code: 1
23843
+ };
23844
+ }
23845
+ if (opts.name === void 0 && opts.grep === void 0) {
23846
+ return { text: "symbol requires a name or --grep <pattern>", code: 1 };
23847
+ }
23848
+ const matchesGrep = opts.grep !== void 0 ? compileGrepMatcher(opts.grep) : void 0;
23849
+ const excludeTests = opts.excludeTests === true;
23798
23850
  const queryOpts = {};
23799
23851
  if (opts.name !== void 0) queryOpts.name = opts.name;
23800
23852
  if (opts.file !== void 0) {
@@ -23802,11 +23854,36 @@ function runSymbol(opts) {
23802
23854
  healStaleIndex(queryOpts.filePath);
23803
23855
  }
23804
23856
  if (opts.kind !== void 0) queryOpts.kind = opts.kind;
23805
- if (opts.limit !== void 0) queryOpts.limit = opts.limit;
23857
+ if (matchesGrep !== void 0 || excludeTests) {
23858
+ queryOpts.limit = FIND_SCAN_LIMIT;
23859
+ } else if (opts.limit !== void 0) {
23860
+ queryOpts.limit = opts.limit;
23861
+ }
23806
23862
  if (opts.file === void 0 && opts.projectRoot !== void 0) queryOpts.rootDir = opts.projectRoot;
23807
- const results = querySymbols(queryOpts);
23863
+ const rawResults = querySymbols(queryOpts);
23864
+ const preFilterCount = rawResults.length;
23865
+ const effectiveLimit = opts.limit ?? 100;
23866
+ const anyClientFilter = matchesGrep !== void 0 || excludeTests;
23867
+ const filtered = anyClientFilter ? rawResults.filter((s) => (matchesGrep === void 0 || matchesGrep(s.name)) && !(excludeTests && isTestFile(s.filePath))) : rawResults;
23868
+ const results = anyClientFilter ? filtered.slice(0, effectiveLimit) : filtered;
23869
+ const hiddenByExcludeTests = excludeTests ? rawResults.filter((s) => (matchesGrep === void 0 || matchesGrep(s.name)) && isTestFile(s.filePath)).length : 0;
23870
+ if (excludeTests && filtered.length === 0 && hiddenByExcludeTests > 0) {
23871
+ const label = opts.name ?? opts.grep ?? "*";
23872
+ const notice = `no non-test matches for '${label}' (${excludeTestsHiddenNote(hiddenByExcludeTests)})`;
23873
+ if (opts.json === true) {
23874
+ return { text: JSON.stringify({ items: [], truncated: false, totalCount: 0 }, null, 2), code: 0 };
23875
+ }
23876
+ return { text: `token-goat: ${notice}`, code: 0 };
23877
+ }
23878
+ if (matchesGrep !== void 0 && filtered.length === 0 && preFilterCount > 0) {
23879
+ if (opts.json === true) {
23880
+ const text2 = JSON.stringify({ items: [], truncated: false, totalCount: 0 }, null, 2);
23881
+ return { text: text2, code: 0 };
23882
+ }
23883
+ return { text: grepFilteredToEmptyNotice(preFilterCount, opts.grep ?? "", "symbol", "symbols"), code: 0 };
23884
+ }
23808
23885
  if (results.length === 0) {
23809
- let text2 = `No matches for '${opts.name ?? "*"}'`;
23886
+ let text2 = `No matches for '${opts.name ?? opts.grep ?? "*"}'`;
23810
23887
  const emptyIndexRoot = opts.json !== true ? opts.projectRoot ?? resolveProjectRoot({ project: process.cwd() }) : null;
23811
23888
  const indexEmpty = emptyIndexRoot !== null && isIndexEmptyForProject(globalDbPath(), emptyIndexRoot);
23812
23889
  if (opts.name !== void 0 && emptyIndexRoot !== null) {
@@ -23827,11 +23904,19 @@ ${emptyIndexMessage(emptyIndexRoot)}`;
23827
23904
  const symbolDisplayRoot = getDisplayRoot(opts.projectRoot);
23828
23905
  if (opts.json === true) {
23829
23906
  const capped = guardJsonRows(results);
23830
- const trueTotal = countSymbols(queryOpts);
23907
+ let trueTotal;
23908
+ let truncatedFlag;
23909
+ if (anyClientFilter) {
23910
+ trueTotal = filtered.length;
23911
+ truncatedFlag = capped.truncated || results.length < filtered.length;
23912
+ } else {
23913
+ trueTotal = countSymbols(queryOpts);
23914
+ truncatedFlag = capped.truncated || trueTotal > results.length;
23915
+ }
23831
23916
  const items = capped.items.map((s) => ({ ...s, filePath: toDisplayPath(symbolDisplayRoot, s.filePath) }));
23832
- const payload = { items, truncated: capped.truncated || trueTotal > results.length, totalCount: trueTotal };
23917
+ const payload = { items, truncated: truncatedFlag, totalCount: trueTotal };
23833
23918
  const text2 = JSON.stringify(payload, null, 2);
23834
- recordReadStat("symbol_lookup", fullSourceBytes, text2, opts.name ?? opts.file);
23919
+ recordReadStat("symbol_lookup", fullSourceBytes, text2, opts.name ?? opts.file ?? opts.grep);
23835
23920
  return { text: text2, code: 0 };
23836
23921
  }
23837
23922
  const blocks = results.map((sym) => {
@@ -23843,7 +23928,7 @@ ${preview}` : header;
23843
23928
  });
23844
23929
  const warning = opts.file !== void 0 ? staleWarning(resolveIndexPath(opts.file, opts.projectRoot ?? process.cwd())) : "";
23845
23930
  const text = guardText(warning + blocks.join("\n\n"), "symbol");
23846
- recordReadStat("symbol_lookup", fullSourceBytes, text, opts.name ?? opts.file);
23931
+ recordReadStat("symbol_lookup", fullSourceBytes, text, opts.name ?? opts.file ?? opts.grep);
23847
23932
  return { text, code: 0 };
23848
23933
  }
23849
23934
  function parseReadSpec(spec) {
@@ -23912,7 +23997,7 @@ function runLineRange(range2, opts) {
23912
23997
  const allLines = text.split(/\r?\n/);
23913
23998
  if (allLines.length > 1 && allLines[allLines.length - 1] === "") allLines.pop();
23914
23999
  if (start > allLines.length) {
23915
- return { text: `Line ${start} is past end of file (${allLines.length} lines): ${file2}`, code: 1 };
24000
+ return { text: `Line ${start} is past end of file (${countNoun(allLines.length, "line")}): ${file2}`, code: 1 };
23916
24001
  }
23917
24002
  const clampedEnd = Math.min(end, allLines.length);
23918
24003
  const slice = allLines.slice(start - 1, clampedEnd);
@@ -23948,13 +24033,13 @@ function formatAmbiguity(symbol3, file2, candidates, explicitRoot, commandName =
23948
24033
  const multiFile = new Set(candidates.map((c) => c.filePath)).size > 1;
23949
24034
  const displayRoot = getDisplayRoot(explicitRoot);
23950
24035
  const lines2 = [
23951
- `Ambiguous symbol '${symbol3}' in '${file2}': ${candidates.length} definitions match. Retry with one of the qualified commands below to pick one:`
24036
+ `Ambiguous symbol '${symbol3}' in '${file2}': ${countNoun(candidates.length, "definition")} match. Retry with one of the qualified commands below to pick one:`
23952
24037
  ];
23953
24038
  const fileSymCache = /* @__PURE__ */ new Map();
23954
24039
  const getFileSyms = (filePath) => {
23955
24040
  let fileSyms = fileSymCache.get(filePath);
23956
24041
  if (fileSyms === void 0) {
23957
- fileSyms = querySymbols({ filePath, limit: 1e3 });
24042
+ fileSyms = querySymbols({ filePath, limit: FIND_SCAN_LIMIT });
23958
24043
  fileSymCache.set(filePath, fileSyms);
23959
24044
  }
23960
24045
  return fileSyms;
@@ -24340,17 +24425,17 @@ function runRefs(opts) {
24340
24425
  lines2.push(`${sym}: ${grepFilteredToEmptyNotice(preGrepCount, opts.grep ?? "", "reference", "references").trim()}`);
24341
24426
  continue;
24342
24427
  }
24343
- 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)`);
24428
+ lines2.push(opts.excludeTests === true && suppressed > 0 ? `${sym}: (no non-test references found; ${excludeTestsHiddenNote(suppressed)})` : `${sym}: (no references found)`);
24344
24429
  continue;
24345
24430
  }
24346
24431
  lines2.push(`${sym}:`);
24347
24432
  if (opts.top !== void 0) {
24348
24433
  lines2.push(...renderTopFilesSummary(results, opts.top, suppressed));
24349
24434
  } else if (opts.callers === true) {
24350
- if (opts.excludeTests === true && suppressed > 0) lines2.push(` ${results.length} references (${suppressed} in test files hidden by --exclude-tests)`);
24435
+ if (opts.excludeTests === true && suppressed > 0) lines2.push(` ${countNoun(results.length, "reference")} (${excludeTestsHiddenNote(suppressed)})`);
24351
24436
  lines2.push(...renderCallerGroups(results, opts.context ?? 0));
24352
24437
  } else {
24353
- if (opts.excludeTests === true && suppressed > 0) lines2.push(` ${results.length} references (${suppressed} in test files hidden by --exclude-tests)`);
24438
+ if (opts.excludeTests === true && suppressed > 0) lines2.push(` ${countNoun(results.length, "reference")} (${excludeTestsHiddenNote(suppressed)})`);
24354
24439
  for (const ref2 of results) lines2.push(...renderRefLines(ref2, opts.context ?? 0));
24355
24440
  }
24356
24441
  }
@@ -24411,17 +24496,17 @@ function runRefsCrossFile(pairs, opts) {
24411
24496
  lines2.push(`${key}: ${grepFilteredToEmptyNotice(preGrepCount, opts.grep ?? "", "reference", "references").trim()}`);
24412
24497
  continue;
24413
24498
  }
24414
- 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)`);
24499
+ lines2.push(opts.excludeTests === true && suppressed > 0 ? `${key}: (no non-test references found; ${excludeTestsHiddenNote(suppressed)})` : `${key}: (no references found)`);
24415
24500
  continue;
24416
24501
  }
24417
24502
  lines2.push(`${key}:`);
24418
24503
  if (opts.top !== void 0) {
24419
24504
  lines2.push(...renderTopFilesSummary(results, opts.top, suppressed));
24420
24505
  } else if (opts.callers === true) {
24421
- if (opts.excludeTests === true && suppressed > 0) lines2.push(` ${results.length} references (${suppressed} in test files hidden by --exclude-tests)`);
24506
+ if (opts.excludeTests === true && suppressed > 0) lines2.push(` ${countNoun(results.length, "reference")} (${excludeTestsHiddenNote(suppressed)})`);
24422
24507
  lines2.push(...renderCallerGroups(results, opts.context ?? 0));
24423
24508
  } else {
24424
- if (opts.excludeTests === true && suppressed > 0) lines2.push(` ${results.length} references (${suppressed} in test files hidden by --exclude-tests)`);
24509
+ if (opts.excludeTests === true && suppressed > 0) lines2.push(` ${countNoun(results.length, "reference")} (${excludeTestsHiddenNote(suppressed)})`);
24425
24510
  for (const ref2 of results) lines2.push(...renderRefLines(ref2, opts.context ?? 0));
24426
24511
  }
24427
24512
  }
@@ -24462,16 +24547,25 @@ function runRefsSingle(opts) {
24462
24547
  }
24463
24548
  if (results.length === 0) {
24464
24549
  if (matchesGrep !== void 0 && preGrepCount > 0) {
24550
+ if (opts.json === true) {
24551
+ emit2(JSON.stringify({ items: [], truncated: false, totalCount: 0 }, null, 2));
24552
+ return 0;
24553
+ }
24465
24554
  emit2(grepFilteredToEmptyNotice(preGrepCount, opts.grep ?? "", "reference", "references"));
24466
24555
  return 0;
24467
24556
  }
24468
24557
  if (opts.excludeTests === true && suppressed > 0) {
24469
- emitErr(`No non-test references found for '${symName}' (${suppressed} in test files hidden by --exclude-tests)`);
24558
+ emitErr(`No non-test references found for '${symName}' (${excludeTestsHiddenNote(suppressed)})`);
24559
+ return 1;
24560
+ }
24561
+ const rootDir = resolveProjectRoot({ project: process.cwd() });
24562
+ if (querySymbols({ name: symName, rootDir, limit: 1 }).length === 0) {
24563
+ emitErr(`Symbol not found: ${symName}${unknownSymbolSuggestion(symName, rootDir)}`);
24564
+ if (opts.json !== true && isIndexEmptyForProject(globalDbPath(), rootDir)) emitErr(emptyIndexMessage(rootDir));
24470
24565
  return 1;
24471
24566
  }
24472
24567
  emitErr(`No references found for '${symName}'`);
24473
24568
  if (opts.json !== true) {
24474
- const rootDir = resolveProjectRoot({ project: process.cwd() });
24475
24569
  if (isIndexEmptyForProject(globalDbPath(), rootDir)) emitErr(emptyIndexMessage(rootDir));
24476
24570
  }
24477
24571
  return 1;
@@ -24491,7 +24585,7 @@ function runRefsSingle(opts) {
24491
24585
  recordReadStat("symbol_read", fullSourceBytes, text2, symName);
24492
24586
  return 0;
24493
24587
  }
24494
- const lines2 = opts.top !== void 0 ? renderTopFilesSummary(results, opts.top, suppressed) : opts.callers === true ? [...opts.excludeTests === true && suppressed > 0 ? [`${results.length} references (${suppressed} in test files hidden by --exclude-tests)`] : [], ...renderCallerGroups(results, opts.context ?? 0)] : [...opts.excludeTests === true && suppressed > 0 ? [`${results.length} references (${suppressed} in test files hidden by --exclude-tests)`] : [], ...results.flatMap((ref2) => renderRefLines(ref2, opts.context ?? 0, ""))];
24588
+ const lines2 = opts.top !== void 0 ? renderTopFilesSummary(results, opts.top, suppressed) : opts.callers === true ? [...opts.excludeTests === true && suppressed > 0 ? [`${countNoun(results.length, "reference")} (${excludeTestsHiddenNote(suppressed)})`] : [], ...renderCallerGroups(results, opts.context ?? 0)] : [...opts.excludeTests === true && suppressed > 0 ? [`${countNoun(results.length, "reference")} (${excludeTestsHiddenNote(suppressed)})`] : [], ...results.flatMap((ref2) => renderRefLines(ref2, opts.context ?? 0, ""))];
24495
24589
  const text = lines2.join("\n");
24496
24590
  emitGuarded(text, "symbol");
24497
24591
  recordReadStat("symbol_read", fullSourceBytes, text, symName);
@@ -24509,8 +24603,8 @@ function groupRefsByFile(refs) {
24509
24603
  function renderTopFilesSummary(refs, topN, suppressed) {
24510
24604
  const grouped = groupRefsByFile(refs);
24511
24605
  const shown = grouped.slice(0, topN);
24512
- const suppressedNote = suppressed !== void 0 && suppressed > 0 ? ` (${suppressed} in test files hidden by --exclude-tests)` : "";
24513
- const lines2 = [`${refs.length} references across ${grouped.length} files (showing top ${shown.length})${suppressedNote}`];
24606
+ const suppressedNote = suppressed !== void 0 && suppressed > 0 ? ` (${excludeTestsHiddenNote(suppressed)})` : "";
24607
+ const lines2 = [`${countNoun(refs.length, "reference")} across ${countNoun(grouped.length, "file")} (showing top ${shown.length})${suppressedNote}`];
24514
24608
  for (const { file: file2, count } of shown) lines2.push(` ${count} ${refsDisplayPath(file2)}`);
24515
24609
  const omittedFiles = grouped.length - shown.length;
24516
24610
  if (omittedFiles > 0) {
@@ -24659,7 +24753,7 @@ function runSkeleton(opts) {
24659
24753
  return { text: text2, code: 0 };
24660
24754
  }
24661
24755
  const totalLines = filtered.length > 0 ? Math.max(...filtered.map((s) => s.lineEnd)) : 0;
24662
- const lines2 = [`# Skeleton: ${opts.file} (${filtered.length} symbols, ${totalLines} lines)`];
24756
+ const lines2 = [`# Skeleton: ${opts.file} (${countNoun(filtered.length, "symbol")}, ${countNoun(totalLines, "line")})`];
24663
24757
  if (filtered.length === 0 && preFilterCount > 0) lines2.push(filteredToEmptyNotice(preFilterCount, opts.minLines, opts.grep));
24664
24758
  for (const sym of filtered) {
24665
24759
  const lineStr = sym.lineStart.toString().padStart(6);
@@ -24699,7 +24793,7 @@ function runOutline(opts) {
24699
24793
  recordReadStat("outline", fullSourceBytes, text2, opts.file);
24700
24794
  return { text: text2, code: 0 };
24701
24795
  }
24702
- const lines2 = [`# Outline: ${opts.file} (${filtered.length} symbols)`];
24796
+ const lines2 = [`# Outline: ${opts.file} (${countNoun(filtered.length, "symbol")})`];
24703
24797
  if (filtered.length === 0 && preFilterCount > 0) lines2.push(filteredToEmptyNotice(preFilterCount, opts.minLines, opts.grep));
24704
24798
  for (const sym of filtered) {
24705
24799
  const rangeStr = `${sym.lineStart.toString().padStart(4)}-${sym.lineEnd.toString().padEnd(6)}`;
@@ -25406,16 +25500,25 @@ function runFind(opts) {
25406
25500
  const symbols = rawSymbols.filter(
25407
25501
  (s) => s.name.toLowerCase().includes(patternLower)
25408
25502
  );
25409
- const files = [...new Set(symbols.map((s) => s.filePath))].slice(0, opts.limit ?? 50);
25503
+ let fuzzyNames = [];
25504
+ if (symbols.length === 0) {
25505
+ fuzzyNames = rankSimilarNames(rawSymbols.map((s) => s.name), opts.pattern);
25506
+ }
25507
+ const matched = fuzzyNames.length > 0 ? fuzzyNames.flatMap((n) => rawSymbols.filter((s) => s.name === n)) : symbols;
25508
+ const files = [...new Set(matched.map((s) => s.filePath))].slice(0, opts.limit ?? 50);
25410
25509
  const truncated = rawSymbols.length === FIND_SCAN_LIMIT;
25411
25510
  if (files.length === 0) {
25412
25511
  emitErr(`No indexed files match '${opts.pattern}'`);
25413
25512
  return 1;
25414
25513
  }
25415
25514
  if (opts.json === true) {
25416
- emit2(JSON.stringify({ files, truncated }, null, 2));
25515
+ const fuzzyPayload = fuzzyNames.length > 0 ? { fuzzy: true, matchedNames: fuzzyNames } : {};
25516
+ emit2(JSON.stringify({ files, truncated, ...fuzzyPayload }, null, 2));
25417
25517
  return 0;
25418
25518
  }
25519
+ if (fuzzyNames.length > 0) {
25520
+ emitErr(`No symbol name contains '${opts.pattern}'; showing files for the nearest indexed ${fuzzyNames.length === 1 ? "name" : "names"}: ${fuzzyNames.join(", ")}`);
25521
+ }
25419
25522
  for (const f of files) {
25420
25523
  emit2(toDisplayPath(rootDir, f));
25421
25524
  }
@@ -25519,7 +25622,12 @@ function runChanged(opts = {}) {
25519
25622
  emitErr(`Could not run git diff against '${ref2}'`);
25520
25623
  return 1;
25521
25624
  }
25625
+ const emptyEnvelope = () => {
25626
+ emit2(JSON.stringify({ items: [], truncated: false, totalCount: 0 }, null, 2));
25627
+ return 0;
25628
+ };
25522
25629
  if (changedFiles.length === 0) {
25630
+ if (opts.json === true) return emptyEnvelope();
25523
25631
  emit2("No files changed.");
25524
25632
  return 0;
25525
25633
  }
@@ -25527,9 +25635,29 @@ function runChanged(opts = {}) {
25527
25635
  const matchesGrep = opts.grep !== void 0 ? compileGrepMatcher(opts.grep) : void 0;
25528
25636
  if (matchesGrep !== void 0) changedFiles = changedFiles.filter((f) => matchesGrep(f));
25529
25637
  if (matchesGrep !== void 0 && changedFiles.length === 0) {
25638
+ if (opts.json === true) return emptyEnvelope();
25530
25639
  emit2(grepFilteredToEmptyNotice(preGrepFileCount, opts.grep ?? "", "changed file", "changed files"));
25531
25640
  return 0;
25532
25641
  }
25642
+ const postGrepFileCount = changedFiles.length;
25643
+ let hiddenTestFiles = 0;
25644
+ if (opts.excludeTests === true) {
25645
+ const kept = changedFiles.filter((f) => !isTestFile(f));
25646
+ hiddenTestFiles = changedFiles.length - kept.length;
25647
+ changedFiles = kept;
25648
+ }
25649
+ if (changedFiles.length === 0 && hiddenTestFiles > 0) {
25650
+ if (opts.json === true) return emptyEnvelope();
25651
+ const grepRemoved = preGrepFileCount - postGrepFileCount;
25652
+ if (matchesGrep !== void 0 && grepRemoved > 0) {
25653
+ emit2(
25654
+ `No non-test files matched --grep ${opts.grep ?? ""} (${excludeTestsHiddenNote(hiddenTestFiles)}; ${countNoun(grepRemoved, "other changed file")} did not match the filter)`
25655
+ );
25656
+ return 0;
25657
+ }
25658
+ emit2(`No non-test files changed (${excludeTestsHiddenNote(hiddenTestFiles)})`);
25659
+ return 0;
25660
+ }
25533
25661
  if (opts.symbolMode === true) {
25534
25662
  let hunksByFile = /* @__PURE__ */ new Map();
25535
25663
  try {
@@ -25541,12 +25669,13 @@ function runChanged(opts = {}) {
25541
25669
  }
25542
25670
  const allSymbols = [];
25543
25671
  for (const f of changedFiles) {
25544
- const fileSymbols = querySymbols({ filePath: resolveIndexPath(f, projectRoot), limit: 1e3 });
25672
+ const fileSymbols = querySymbols({ filePath: resolveIndexPath(f, projectRoot), limit: FIND_SCAN_LIMIT });
25545
25673
  const hunks = hunksByFile.get(f);
25546
25674
  const scoped = hunks === void 0 ? fileSymbols : fileSymbols.filter((s) => hunks.some((h) => h.start <= s.lineEnd && h.end >= s.lineStart));
25547
25675
  allSymbols.push(...scoped);
25548
25676
  }
25549
25677
  if (allSymbols.length === 0) {
25678
+ if (opts.json === true) return emptyEnvelope();
25550
25679
  emit2("No symbols changed.");
25551
25680
  return 0;
25552
25681
  }
@@ -26525,7 +26654,17 @@ async function runSemantic(query, opts) {
26525
26654
  void 0,
26526
26655
  rootDir
26527
26656
  );
26528
- const hits = mergeNearbyHits(rawHits).slice(0, n);
26657
+ const matchesGrep = opts.grep !== void 0 ? compileGrepMatcher(opts.grep) : void 0;
26658
+ const mergedHits = mergeNearbyHits(rawHits);
26659
+ const preGrepHitCount = mergedHits.length;
26660
+ const keepHit = (h) => {
26661
+ if (opts.excludeTests === true && isTestFile(h.filePath)) return false;
26662
+ return matchesGrep === void 0 || matchesGrep(toDisplayPath(rootDir, h.filePath));
26663
+ };
26664
+ const anyFilter = matchesGrep !== void 0 || opts.excludeTests === true;
26665
+ const filteredHits = anyFilter ? mergedHits.filter(keepHit) : mergedHits;
26666
+ const suppressedHits = opts.excludeTests === true ? mergedHits.filter((h) => (matchesGrep === void 0 || matchesGrep(toDisplayPath(rootDir, h.filePath))) && isTestFile(h.filePath)).length : 0;
26667
+ const hits = filteredHits.slice(0, n);
26529
26668
  if (hits.length > 0) {
26530
26669
  const enclosing = hits.map((h) => resolveEnclosingSymbol(h.filePath, h.startLine));
26531
26670
  if (opts.json === true) {
@@ -26553,8 +26692,41 @@ ${previewLines(h.text, 3)}`;
26553
26692
  recordReadStat("semantic_search", sumFileSizes(hits.map((h) => h.filePath)), text2, query);
26554
26693
  return { text: text2, code: 0 };
26555
26694
  }
26556
- const results = searchSymbolsFts(query, n, void 0, rootDir);
26695
+ let results;
26696
+ let preGrepFtsCount = 0;
26697
+ let suppressedFts = 0;
26698
+ if (anyFilter) {
26699
+ const overFetchFts = Math.min(MAX_OVER_FETCH, n * OVER_FETCH_FACTOR);
26700
+ const rawResults = searchSymbolsFts(query, overFetchFts, void 0, rootDir);
26701
+ preGrepFtsCount = rawResults.length;
26702
+ if (opts.excludeTests === true) {
26703
+ suppressedFts = rawResults.filter((s) => (matchesGrep === void 0 || matchesGrep(toDisplayPath(rootDir, s.filePath))) && isTestFile(s.filePath)).length;
26704
+ }
26705
+ results = rawResults.filter(keepHit).slice(0, n);
26706
+ } else {
26707
+ results = searchSymbolsFts(query, n, void 0, rootDir);
26708
+ }
26557
26709
  if (results.length === 0) {
26710
+ const preGrepTotal = preGrepHitCount + preGrepFtsCount;
26711
+ if (matchesGrep !== void 0 && preGrepTotal > 0) {
26712
+ const notice = grepFilteredToEmptyNotice(preGrepTotal, opts.grep ?? "", "match", "matches");
26713
+ if (opts.json === true) {
26714
+ const payload = { source: "fts", items: [], truncated: false, totalCount: 0, grepFilteredToEmpty: true, hint: notice.trim() };
26715
+ return { text: JSON.stringify(payload, null, 2), code: 0 };
26716
+ }
26717
+ return { text: `token-goat: ${notice.trim()}`, code: 0 };
26718
+ }
26719
+ if (opts.excludeTests === true) {
26720
+ const suppressedTotal = suppressedHits + suppressedFts;
26721
+ if (suppressedTotal > 0) {
26722
+ const notice = `no non-test matches for '${query}' (${excludeTestsHiddenNote(suppressedTotal)})`;
26723
+ if (opts.json === true) {
26724
+ const payload = { source: "fts", items: [], truncated: false, totalCount: 0, excludeTestsFilteredToEmpty: true, hint: notice };
26725
+ return { text: JSON.stringify(payload, null, 2), code: 0 };
26726
+ }
26727
+ return { text: `token-goat: ${notice}`, code: 0 };
26728
+ }
26729
+ }
26558
26730
  const indexEmpty = isIndexEmptyForProject(globalDbPath(), rootDir);
26559
26731
  if (opts.json === true) {
26560
26732
  const payload = indexEmpty ? { source: "fts", items: [], truncated: false, totalCount: 0, indexEmpty: true, hint: emptyIndexMessage(rootDir) } : { source: "fts", items: [], truncated: false, totalCount: 0 };
@@ -26814,7 +26986,7 @@ function fileDefinesName(fp, name2, getSyms) {
26814
26986
  return getSyms(fp).some((s) => s.name === name2);
26815
26987
  }
26816
26988
  function filterRefsForSymbol(refs, name2, filePath, getSyms) {
26817
- return refs.filter((ref2) => ref2.filePath === filePath || !fileDefinesName(ref2.filePath, name2, getSyms));
26989
+ return refs.filter((ref2) => foldPath(ref2.filePath) === foldPath(filePath) || !fileDefinesName(ref2.filePath, name2, getSyms));
26818
26990
  }
26819
26991
  function resolveCallers(name2, limit, filePath, rootDir, excludeTests) {
26820
26992
  const resolvedRootDir = rootDir ?? resolveProjectRoot({ project: process.cwd() });
@@ -26862,7 +27034,12 @@ function runCallers(opts) {
26862
27034
  return 0;
26863
27035
  }
26864
27036
  if (opts.excludeTests === true && suppressed > 0) {
26865
- emitErr2(`No non-test references found for '${opts.symbol}' (${suppressed} in test files hidden by --exclude-tests)`);
27037
+ emitErr2(`No non-test references found for '${opts.symbol}' (${excludeTestsHiddenNote(suppressed)})`);
27038
+ return 1;
27039
+ }
27040
+ if (querySymbols({ name: name2, rootDir, limit: 1 }).length === 0) {
27041
+ emitErr2(`Symbol not found: ${opts.symbol}${unknownSymbolSuggestion(name2, rootDir)}`);
27042
+ if (opts.json !== true && isIndexEmptyForProject(globalDbPath(), rootDir)) emitErr2(emptyIndexMessage(rootDir));
26866
27043
  return 1;
26867
27044
  }
26868
27045
  emitErr2(`No references found for '${opts.symbol}'`);
@@ -26882,7 +27059,7 @@ function runCallers(opts) {
26882
27059
  return 0;
26883
27060
  }
26884
27061
  if (opts.excludeTests === true && suppressed > 0) {
26885
- emit3(`${entries.length} callers found (${suppressed} in test files hidden by --exclude-tests)`);
27062
+ emit3(`${countNoun(entries.length, "caller")} found (${excludeTestsHiddenNote(suppressed)})`);
26886
27063
  }
26887
27064
  for (const e of entries) {
26888
27065
  const displayPath = toDisplayPath(rootDir, e.file);
@@ -26908,17 +27085,22 @@ function runCallChain(opts) {
26908
27085
  return 1;
26909
27086
  }
26910
27087
  } else if (querySymbols({ name: name2, rootDir, limit: 1 }).length === 0) {
26911
- emitErr2(`Symbol not found: ${opts.symbol}`);
27088
+ emitErr2(`Symbol not found: ${opts.symbol}${unknownSymbolSuggestion(name2, rootDir)}`);
26912
27089
  if (opts.json !== true && isIndexEmptyForProject(globalDbPath(), rootDir)) emitErr2(emptyIndexMessage(rootDir));
26913
27090
  return 1;
26914
27091
  }
26915
27092
  const getSyms = buildFileSymCache();
27093
+ let suppressedCount = 0;
26916
27094
  const callersOf = (n) => {
26917
27095
  const refs = queryRefs({ name: n, limit: DEFAULT_REF_QUERY_LIMIT, rootDir });
26918
27096
  if (refs.length === 0) return [];
26919
27097
  const scoped = fileHint !== void 0 && n === name2 ? filterRefsForSymbol(refs, n, fileHint, getSyms) : refs;
26920
27098
  const names = /* @__PURE__ */ new Set();
26921
27099
  for (const ref2 of scoped) {
27100
+ if (opts.excludeTests === true && isTestFile(ref2.filePath)) {
27101
+ suppressedCount += 1;
27102
+ continue;
27103
+ }
26922
27104
  const enc = enclosingSymbol(getSyms(ref2.filePath), ref2.line);
26923
27105
  if (enc !== null) names.add(enc.name);
26924
27106
  }
@@ -26930,6 +27112,10 @@ function runCallChain(opts) {
26930
27112
  return 0;
26931
27113
  }
26932
27114
  if (chains.length === 1 && chains[0]?.length === 1 && chains[0][0] === name2) {
27115
+ if (opts.excludeTests === true && suppressedCount > 0) {
27116
+ emit3(`${name2} (no non-test callers; ${excludeTestsHiddenNote(suppressedCount)})`);
27117
+ return 0;
27118
+ }
26933
27119
  emit3(`${name2} (no callers)`);
26934
27120
  return 0;
26935
27121
  }
@@ -26959,6 +27145,7 @@ function runImpact(opts) {
26959
27145
  const getSyms = buildFileSymCache();
26960
27146
  const hops = /* @__PURE__ */ new Map([[rootName, 0]]);
26961
27147
  const queue = [[rootName, 0]];
27148
+ let suppressedCount = 0;
26962
27149
  while (queue.length > 0) {
26963
27150
  const item = queue.shift();
26964
27151
  if (item === void 0) break;
@@ -26967,6 +27154,10 @@ function runImpact(opts) {
26967
27154
  const refs = queryRefs({ name: name2, limit: DEFAULT_REF_QUERY_LIMIT, rootDir });
26968
27155
  const scoped = fileHint !== void 0 && name2 === rootName ? filterRefsForSymbol(refs, name2, fileHint, getSyms) : refs;
26969
27156
  for (const ref2 of scoped) {
27157
+ if (opts.excludeTests === true && isTestFile(ref2.filePath)) {
27158
+ suppressedCount += 1;
27159
+ continue;
27160
+ }
26970
27161
  const newHop = depth + 1;
26971
27162
  const enc = enclosingSymbol(getSyms(ref2.filePath), ref2.line);
26972
27163
  if (enc === null) {
@@ -26986,6 +27177,14 @@ function runImpact(opts) {
26986
27177
  hops.delete(rootName);
26987
27178
  const sorted = [...hops.entries()].sort(compareHopEntries).slice(0, top);
26988
27179
  if (sorted.length === 0) {
27180
+ if (opts.excludeTests === true && suppressedCount > 0) {
27181
+ emitErr2(`No non-test impact found for '${opts.symbol}' (${excludeTestsHiddenNote(suppressedCount)})`);
27182
+ return 1;
27183
+ }
27184
+ if (querySymbols({ name: rootName, rootDir, limit: 1 }).length === 0) {
27185
+ emitErr2(`Symbol not found: ${opts.symbol}${unknownSymbolSuggestion(rootName, rootDir)}`);
27186
+ return 1;
27187
+ }
26989
27188
  emitErr2(`No callers found for '${opts.symbol}'`);
26990
27189
  return 1;
26991
27190
  }
@@ -27082,7 +27281,7 @@ function runDead(opts) {
27082
27281
  return 0;
27083
27282
  }
27084
27283
  if (opts.excludeTests === true && suppressed > 0) {
27085
- emit3(`No dead symbols found (${suppressed} in test files hidden by --exclude-tests).`);
27284
+ emit3(`No dead symbols found (${excludeTestsHiddenNote(suppressed)}).`);
27086
27285
  } else {
27087
27286
  emit3("No dead symbols found.");
27088
27287
  }
@@ -27090,7 +27289,7 @@ function runDead(opts) {
27090
27289
  return 0;
27091
27290
  }
27092
27291
  if (opts.excludeTests === true && suppressed > 0) {
27093
- emit3(`${sliced.length} dead symbols (${suppressed} in test files hidden by --exclude-tests)`);
27292
+ emit3(`${countNoun(sliced.length, "dead symbol")} (${excludeTestsHiddenNote(suppressed)})`);
27094
27293
  }
27095
27294
  for (const r of sliced) {
27096
27295
  emit3(`${r.name} ${toDisplayPath(rootDir, r.file)}:${r.line}`);
@@ -31217,6 +31416,8 @@ function writeGuidance(filePath) {
31217
31416
  BEGIN,
31218
31417
  buildGuidanceBody("VS Code\u2019s supported MCP integration and its built-in file-read tools"),
31219
31418
  "",
31419
+ "**Compressed payloads:** a message containing a token-goat payload block (recognizable by a `recovery: token-goat retrieve <id>` line) is compressed text, not an answer. Call the MCP tool `retrieve_text` with that id to recover the original text, then answer the question the message asks using the recovered text. Never present the raw payload to the user as the response; if the `retrieve_text` tool is unavailable (the MCP server is not running, or the chat is not in Agent mode), say so plainly and ask the user to switch to Agent mode or run `token-goat install --vscode`.",
31420
+ "",
31220
31421
  "VS Code support: token-goat install --vscode configures a project-local stdio MCP server in .vscode/mcp.json under the servers root key. VS Code may call these MCP tools when selected; MCP does not intercept VS Code\u2019s built-in file reads.",
31221
31422
  END
31222
31423
  ].join("\n");
@@ -65994,7 +66195,6 @@ function checkSymbolCount(dbPath, rootDir) {
65994
66195
  }
65995
66196
  }
65996
66197
  var DIRTY_QUEUE_BACKLOG_WARN_THRESHOLD = 500;
65997
- var DRAIN_HEARTBEAT_STALE_MS = 6e4;
65998
66198
  function checkDirtyQueueHealth(dataDir2) {
65999
66199
  let pendingCount = 0;
66000
66200
  try {
@@ -66017,7 +66217,7 @@ function checkDirtyQueueHealth(dataDir2) {
66017
66217
  heartbeatAgeMs = Date.now() - fs40.statSync(drainHeartbeatPathFor(dataDir2)).mtimeMs;
66018
66218
  } catch {
66019
66219
  }
66020
- if (heartbeatAgeMs !== null && heartbeatAgeMs > DRAIN_HEARTBEAT_STALE_MS) {
66220
+ if (heartbeatAgeMs !== null && heartbeatAgeMs > WORKER_HEARTBEAT_STALE_MS) {
66021
66221
  return {
66022
66222
  name: "Dirty queue",
66023
66223
  status: "warn",
@@ -82829,7 +83029,12 @@ function requirePositiveInt(flag, raw) {
82829
83029
  }
82830
83030
  async function cmdSemantic(query, opts) {
82831
83031
  const limit = opts.limit !== void 0 ? requireNonNegativeInt("--limit", opts.limit) : 20;
82832
- const { text, code } = await runSemantic(query, { limit, ...opts.json === true ? { json: true } : {} });
83032
+ const { text, code } = await runSemantic(query, {
83033
+ limit,
83034
+ ...opts.json === true ? { json: true } : {},
83035
+ ...opts.grep !== void 0 ? { grep: opts.grep } : {},
83036
+ ...opts.excludeTests === true ? { excludeTests: true } : {}
83037
+ });
82833
83038
  (opts.json === true || code === 0 ? out : err)(text);
82834
83039
  process.exitCode = code;
82835
83040
  }
@@ -84764,7 +84969,7 @@ function buildProgram() {
84764
84969
  process.exitCode = 1;
84765
84970
  }
84766
84971
  };
84767
- 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) => {
84972
+ program2.command("symbol [name]").description("search for a symbol by name, or project-wide by --grep name pattern").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").option("--grep <pattern>", "only show symbols whose name matches this regex (literal substring if it is not valid regex); cannot be combined with <name>").option("--exclude-tests", "hide symbols defined in a test file (opt-in; default output is unchanged)").action((name2, opts) => {
84768
84973
  let projectRoot;
84769
84974
  if (opts.project === true) {
84770
84975
  projectRoot = resolveProjectRoot({ project: process.cwd() });
@@ -84773,12 +84978,14 @@ function buildProgram() {
84773
84978
  }
84774
84979
  return runExitText(
84775
84980
  () => runSymbol({
84776
- name: name2,
84981
+ ...name2 !== void 0 ? { name: name2 } : {},
84777
84982
  limit: opts.limit !== void 0 ? requireNonNegativeInt("--limit", opts.limit) : 20,
84778
84983
  ...opts.file !== void 0 ? { file: opts.file } : {},
84779
84984
  ...opts.kind !== void 0 ? { kind: opts.kind } : {},
84780
84985
  ...projectRoot !== void 0 ? { projectRoot } : {},
84781
- ...opts.json === true ? { json: true } : {}
84986
+ ...opts.json === true ? { json: true } : {},
84987
+ ...opts.grep !== void 0 ? { grep: opts.grep } : {},
84988
+ ...opts.excludeTests === true ? { excludeTests: true } : {}
84782
84989
  })
84783
84990
  );
84784
84991
  });
@@ -84811,7 +85018,7 @@ function buildProgram() {
84811
85018
  ).option("-j, --json", "output as JSON").option("--list", "list all section headings in the file instead of reading one").action(
84812
85019
  (spec, opts) => opts.list === true ? runExit(() => runListSections({ file: spec, ...opts.json === true ? { json: true } : {} })) : runExitText(() => runSection({ spec, ...opts.json === true ? { json: true } : {} }))
84813
85020
  );
84814
- 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));
85021
+ 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").option("--grep <pattern>", "filter to hits whose file path matches this regex (literal substring if it is not valid regex); matched against the path as rendered").option("--exclude-tests", "hide hits whose file is a test file (opt-in; default output is unchanged)").action(guard(cmdSemantic));
84815
85022
  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("--grep <pattern>", "only show symbols whose name matches this regex (literal substring if it is not valid regex)").option("--force-refresh", "reparse file from disk before querying (ignore stale index)").option("--stats", "add per-symbol reference count and doc-coverage flag").action(
84816
85023
  (file2, more, opts) => runExitText(
84817
85024
  () => noteExtraFileArgs(
@@ -84947,21 +85154,23 @@ function buildProgram() {
84947
85154
  })
84948
85155
  )
84949
85156
  );
84950
- 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(
85157
+ 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").option("--exclude-tests", "hide callers whose call site lives in a test file (opt-in; default output is unchanged)").action(
84951
85158
  (symbol3, opts) => runExit(
84952
85159
  () => runCallChain({
84953
85160
  symbol: symbol3,
84954
85161
  ...opts.depth !== void 0 ? { depth: requireInt("--depth", opts.depth) } : {},
84955
- ...opts.json === true ? { json: true } : {}
85162
+ ...opts.json === true ? { json: true } : {},
85163
+ ...opts.excludeTests === true ? { excludeTests: true } : {}
84956
85164
  })
84957
85165
  )
84958
85166
  );
84959
- 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(
85167
+ 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").option("--exclude-tests", "hide callers whose call site lives in a test file (opt-in; default output is unchanged)").action(
84960
85168
  (symbol3, opts) => runExit(
84961
85169
  () => runImpact({
84962
85170
  symbol: symbol3,
84963
85171
  ...opts.top !== void 0 ? { top: requireNonNegativeInt("--top", opts.top) } : {},
84964
- ...opts.json === true ? { json: true } : {}
85172
+ ...opts.json === true ? { json: true } : {},
85173
+ ...opts.excludeTests === true ? { excludeTests: true } : {}
84965
85174
  })
84966
85175
  )
84967
85176
  );
@@ -85118,13 +85327,14 @@ function buildProgram() {
85118
85327
  }))());
85119
85328
  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 } : {} }))());
85120
85329
  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))());
85121
- 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").option("--grep <pattern>", "filter to changed files whose path matches this regex (falls back to a literal substring match when the pattern does not compile); applies to file paths even in --symbol mode").action(
85330
+ 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").option("--grep <pattern>", "filter to changed files whose path matches this regex (falls back to a literal substring match when the pattern does not compile); applies to file paths even in --symbol mode").option("--exclude-tests", "hide changed files that live in a test file (opt-in; default output is unchanged); applies to file paths even in --symbol mode").action(
85122
85331
  (ref2, opts) => runExit(
85123
85332
  () => runChanged({
85124
85333
  ref: opts.since ?? ref2 ?? "HEAD~5",
85125
85334
  ...opts.symbol === true ? { symbolMode: true } : {},
85126
85335
  ...opts.json === true ? { json: true } : {},
85127
- ...opts.grep !== void 0 ? { grep: opts.grep } : {}
85336
+ ...opts.grep !== void 0 ? { grep: opts.grep } : {},
85337
+ ...opts.excludeTests === true ? { excludeTests: true } : {}
85128
85338
  })
85129
85339
  )
85130
85340
  );