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.
@@ -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.26";
52
+ return "2.6.27";
53
53
  }
54
54
  const require2 = createRequire(import.meta.url);
55
55
  const pkg = require2("../package.json");
@@ -1901,6 +1901,12 @@ function grepFilteredToEmptyNotice(preFilterCount, grep, nounSingular, nounPlura
1901
1901
  const verb = preFilterCount === 1 ? "was" : "were";
1902
1902
  return ` (all ${preFilterCount} ${noun} ${verb} filtered out by --grep ${grep} -- widen or drop the filter to see them)`;
1903
1903
  }
1904
+ function countNoun(count, singular, plural2 = `${singular}s`) {
1905
+ return `${count} ${count === 1 ? singular : plural2}`;
1906
+ }
1907
+ function excludeTestsHiddenNote(count) {
1908
+ return `${count} in test ${count === 1 ? "file" : "files"} hidden by --exclude-tests`;
1909
+ }
1904
1910
  function escapeRegExp(s) {
1905
1911
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1906
1912
  }
@@ -3406,6 +3412,8 @@ function writeGuidance(filePath) {
3406
3412
  BEGIN,
3407
3413
  buildGuidanceBody("VS Code\u2019s supported MCP integration and its built-in file-read tools"),
3408
3414
  "",
3415
+ "**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`.",
3416
+ "",
3409
3417
  "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.",
3410
3418
  END
3411
3419
  ].join("\n");
@@ -20834,6 +20842,22 @@ function dirtyQueuePathFor(dir) {
20834
20842
  function drainHeartbeatPathFor(dir) {
20835
20843
  return path30.join(dir, "queue", "drain-heartbeat");
20836
20844
  }
20845
+ function hasFreshWorkerHeartbeat(dir, pid) {
20846
+ try {
20847
+ const heartbeatPath = drainHeartbeatPathFor(dir);
20848
+ if (Date.now() - fs26.statSync(heartbeatPath).mtimeMs > WORKER_HEARTBEAT_STALE_MS) return false;
20849
+ return fs26.readFileSync(heartbeatPath, "utf8").trim() === String(pid);
20850
+ } catch {
20851
+ return false;
20852
+ }
20853
+ }
20854
+ function pidFileIsWithinStartupGrace(dir) {
20855
+ try {
20856
+ return Date.now() - fs26.statSync(workerPidPath(dir)).mtimeMs < WORKER_STARTUP_GRACE_MS;
20857
+ } catch {
20858
+ return false;
20859
+ }
20860
+ }
20837
20861
  function parseDirtyQueueLines(raw) {
20838
20862
  const seen = /* @__PURE__ */ new Set();
20839
20863
  const out2 = [];
@@ -20889,7 +20913,7 @@ function readPidFile(dir) {
20889
20913
  function isWorkerRunning(dir = dataDir()) {
20890
20914
  const pid = readPidFile(dir);
20891
20915
  if (pid === null) return false;
20892
- return pidAlive(pid);
20916
+ return pidAlive(pid) && hasFreshWorkerHeartbeat(dir, pid);
20893
20917
  }
20894
20918
  function workerHealthCheckMarkerPath(dir) {
20895
20919
  return path30.join(dir, "worker-healthcheck.marker");
@@ -20925,8 +20949,8 @@ function ensureWorkerAlive(dir = dataDir()) {
20925
20949
  function stopWorker(dir = dataDir()) {
20926
20950
  const pid = readPidFile(dir);
20927
20951
  if (pid === null) return false;
20928
- const alive = pidAlive(pid);
20929
- if (alive) {
20952
+ const running = isWorkerRunning(dir);
20953
+ if (running) {
20930
20954
  try {
20931
20955
  process.kill(pid);
20932
20956
  } catch {
@@ -20938,7 +20962,7 @@ function stopWorker(dir = dataDir()) {
20938
20962
  } catch {
20939
20963
  }
20940
20964
  }
20941
- return alive;
20965
+ return running;
20942
20966
  }
20943
20967
  function claimWorkerPidFile(dir, pid) {
20944
20968
  const pidPath = workerPidPath(dir);
@@ -20950,7 +20974,7 @@ function claimWorkerPidFile(dir, pid) {
20950
20974
  if (e.code !== "EEXIST") throw e;
20951
20975
  }
20952
20976
  const existingPid = readPidFile(dir);
20953
- if (existingPid !== null && pidAlive(existingPid)) {
20977
+ if (existingPid !== null && pidAlive(existingPid) && (hasFreshWorkerHeartbeat(dir, existingPid) || pidFileIsWithinStartupGrace(dir))) {
20954
20978
  return false;
20955
20979
  }
20956
20980
  try {
@@ -21003,7 +21027,7 @@ function startDetachedWorker(opts) {
21003
21027
  child.unref();
21004
21028
  return pid;
21005
21029
  }
21006
- var DEFAULT_POLL_INTERVAL_MS, SNAPSHOT_CLEANUP_INTERVAL_MS, WORKER_ERROR_LOG_MAX_BYTES, CORRUPT_QUARANTINE_MAX_AGE_MS, INDEX_FAILED, activeEmbedSlots, embedSlotWaiters, WORKER_HEALTHCHECK_MIN_INTERVAL_MS, WorkerAlreadyRunningError, KNOWN_ROOTS_SWEEP_INTERVAL_MS;
21030
+ var DEFAULT_POLL_INTERVAL_MS, WORKER_HEARTBEAT_STALE_MS, WORKER_STARTUP_GRACE_MS, SNAPSHOT_CLEANUP_INTERVAL_MS, WORKER_ERROR_LOG_MAX_BYTES, CORRUPT_QUARANTINE_MAX_AGE_MS, INDEX_FAILED, activeEmbedSlots, embedSlotWaiters, WORKER_HEALTHCHECK_MIN_INTERVAL_MS, WorkerAlreadyRunningError, KNOWN_ROOTS_SWEEP_INTERVAL_MS;
21007
21031
  var init_worker = __esm({
21008
21032
  "src/worker.ts"() {
21009
21033
  "use strict";
@@ -21023,6 +21047,8 @@ var init_worker = __esm({
21023
21047
  init_project();
21024
21048
  init_reset();
21025
21049
  DEFAULT_POLL_INTERVAL_MS = 2e3;
21050
+ WORKER_HEARTBEAT_STALE_MS = 6e4;
21051
+ WORKER_STARTUP_GRACE_MS = 1e4;
21026
21052
  SNAPSHOT_CLEANUP_INTERVAL_MS = 60 * 60 * 1e3;
21027
21053
  WORKER_ERROR_LOG_MAX_BYTES = 5 * 1024 * 1024;
21028
21054
  CORRUPT_QUARANTINE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
@@ -28434,6 +28460,12 @@ function didYouMean(candidates) {
28434
28460
  }
28435
28461
  return lines2.join("\n");
28436
28462
  }
28463
+ function unknownSymbolSuggestion(name2, rootDir) {
28464
+ const rawSymbols = querySymbols({ limit: FIND_SCAN_LIMIT, rootDir });
28465
+ const candidates = rankSimilarNames(rawSymbols.map((s) => s.name), name2);
28466
+ return candidates.length > 0 ? `
28467
+ ${didYouMean(candidates)}` : "";
28468
+ }
28437
28469
  function formatBareNameSpecError(command, name2, projectRoot) {
28438
28470
  const rootDir = projectRoot ?? resolveProjectRoot({ project: process.cwd() });
28439
28471
  const matches2 = querySymbols({ name: name2, limit: 50, rootDir });
@@ -28507,6 +28539,17 @@ function runSymbol(opts) {
28507
28539
  if (opts.limit !== void 0 && opts.limit <= 0) {
28508
28540
  return { text: `--limit must be a positive number, got: ${opts.limit}`, code: 1 };
28509
28541
  }
28542
+ if (opts.name !== void 0 && opts.grep !== void 0) {
28543
+ return {
28544
+ text: "symbol: --grep cannot be combined with a name; drop the name to search by pattern, or drop --grep to search by exact name",
28545
+ code: 1
28546
+ };
28547
+ }
28548
+ if (opts.name === void 0 && opts.grep === void 0) {
28549
+ return { text: "symbol requires a name or --grep <pattern>", code: 1 };
28550
+ }
28551
+ const matchesGrep = opts.grep !== void 0 ? compileGrepMatcher(opts.grep) : void 0;
28552
+ const excludeTests = opts.excludeTests === true;
28510
28553
  const queryOpts = {};
28511
28554
  if (opts.name !== void 0) queryOpts.name = opts.name;
28512
28555
  if (opts.file !== void 0) {
@@ -28514,11 +28557,36 @@ function runSymbol(opts) {
28514
28557
  healStaleIndex(queryOpts.filePath);
28515
28558
  }
28516
28559
  if (opts.kind !== void 0) queryOpts.kind = opts.kind;
28517
- if (opts.limit !== void 0) queryOpts.limit = opts.limit;
28560
+ if (matchesGrep !== void 0 || excludeTests) {
28561
+ queryOpts.limit = FIND_SCAN_LIMIT;
28562
+ } else if (opts.limit !== void 0) {
28563
+ queryOpts.limit = opts.limit;
28564
+ }
28518
28565
  if (opts.file === void 0 && opts.projectRoot !== void 0) queryOpts.rootDir = opts.projectRoot;
28519
- const results = querySymbols(queryOpts);
28566
+ const rawResults = querySymbols(queryOpts);
28567
+ const preFilterCount = rawResults.length;
28568
+ const effectiveLimit = opts.limit ?? 100;
28569
+ const anyClientFilter = matchesGrep !== void 0 || excludeTests;
28570
+ const filtered = anyClientFilter ? rawResults.filter((s) => (matchesGrep === void 0 || matchesGrep(s.name)) && !(excludeTests && isTestFile(s.filePath))) : rawResults;
28571
+ const results = anyClientFilter ? filtered.slice(0, effectiveLimit) : filtered;
28572
+ const hiddenByExcludeTests = excludeTests ? rawResults.filter((s) => (matchesGrep === void 0 || matchesGrep(s.name)) && isTestFile(s.filePath)).length : 0;
28573
+ if (excludeTests && filtered.length === 0 && hiddenByExcludeTests > 0) {
28574
+ const label = opts.name ?? opts.grep ?? "*";
28575
+ const notice = `no non-test matches for '${label}' (${excludeTestsHiddenNote(hiddenByExcludeTests)})`;
28576
+ if (opts.json === true) {
28577
+ return { text: JSON.stringify({ items: [], truncated: false, totalCount: 0 }, null, 2), code: 0 };
28578
+ }
28579
+ return { text: `token-goat: ${notice}`, code: 0 };
28580
+ }
28581
+ if (matchesGrep !== void 0 && filtered.length === 0 && preFilterCount > 0) {
28582
+ if (opts.json === true) {
28583
+ const text2 = JSON.stringify({ items: [], truncated: false, totalCount: 0 }, null, 2);
28584
+ return { text: text2, code: 0 };
28585
+ }
28586
+ return { text: grepFilteredToEmptyNotice(preFilterCount, opts.grep ?? "", "symbol", "symbols"), code: 0 };
28587
+ }
28520
28588
  if (results.length === 0) {
28521
- let text2 = `No matches for '${opts.name ?? "*"}'`;
28589
+ let text2 = `No matches for '${opts.name ?? opts.grep ?? "*"}'`;
28522
28590
  const emptyIndexRoot = opts.json !== true ? opts.projectRoot ?? resolveProjectRoot({ project: process.cwd() }) : null;
28523
28591
  const indexEmpty = emptyIndexRoot !== null && isIndexEmptyForProject(globalDbPath(), emptyIndexRoot);
28524
28592
  if (opts.name !== void 0 && emptyIndexRoot !== null) {
@@ -28539,11 +28607,19 @@ ${emptyIndexMessage(emptyIndexRoot)}`;
28539
28607
  const symbolDisplayRoot = getDisplayRoot(opts.projectRoot);
28540
28608
  if (opts.json === true) {
28541
28609
  const capped = guardJsonRows(results);
28542
- const trueTotal = countSymbols(queryOpts);
28610
+ let trueTotal;
28611
+ let truncatedFlag;
28612
+ if (anyClientFilter) {
28613
+ trueTotal = filtered.length;
28614
+ truncatedFlag = capped.truncated || results.length < filtered.length;
28615
+ } else {
28616
+ trueTotal = countSymbols(queryOpts);
28617
+ truncatedFlag = capped.truncated || trueTotal > results.length;
28618
+ }
28543
28619
  const items = capped.items.map((s) => ({ ...s, filePath: toDisplayPath(symbolDisplayRoot, s.filePath) }));
28544
- const payload = { items, truncated: capped.truncated || trueTotal > results.length, totalCount: trueTotal };
28620
+ const payload = { items, truncated: truncatedFlag, totalCount: trueTotal };
28545
28621
  const text2 = JSON.stringify(payload, null, 2);
28546
- recordReadStat("symbol_lookup", fullSourceBytes, text2, opts.name ?? opts.file);
28622
+ recordReadStat("symbol_lookup", fullSourceBytes, text2, opts.name ?? opts.file ?? opts.grep);
28547
28623
  return { text: text2, code: 0 };
28548
28624
  }
28549
28625
  const blocks = results.map((sym) => {
@@ -28555,7 +28631,7 @@ ${preview}` : header;
28555
28631
  });
28556
28632
  const warning = opts.file !== void 0 ? staleWarning(resolveIndexPath(opts.file, opts.projectRoot ?? process.cwd())) : "";
28557
28633
  const text = guardText(warning + blocks.join("\n\n"), "symbol");
28558
- recordReadStat("symbol_lookup", fullSourceBytes, text, opts.name ?? opts.file);
28634
+ recordReadStat("symbol_lookup", fullSourceBytes, text, opts.name ?? opts.file ?? opts.grep);
28559
28635
  return { text, code: 0 };
28560
28636
  }
28561
28637
  function parseReadSpec(spec) {
@@ -28624,7 +28700,7 @@ function runLineRange(range2, opts) {
28624
28700
  const allLines = text.split(/\r?\n/);
28625
28701
  if (allLines.length > 1 && allLines[allLines.length - 1] === "") allLines.pop();
28626
28702
  if (start > allLines.length) {
28627
- return { text: `Line ${start} is past end of file (${allLines.length} lines): ${file2}`, code: 1 };
28703
+ return { text: `Line ${start} is past end of file (${countNoun(allLines.length, "line")}): ${file2}`, code: 1 };
28628
28704
  }
28629
28705
  const clampedEnd = Math.min(end, allLines.length);
28630
28706
  const slice = allLines.slice(start - 1, clampedEnd);
@@ -28660,13 +28736,13 @@ function formatAmbiguity(symbol3, file2, candidates, explicitRoot, commandName =
28660
28736
  const multiFile = new Set(candidates.map((c) => c.filePath)).size > 1;
28661
28737
  const displayRoot = getDisplayRoot(explicitRoot);
28662
28738
  const lines2 = [
28663
- `Ambiguous symbol '${symbol3}' in '${file2}': ${candidates.length} definitions match. Retry with one of the qualified commands below to pick one:`
28739
+ `Ambiguous symbol '${symbol3}' in '${file2}': ${countNoun(candidates.length, "definition")} match. Retry with one of the qualified commands below to pick one:`
28664
28740
  ];
28665
28741
  const fileSymCache = /* @__PURE__ */ new Map();
28666
28742
  const getFileSyms = (filePath) => {
28667
28743
  let fileSyms = fileSymCache.get(filePath);
28668
28744
  if (fileSyms === void 0) {
28669
- fileSyms = querySymbols({ filePath, limit: 1e3 });
28745
+ fileSyms = querySymbols({ filePath, limit: FIND_SCAN_LIMIT });
28670
28746
  fileSymCache.set(filePath, fileSyms);
28671
28747
  }
28672
28748
  return fileSyms;
@@ -29052,17 +29128,17 @@ function runRefs(opts) {
29052
29128
  lines2.push(`${sym}: ${grepFilteredToEmptyNotice(preGrepCount, opts.grep ?? "", "reference", "references").trim()}`);
29053
29129
  continue;
29054
29130
  }
29055
- 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)`);
29131
+ lines2.push(opts.excludeTests === true && suppressed > 0 ? `${sym}: (no non-test references found; ${excludeTestsHiddenNote(suppressed)})` : `${sym}: (no references found)`);
29056
29132
  continue;
29057
29133
  }
29058
29134
  lines2.push(`${sym}:`);
29059
29135
  if (opts.top !== void 0) {
29060
29136
  lines2.push(...renderTopFilesSummary(results, opts.top, suppressed));
29061
29137
  } else if (opts.callers === true) {
29062
- if (opts.excludeTests === true && suppressed > 0) lines2.push(` ${results.length} references (${suppressed} in test files hidden by --exclude-tests)`);
29138
+ if (opts.excludeTests === true && suppressed > 0) lines2.push(` ${countNoun(results.length, "reference")} (${excludeTestsHiddenNote(suppressed)})`);
29063
29139
  lines2.push(...renderCallerGroups(results, opts.context ?? 0));
29064
29140
  } else {
29065
- if (opts.excludeTests === true && suppressed > 0) lines2.push(` ${results.length} references (${suppressed} in test files hidden by --exclude-tests)`);
29141
+ if (opts.excludeTests === true && suppressed > 0) lines2.push(` ${countNoun(results.length, "reference")} (${excludeTestsHiddenNote(suppressed)})`);
29066
29142
  for (const ref2 of results) lines2.push(...renderRefLines(ref2, opts.context ?? 0));
29067
29143
  }
29068
29144
  }
@@ -29123,17 +29199,17 @@ function runRefsCrossFile(pairs, opts) {
29123
29199
  lines2.push(`${key}: ${grepFilteredToEmptyNotice(preGrepCount, opts.grep ?? "", "reference", "references").trim()}`);
29124
29200
  continue;
29125
29201
  }
29126
- 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)`);
29202
+ lines2.push(opts.excludeTests === true && suppressed > 0 ? `${key}: (no non-test references found; ${excludeTestsHiddenNote(suppressed)})` : `${key}: (no references found)`);
29127
29203
  continue;
29128
29204
  }
29129
29205
  lines2.push(`${key}:`);
29130
29206
  if (opts.top !== void 0) {
29131
29207
  lines2.push(...renderTopFilesSummary(results, opts.top, suppressed));
29132
29208
  } else if (opts.callers === true) {
29133
- if (opts.excludeTests === true && suppressed > 0) lines2.push(` ${results.length} references (${suppressed} in test files hidden by --exclude-tests)`);
29209
+ if (opts.excludeTests === true && suppressed > 0) lines2.push(` ${countNoun(results.length, "reference")} (${excludeTestsHiddenNote(suppressed)})`);
29134
29210
  lines2.push(...renderCallerGroups(results, opts.context ?? 0));
29135
29211
  } else {
29136
- if (opts.excludeTests === true && suppressed > 0) lines2.push(` ${results.length} references (${suppressed} in test files hidden by --exclude-tests)`);
29212
+ if (opts.excludeTests === true && suppressed > 0) lines2.push(` ${countNoun(results.length, "reference")} (${excludeTestsHiddenNote(suppressed)})`);
29137
29213
  for (const ref2 of results) lines2.push(...renderRefLines(ref2, opts.context ?? 0));
29138
29214
  }
29139
29215
  }
@@ -29174,16 +29250,25 @@ function runRefsSingle(opts) {
29174
29250
  }
29175
29251
  if (results.length === 0) {
29176
29252
  if (matchesGrep !== void 0 && preGrepCount > 0) {
29253
+ if (opts.json === true) {
29254
+ emit2(JSON.stringify({ items: [], truncated: false, totalCount: 0 }, null, 2));
29255
+ return 0;
29256
+ }
29177
29257
  emit2(grepFilteredToEmptyNotice(preGrepCount, opts.grep ?? "", "reference", "references"));
29178
29258
  return 0;
29179
29259
  }
29180
29260
  if (opts.excludeTests === true && suppressed > 0) {
29181
- emitErr(`No non-test references found for '${symName}' (${suppressed} in test files hidden by --exclude-tests)`);
29261
+ emitErr(`No non-test references found for '${symName}' (${excludeTestsHiddenNote(suppressed)})`);
29262
+ return 1;
29263
+ }
29264
+ const rootDir = resolveProjectRoot({ project: process.cwd() });
29265
+ if (querySymbols({ name: symName, rootDir, limit: 1 }).length === 0) {
29266
+ emitErr(`Symbol not found: ${symName}${unknownSymbolSuggestion(symName, rootDir)}`);
29267
+ if (opts.json !== true && isIndexEmptyForProject(globalDbPath(), rootDir)) emitErr(emptyIndexMessage(rootDir));
29182
29268
  return 1;
29183
29269
  }
29184
29270
  emitErr(`No references found for '${symName}'`);
29185
29271
  if (opts.json !== true) {
29186
- const rootDir = resolveProjectRoot({ project: process.cwd() });
29187
29272
  if (isIndexEmptyForProject(globalDbPath(), rootDir)) emitErr(emptyIndexMessage(rootDir));
29188
29273
  }
29189
29274
  return 1;
@@ -29203,7 +29288,7 @@ function runRefsSingle(opts) {
29203
29288
  recordReadStat("symbol_read", fullSourceBytes, text2, symName);
29204
29289
  return 0;
29205
29290
  }
29206
- 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, ""))];
29291
+ 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, ""))];
29207
29292
  const text = lines2.join("\n");
29208
29293
  emitGuarded(text, "symbol");
29209
29294
  recordReadStat("symbol_read", fullSourceBytes, text, symName);
@@ -29221,8 +29306,8 @@ function groupRefsByFile(refs) {
29221
29306
  function renderTopFilesSummary(refs, topN, suppressed) {
29222
29307
  const grouped = groupRefsByFile(refs);
29223
29308
  const shown = grouped.slice(0, topN);
29224
- const suppressedNote = suppressed !== void 0 && suppressed > 0 ? ` (${suppressed} in test files hidden by --exclude-tests)` : "";
29225
- const lines2 = [`${refs.length} references across ${grouped.length} files (showing top ${shown.length})${suppressedNote}`];
29309
+ const suppressedNote = suppressed !== void 0 && suppressed > 0 ? ` (${excludeTestsHiddenNote(suppressed)})` : "";
29310
+ const lines2 = [`${countNoun(refs.length, "reference")} across ${countNoun(grouped.length, "file")} (showing top ${shown.length})${suppressedNote}`];
29226
29311
  for (const { file: file2, count } of shown) lines2.push(` ${count} ${refsDisplayPath(file2)}`);
29227
29312
  const omittedFiles = grouped.length - shown.length;
29228
29313
  if (omittedFiles > 0) {
@@ -29371,7 +29456,7 @@ function runSkeleton(opts) {
29371
29456
  return { text: text2, code: 0 };
29372
29457
  }
29373
29458
  const totalLines = filtered.length > 0 ? Math.max(...filtered.map((s) => s.lineEnd)) : 0;
29374
- const lines2 = [`# Skeleton: ${opts.file} (${filtered.length} symbols, ${totalLines} lines)`];
29459
+ const lines2 = [`# Skeleton: ${opts.file} (${countNoun(filtered.length, "symbol")}, ${countNoun(totalLines, "line")})`];
29375
29460
  if (filtered.length === 0 && preFilterCount > 0) lines2.push(filteredToEmptyNotice(preFilterCount, opts.minLines, opts.grep));
29376
29461
  for (const sym of filtered) {
29377
29462
  const lineStr = sym.lineStart.toString().padStart(6);
@@ -29411,7 +29496,7 @@ function runOutline(opts) {
29411
29496
  recordReadStat("outline", fullSourceBytes, text2, opts.file);
29412
29497
  return { text: text2, code: 0 };
29413
29498
  }
29414
- const lines2 = [`# Outline: ${opts.file} (${filtered.length} symbols)`];
29499
+ const lines2 = [`# Outline: ${opts.file} (${countNoun(filtered.length, "symbol")})`];
29415
29500
  if (filtered.length === 0 && preFilterCount > 0) lines2.push(filteredToEmptyNotice(preFilterCount, opts.minLines, opts.grep));
29416
29501
  for (const sym of filtered) {
29417
29502
  const rangeStr = `${sym.lineStart.toString().padStart(4)}-${sym.lineEnd.toString().padEnd(6)}`;
@@ -30118,16 +30203,25 @@ function runFind(opts) {
30118
30203
  const symbols = rawSymbols.filter(
30119
30204
  (s) => s.name.toLowerCase().includes(patternLower)
30120
30205
  );
30121
- const files = [...new Set(symbols.map((s) => s.filePath))].slice(0, opts.limit ?? 50);
30206
+ let fuzzyNames = [];
30207
+ if (symbols.length === 0) {
30208
+ fuzzyNames = rankSimilarNames(rawSymbols.map((s) => s.name), opts.pattern);
30209
+ }
30210
+ const matched = fuzzyNames.length > 0 ? fuzzyNames.flatMap((n) => rawSymbols.filter((s) => s.name === n)) : symbols;
30211
+ const files = [...new Set(matched.map((s) => s.filePath))].slice(0, opts.limit ?? 50);
30122
30212
  const truncated = rawSymbols.length === FIND_SCAN_LIMIT;
30123
30213
  if (files.length === 0) {
30124
30214
  emitErr(`No indexed files match '${opts.pattern}'`);
30125
30215
  return 1;
30126
30216
  }
30127
30217
  if (opts.json === true) {
30128
- emit2(JSON.stringify({ files, truncated }, null, 2));
30218
+ const fuzzyPayload = fuzzyNames.length > 0 ? { fuzzy: true, matchedNames: fuzzyNames } : {};
30219
+ emit2(JSON.stringify({ files, truncated, ...fuzzyPayload }, null, 2));
30129
30220
  return 0;
30130
30221
  }
30222
+ if (fuzzyNames.length > 0) {
30223
+ emitErr(`No symbol name contains '${opts.pattern}'; showing files for the nearest indexed ${fuzzyNames.length === 1 ? "name" : "names"}: ${fuzzyNames.join(", ")}`);
30224
+ }
30131
30225
  for (const f of files) {
30132
30226
  emit2(toDisplayPath(rootDir, f));
30133
30227
  }
@@ -30231,7 +30325,12 @@ function runChanged(opts = {}) {
30231
30325
  emitErr(`Could not run git diff against '${ref2}'`);
30232
30326
  return 1;
30233
30327
  }
30328
+ const emptyEnvelope = () => {
30329
+ emit2(JSON.stringify({ items: [], truncated: false, totalCount: 0 }, null, 2));
30330
+ return 0;
30331
+ };
30234
30332
  if (changedFiles.length === 0) {
30333
+ if (opts.json === true) return emptyEnvelope();
30235
30334
  emit2("No files changed.");
30236
30335
  return 0;
30237
30336
  }
@@ -30239,9 +30338,29 @@ function runChanged(opts = {}) {
30239
30338
  const matchesGrep = opts.grep !== void 0 ? compileGrepMatcher(opts.grep) : void 0;
30240
30339
  if (matchesGrep !== void 0) changedFiles = changedFiles.filter((f) => matchesGrep(f));
30241
30340
  if (matchesGrep !== void 0 && changedFiles.length === 0) {
30341
+ if (opts.json === true) return emptyEnvelope();
30242
30342
  emit2(grepFilteredToEmptyNotice(preGrepFileCount, opts.grep ?? "", "changed file", "changed files"));
30243
30343
  return 0;
30244
30344
  }
30345
+ const postGrepFileCount = changedFiles.length;
30346
+ let hiddenTestFiles = 0;
30347
+ if (opts.excludeTests === true) {
30348
+ const kept = changedFiles.filter((f) => !isTestFile(f));
30349
+ hiddenTestFiles = changedFiles.length - kept.length;
30350
+ changedFiles = kept;
30351
+ }
30352
+ if (changedFiles.length === 0 && hiddenTestFiles > 0) {
30353
+ if (opts.json === true) return emptyEnvelope();
30354
+ const grepRemoved = preGrepFileCount - postGrepFileCount;
30355
+ if (matchesGrep !== void 0 && grepRemoved > 0) {
30356
+ emit2(
30357
+ `No non-test files matched --grep ${opts.grep ?? ""} (${excludeTestsHiddenNote(hiddenTestFiles)}; ${countNoun(grepRemoved, "other changed file")} did not match the filter)`
30358
+ );
30359
+ return 0;
30360
+ }
30361
+ emit2(`No non-test files changed (${excludeTestsHiddenNote(hiddenTestFiles)})`);
30362
+ return 0;
30363
+ }
30245
30364
  if (opts.symbolMode === true) {
30246
30365
  let hunksByFile = /* @__PURE__ */ new Map();
30247
30366
  try {
@@ -30253,12 +30372,13 @@ function runChanged(opts = {}) {
30253
30372
  }
30254
30373
  const allSymbols = [];
30255
30374
  for (const f of changedFiles) {
30256
- const fileSymbols = querySymbols({ filePath: resolveIndexPath(f, projectRoot), limit: 1e3 });
30375
+ const fileSymbols = querySymbols({ filePath: resolveIndexPath(f, projectRoot), limit: FIND_SCAN_LIMIT });
30257
30376
  const hunks = hunksByFile.get(f);
30258
30377
  const scoped = hunks === void 0 ? fileSymbols : fileSymbols.filter((s) => hunks.some((h) => h.start <= s.lineEnd && h.end >= s.lineStart));
30259
30378
  allSymbols.push(...scoped);
30260
30379
  }
30261
30380
  if (allSymbols.length === 0) {
30381
+ if (opts.json === true) return emptyEnvelope();
30262
30382
  emit2("No symbols changed.");
30263
30383
  return 0;
30264
30384
  }
@@ -31237,7 +31357,17 @@ async function runSemantic(query, opts) {
31237
31357
  void 0,
31238
31358
  rootDir
31239
31359
  );
31240
- const hits = mergeNearbyHits(rawHits).slice(0, n);
31360
+ const matchesGrep = opts.grep !== void 0 ? compileGrepMatcher(opts.grep) : void 0;
31361
+ const mergedHits = mergeNearbyHits(rawHits);
31362
+ const preGrepHitCount = mergedHits.length;
31363
+ const keepHit = (h) => {
31364
+ if (opts.excludeTests === true && isTestFile(h.filePath)) return false;
31365
+ return matchesGrep === void 0 || matchesGrep(toDisplayPath(rootDir, h.filePath));
31366
+ };
31367
+ const anyFilter = matchesGrep !== void 0 || opts.excludeTests === true;
31368
+ const filteredHits = anyFilter ? mergedHits.filter(keepHit) : mergedHits;
31369
+ const suppressedHits = opts.excludeTests === true ? mergedHits.filter((h) => (matchesGrep === void 0 || matchesGrep(toDisplayPath(rootDir, h.filePath))) && isTestFile(h.filePath)).length : 0;
31370
+ const hits = filteredHits.slice(0, n);
31241
31371
  if (hits.length > 0) {
31242
31372
  const enclosing = hits.map((h) => resolveEnclosingSymbol(h.filePath, h.startLine));
31243
31373
  if (opts.json === true) {
@@ -31265,8 +31395,41 @@ ${previewLines(h.text, 3)}`;
31265
31395
  recordReadStat("semantic_search", sumFileSizes(hits.map((h) => h.filePath)), text2, query);
31266
31396
  return { text: text2, code: 0 };
31267
31397
  }
31268
- const results = searchSymbolsFts(query, n, void 0, rootDir);
31398
+ let results;
31399
+ let preGrepFtsCount = 0;
31400
+ let suppressedFts = 0;
31401
+ if (anyFilter) {
31402
+ const overFetchFts = Math.min(MAX_OVER_FETCH, n * OVER_FETCH_FACTOR);
31403
+ const rawResults = searchSymbolsFts(query, overFetchFts, void 0, rootDir);
31404
+ preGrepFtsCount = rawResults.length;
31405
+ if (opts.excludeTests === true) {
31406
+ suppressedFts = rawResults.filter((s) => (matchesGrep === void 0 || matchesGrep(toDisplayPath(rootDir, s.filePath))) && isTestFile(s.filePath)).length;
31407
+ }
31408
+ results = rawResults.filter(keepHit).slice(0, n);
31409
+ } else {
31410
+ results = searchSymbolsFts(query, n, void 0, rootDir);
31411
+ }
31269
31412
  if (results.length === 0) {
31413
+ const preGrepTotal = preGrepHitCount + preGrepFtsCount;
31414
+ if (matchesGrep !== void 0 && preGrepTotal > 0) {
31415
+ const notice = grepFilteredToEmptyNotice(preGrepTotal, opts.grep ?? "", "match", "matches");
31416
+ if (opts.json === true) {
31417
+ const payload = { source: "fts", items: [], truncated: false, totalCount: 0, grepFilteredToEmpty: true, hint: notice.trim() };
31418
+ return { text: JSON.stringify(payload, null, 2), code: 0 };
31419
+ }
31420
+ return { text: `token-goat: ${notice.trim()}`, code: 0 };
31421
+ }
31422
+ if (opts.excludeTests === true) {
31423
+ const suppressedTotal = suppressedHits + suppressedFts;
31424
+ if (suppressedTotal > 0) {
31425
+ const notice = `no non-test matches for '${query}' (${excludeTestsHiddenNote(suppressedTotal)})`;
31426
+ if (opts.json === true) {
31427
+ const payload = { source: "fts", items: [], truncated: false, totalCount: 0, excludeTestsFilteredToEmpty: true, hint: notice };
31428
+ return { text: JSON.stringify(payload, null, 2), code: 0 };
31429
+ }
31430
+ return { text: `token-goat: ${notice}`, code: 0 };
31431
+ }
31432
+ }
31270
31433
  const indexEmpty = isIndexEmptyForProject(globalDbPath(), rootDir);
31271
31434
  if (opts.json === true) {
31272
31435
  const payload = indexEmpty ? { source: "fts", items: [], truncated: false, totalCount: 0, indexEmpty: true, hint: emptyIndexMessage(rootDir) } : { source: "fts", items: [], truncated: false, totalCount: 0 };
@@ -31526,7 +31689,7 @@ function fileDefinesName(fp, name2, getSyms) {
31526
31689
  return getSyms(fp).some((s) => s.name === name2);
31527
31690
  }
31528
31691
  function filterRefsForSymbol(refs, name2, filePath, getSyms) {
31529
- return refs.filter((ref2) => ref2.filePath === filePath || !fileDefinesName(ref2.filePath, name2, getSyms));
31692
+ return refs.filter((ref2) => foldPath(ref2.filePath) === foldPath(filePath) || !fileDefinesName(ref2.filePath, name2, getSyms));
31530
31693
  }
31531
31694
  function resolveCallers(name2, limit, filePath, rootDir, excludeTests) {
31532
31695
  const resolvedRootDir = rootDir ?? resolveProjectRoot({ project: process.cwd() });
@@ -31574,7 +31737,12 @@ function runCallers(opts) {
31574
31737
  return 0;
31575
31738
  }
31576
31739
  if (opts.excludeTests === true && suppressed > 0) {
31577
- emitErr2(`No non-test references found for '${opts.symbol}' (${suppressed} in test files hidden by --exclude-tests)`);
31740
+ emitErr2(`No non-test references found for '${opts.symbol}' (${excludeTestsHiddenNote(suppressed)})`);
31741
+ return 1;
31742
+ }
31743
+ if (querySymbols({ name: name2, rootDir, limit: 1 }).length === 0) {
31744
+ emitErr2(`Symbol not found: ${opts.symbol}${unknownSymbolSuggestion(name2, rootDir)}`);
31745
+ if (opts.json !== true && isIndexEmptyForProject(globalDbPath(), rootDir)) emitErr2(emptyIndexMessage(rootDir));
31578
31746
  return 1;
31579
31747
  }
31580
31748
  emitErr2(`No references found for '${opts.symbol}'`);
@@ -31594,7 +31762,7 @@ function runCallers(opts) {
31594
31762
  return 0;
31595
31763
  }
31596
31764
  if (opts.excludeTests === true && suppressed > 0) {
31597
- emit3(`${entries.length} callers found (${suppressed} in test files hidden by --exclude-tests)`);
31765
+ emit3(`${countNoun(entries.length, "caller")} found (${excludeTestsHiddenNote(suppressed)})`);
31598
31766
  }
31599
31767
  for (const e of entries) {
31600
31768
  const displayPath = toDisplayPath(rootDir, e.file);
@@ -31620,17 +31788,22 @@ function runCallChain(opts) {
31620
31788
  return 1;
31621
31789
  }
31622
31790
  } else if (querySymbols({ name: name2, rootDir, limit: 1 }).length === 0) {
31623
- emitErr2(`Symbol not found: ${opts.symbol}`);
31791
+ emitErr2(`Symbol not found: ${opts.symbol}${unknownSymbolSuggestion(name2, rootDir)}`);
31624
31792
  if (opts.json !== true && isIndexEmptyForProject(globalDbPath(), rootDir)) emitErr2(emptyIndexMessage(rootDir));
31625
31793
  return 1;
31626
31794
  }
31627
31795
  const getSyms = buildFileSymCache();
31796
+ let suppressedCount = 0;
31628
31797
  const callersOf = (n) => {
31629
31798
  const refs = queryRefs({ name: n, limit: DEFAULT_REF_QUERY_LIMIT, rootDir });
31630
31799
  if (refs.length === 0) return [];
31631
31800
  const scoped = fileHint !== void 0 && n === name2 ? filterRefsForSymbol(refs, n, fileHint, getSyms) : refs;
31632
31801
  const names = /* @__PURE__ */ new Set();
31633
31802
  for (const ref2 of scoped) {
31803
+ if (opts.excludeTests === true && isTestFile(ref2.filePath)) {
31804
+ suppressedCount += 1;
31805
+ continue;
31806
+ }
31634
31807
  const enc = enclosingSymbol(getSyms(ref2.filePath), ref2.line);
31635
31808
  if (enc !== null) names.add(enc.name);
31636
31809
  }
@@ -31642,6 +31815,10 @@ function runCallChain(opts) {
31642
31815
  return 0;
31643
31816
  }
31644
31817
  if (chains.length === 1 && chains[0]?.length === 1 && chains[0][0] === name2) {
31818
+ if (opts.excludeTests === true && suppressedCount > 0) {
31819
+ emit3(`${name2} (no non-test callers; ${excludeTestsHiddenNote(suppressedCount)})`);
31820
+ return 0;
31821
+ }
31645
31822
  emit3(`${name2} (no callers)`);
31646
31823
  return 0;
31647
31824
  }
@@ -31671,6 +31848,7 @@ function runImpact(opts) {
31671
31848
  const getSyms = buildFileSymCache();
31672
31849
  const hops = /* @__PURE__ */ new Map([[rootName, 0]]);
31673
31850
  const queue = [[rootName, 0]];
31851
+ let suppressedCount = 0;
31674
31852
  while (queue.length > 0) {
31675
31853
  const item = queue.shift();
31676
31854
  if (item === void 0) break;
@@ -31679,6 +31857,10 @@ function runImpact(opts) {
31679
31857
  const refs = queryRefs({ name: name2, limit: DEFAULT_REF_QUERY_LIMIT, rootDir });
31680
31858
  const scoped = fileHint !== void 0 && name2 === rootName ? filterRefsForSymbol(refs, name2, fileHint, getSyms) : refs;
31681
31859
  for (const ref2 of scoped) {
31860
+ if (opts.excludeTests === true && isTestFile(ref2.filePath)) {
31861
+ suppressedCount += 1;
31862
+ continue;
31863
+ }
31682
31864
  const newHop = depth + 1;
31683
31865
  const enc = enclosingSymbol(getSyms(ref2.filePath), ref2.line);
31684
31866
  if (enc === null) {
@@ -31698,6 +31880,14 @@ function runImpact(opts) {
31698
31880
  hops.delete(rootName);
31699
31881
  const sorted = [...hops.entries()].sort(compareHopEntries).slice(0, top);
31700
31882
  if (sorted.length === 0) {
31883
+ if (opts.excludeTests === true && suppressedCount > 0) {
31884
+ emitErr2(`No non-test impact found for '${opts.symbol}' (${excludeTestsHiddenNote(suppressedCount)})`);
31885
+ return 1;
31886
+ }
31887
+ if (querySymbols({ name: rootName, rootDir, limit: 1 }).length === 0) {
31888
+ emitErr2(`Symbol not found: ${opts.symbol}${unknownSymbolSuggestion(rootName, rootDir)}`);
31889
+ return 1;
31890
+ }
31701
31891
  emitErr2(`No callers found for '${opts.symbol}'`);
31702
31892
  return 1;
31703
31893
  }
@@ -31794,7 +31984,7 @@ function runDead(opts) {
31794
31984
  return 0;
31795
31985
  }
31796
31986
  if (opts.excludeTests === true && suppressed > 0) {
31797
- emit3(`No dead symbols found (${suppressed} in test files hidden by --exclude-tests).`);
31987
+ emit3(`No dead symbols found (${excludeTestsHiddenNote(suppressed)}).`);
31798
31988
  } else {
31799
31989
  emit3("No dead symbols found.");
31800
31990
  }
@@ -31802,7 +31992,7 @@ function runDead(opts) {
31802
31992
  return 0;
31803
31993
  }
31804
31994
  if (opts.excludeTests === true && suppressed > 0) {
31805
- emit3(`${sliced.length} dead symbols (${suppressed} in test files hidden by --exclude-tests)`);
31995
+ emit3(`${countNoun(sliced.length, "dead symbol")} (${excludeTestsHiddenNote(suppressed)})`);
31806
31996
  }
31807
31997
  for (const r of sliced) {
31808
31998
  emit3(`${r.name} ${toDisplayPath(rootDir, r.file)}:${r.line}`);
@@ -67309,7 +67499,6 @@ function checkSymbolCount(dbPath, rootDir) {
67309
67499
  }
67310
67500
  }
67311
67501
  var DIRTY_QUEUE_BACKLOG_WARN_THRESHOLD = 500;
67312
- var DRAIN_HEARTBEAT_STALE_MS = 6e4;
67313
67502
  function checkDirtyQueueHealth(dataDir2) {
67314
67503
  let pendingCount = 0;
67315
67504
  try {
@@ -67332,7 +67521,7 @@ function checkDirtyQueueHealth(dataDir2) {
67332
67521
  heartbeatAgeMs = Date.now() - fs47.statSync(drainHeartbeatPathFor(dataDir2)).mtimeMs;
67333
67522
  } catch {
67334
67523
  }
67335
- if (heartbeatAgeMs !== null && heartbeatAgeMs > DRAIN_HEARTBEAT_STALE_MS) {
67524
+ if (heartbeatAgeMs !== null && heartbeatAgeMs > WORKER_HEARTBEAT_STALE_MS) {
67336
67525
  return {
67337
67526
  name: "Dirty queue",
67338
67527
  status: "warn",
@@ -73409,7 +73598,12 @@ function requirePositiveInt(flag, raw) {
73409
73598
  }
73410
73599
  async function cmdSemantic(query, opts) {
73411
73600
  const limit = opts.limit !== void 0 ? requireNonNegativeInt("--limit", opts.limit) : 20;
73412
- const { text, code } = await runSemantic(query, { limit, ...opts.json === true ? { json: true } : {} });
73601
+ const { text, code } = await runSemantic(query, {
73602
+ limit,
73603
+ ...opts.json === true ? { json: true } : {},
73604
+ ...opts.grep !== void 0 ? { grep: opts.grep } : {},
73605
+ ...opts.excludeTests === true ? { excludeTests: true } : {}
73606
+ });
73413
73607
  (opts.json === true || code === 0 ? out : err)(text);
73414
73608
  process.exitCode = code;
73415
73609
  }
@@ -75344,7 +75538,7 @@ function buildProgram() {
75344
75538
  process.exitCode = 1;
75345
75539
  }
75346
75540
  };
75347
- 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) => {
75541
+ 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) => {
75348
75542
  let projectRoot;
75349
75543
  if (opts.project === true) {
75350
75544
  projectRoot = resolveProjectRoot({ project: process.cwd() });
@@ -75353,12 +75547,14 @@ function buildProgram() {
75353
75547
  }
75354
75548
  return runExitText(
75355
75549
  () => runSymbol({
75356
- name: name2,
75550
+ ...name2 !== void 0 ? { name: name2 } : {},
75357
75551
  limit: opts.limit !== void 0 ? requireNonNegativeInt("--limit", opts.limit) : 20,
75358
75552
  ...opts.file !== void 0 ? { file: opts.file } : {},
75359
75553
  ...opts.kind !== void 0 ? { kind: opts.kind } : {},
75360
75554
  ...projectRoot !== void 0 ? { projectRoot } : {},
75361
- ...opts.json === true ? { json: true } : {}
75555
+ ...opts.json === true ? { json: true } : {},
75556
+ ...opts.grep !== void 0 ? { grep: opts.grep } : {},
75557
+ ...opts.excludeTests === true ? { excludeTests: true } : {}
75362
75558
  })
75363
75559
  );
75364
75560
  });
@@ -75391,7 +75587,7 @@ function buildProgram() {
75391
75587
  ).option("-j, --json", "output as JSON").option("--list", "list all section headings in the file instead of reading one").action(
75392
75588
  (spec, opts) => opts.list === true ? runExit(() => runListSections({ file: spec, ...opts.json === true ? { json: true } : {} })) : runExitText(() => runSection({ spec, ...opts.json === true ? { json: true } : {} }))
75393
75589
  );
75394
- 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));
75590
+ 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));
75395
75591
  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(
75396
75592
  (file2, more, opts) => runExitText(
75397
75593
  () => noteExtraFileArgs(
@@ -75527,21 +75723,23 @@ function buildProgram() {
75527
75723
  })
75528
75724
  )
75529
75725
  );
75530
- 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(
75726
+ 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(
75531
75727
  (symbol3, opts) => runExit(
75532
75728
  () => runCallChain({
75533
75729
  symbol: symbol3,
75534
75730
  ...opts.depth !== void 0 ? { depth: requireInt("--depth", opts.depth) } : {},
75535
- ...opts.json === true ? { json: true } : {}
75731
+ ...opts.json === true ? { json: true } : {},
75732
+ ...opts.excludeTests === true ? { excludeTests: true } : {}
75536
75733
  })
75537
75734
  )
75538
75735
  );
75539
- 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(
75736
+ 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(
75540
75737
  (symbol3, opts) => runExit(
75541
75738
  () => runImpact({
75542
75739
  symbol: symbol3,
75543
75740
  ...opts.top !== void 0 ? { top: requireNonNegativeInt("--top", opts.top) } : {},
75544
- ...opts.json === true ? { json: true } : {}
75741
+ ...opts.json === true ? { json: true } : {},
75742
+ ...opts.excludeTests === true ? { excludeTests: true } : {}
75545
75743
  })
75546
75744
  )
75547
75745
  );
@@ -75698,13 +75896,14 @@ function buildProgram() {
75698
75896
  }))());
75699
75897
  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 } : {} }))());
75700
75898
  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))());
75701
- 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(
75899
+ 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(
75702
75900
  (ref2, opts) => runExit(
75703
75901
  () => runChanged({
75704
75902
  ref: opts.since ?? ref2 ?? "HEAD~5",
75705
75903
  ...opts.symbol === true ? { symbolMode: true } : {},
75706
75904
  ...opts.json === true ? { json: true } : {},
75707
- ...opts.grep !== void 0 ? { grep: opts.grep } : {}
75905
+ ...opts.grep !== void 0 ? { grep: opts.grep } : {},
75906
+ ...opts.excludeTests === true ? { excludeTests: true } : {}
75708
75907
  })
75709
75908
  )
75710
75909
  );