@wrongstack/tools 0.287.0 → 0.289.0

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.
Files changed (93) hide show
  1. package/dist/_fetch-guard.d.ts.map +1 -1
  2. package/dist/_redact-command.d.ts.map +1 -1
  3. package/dist/_spawn-stream.d.ts.map +1 -1
  4. package/dist/audit.js +61 -14
  5. package/dist/audit.js.map +3 -3
  6. package/dist/bash.d.ts +1 -1
  7. package/dist/bash.d.ts.map +1 -1
  8. package/dist/bash.js +97 -19
  9. package/dist/bash.js.map +3 -3
  10. package/dist/builtin.d.ts +12 -10
  11. package/dist/builtin.d.ts.map +1 -1
  12. package/dist/builtin.js +1620 -803
  13. package/dist/builtin.js.map +3 -3
  14. package/dist/codebase-index/background-indexer.d.ts.map +1 -1
  15. package/dist/codebase-index/bm25.d.ts +1 -2
  16. package/dist/codebase-index/bm25.d.ts.map +1 -1
  17. package/dist/codebase-index/codebase-index-tool.d.ts.map +1 -1
  18. package/dist/codebase-index/codebase-search-tool.d.ts.map +1 -1
  19. package/dist/codebase-index/codebase-stats-tool.d.ts +7 -0
  20. package/dist/codebase-index/codebase-stats-tool.d.ts.map +1 -1
  21. package/dist/codebase-index/index-service.d.ts +11 -1
  22. package/dist/codebase-index/index-service.d.ts.map +1 -1
  23. package/dist/codebase-index/index.d.ts +2 -1
  24. package/dist/codebase-index/index.d.ts.map +1 -1
  25. package/dist/codebase-index/index.js +749 -192
  26. package/dist/codebase-index/index.js.map +3 -3
  27. package/dist/codebase-index/indexer.d.ts.map +1 -1
  28. package/dist/codebase-index/schema.d.ts +43 -1
  29. package/dist/codebase-index/schema.d.ts.map +1 -1
  30. package/dist/codebase-index/ts-parser.d.ts.map +1 -1
  31. package/dist/codebase-index/worker-protocol.d.ts +1 -0
  32. package/dist/codebase-index/worker-protocol.d.ts.map +1 -1
  33. package/dist/codebase-index/worker.js +610 -152
  34. package/dist/codebase-index/worker.js.map +3 -3
  35. package/dist/codebase-index/writer.d.ts +27 -1
  36. package/dist/codebase-index/writer.d.ts.map +1 -1
  37. package/dist/e2e.js +477 -0
  38. package/dist/e2e.js.map +7 -0
  39. package/dist/exec.d.ts +1 -1
  40. package/dist/exec.d.ts.map +1 -1
  41. package/dist/exec.js +60 -5
  42. package/dist/exec.js.map +3 -3
  43. package/dist/fetch.js +2 -2
  44. package/dist/fetch.js.map +2 -2
  45. package/dist/format.js +61 -14
  46. package/dist/format.js.map +3 -3
  47. package/dist/glob.js +2 -2
  48. package/dist/glob.js.map +1 -1
  49. package/dist/grep.d.ts.map +1 -1
  50. package/dist/grep.js +2 -2
  51. package/dist/grep.js.map +2 -2
  52. package/dist/index.d.ts +3 -2
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +1786 -809
  55. package/dist/index.js.map +3 -3
  56. package/dist/install.js +61 -14
  57. package/dist/install.js.map +3 -3
  58. package/dist/kanban.d.ts +11 -2
  59. package/dist/kanban.d.ts.map +1 -1
  60. package/dist/kanban.js +695 -571
  61. package/dist/kanban.js.map +3 -3
  62. package/dist/languages/detect.d.ts.map +1 -1
  63. package/dist/languages/index.js +61 -14
  64. package/dist/languages/index.js.map +3 -3
  65. package/dist/languages/types.d.ts +7 -0
  66. package/dist/languages/types.d.ts.map +1 -1
  67. package/dist/lint.js +61 -14
  68. package/dist/lint.js.map +3 -3
  69. package/dist/outdated.js +61 -14
  70. package/dist/outdated.js.map +3 -3
  71. package/dist/pack.js +1617 -800
  72. package/dist/pack.js.map +3 -3
  73. package/dist/plan.js +4 -0
  74. package/dist/plan.js.map +2 -2
  75. package/dist/process-registry.js +12 -5
  76. package/dist/process-registry.js.map +2 -2
  77. package/dist/search.js +2 -2
  78. package/dist/search.js.map +2 -2
  79. package/dist/session-kanban.d.ts +9 -0
  80. package/dist/session-kanban.d.ts.map +1 -1
  81. package/dist/session-kanban.js +137 -0
  82. package/dist/session-kanban.js.map +2 -2
  83. package/dist/task.js +4 -0
  84. package/dist/task.js.map +2 -2
  85. package/dist/test.js +61 -14
  86. package/dist/test.js.map +3 -3
  87. package/dist/todo.js +4 -0
  88. package/dist/todo.js.map +2 -2
  89. package/dist/tree.js +2 -2
  90. package/dist/tree.js.map +1 -1
  91. package/dist/typecheck.js +61 -14
  92. package/dist/typecheck.js.map +3 -3
  93. package/package.json +7 -3
@@ -92,7 +92,7 @@ import * as path from "node:path";
92
92
  import { resolveWstackPaths } from "@wrongstack/core";
93
93
 
94
94
  // src/codebase-index/schema.ts
95
- var SCHEMA_VERSION = 2;
95
+ var SCHEMA_VERSION = 3;
96
96
 
97
97
  // src/codebase-index/lsp-kind.ts
98
98
  function lspKindToInternalKind(k) {
@@ -164,7 +164,7 @@ function tokenise(text) {
164
164
  return sanitised.toLowerCase().split(" ").filter(Boolean);
165
165
  }
166
166
  function splitName(name) {
167
- return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
167
+ return name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([\p{L}])(\d)/gu, "$1 $2").replace(/(\d)([\p{L}])/gu, "$1 $2").replace(/[_-]+/g, " ").trim();
168
168
  }
169
169
  function buildIndexableText(name, signature, docComment) {
170
170
  return [splitName(name), name, signature, docComment].filter(Boolean).join(" ");
@@ -190,14 +190,12 @@ function buildBm25Index(docs) {
190
190
  return new Bm25Index(documents, df, N, avgLen);
191
191
  }
192
192
  var Bm25Index = class {
193
- constructor(documents, df, N, avgLen) {
193
+ constructor(documents, _df, N, avgLen) {
194
194
  this.documents = documents;
195
- this.df = df;
196
195
  this.N = N;
197
196
  this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
198
197
  }
199
198
  documents;
200
- df;
201
199
  N;
202
200
  safeAvgLen;
203
201
  score(query, filter) {
@@ -210,10 +208,13 @@ var Bm25Index = class {
210
208
  for (const qTerm of qTokens) {
211
209
  let tf = 0;
212
210
  for (const t of doc.tokens) {
213
- if (t === qTerm) tf++;
211
+ if (t.startsWith(qTerm)) tf++;
214
212
  }
215
213
  if (tf === 0) continue;
216
- const dfVal = this.df[qTerm] ?? 0;
214
+ let dfVal = 0;
215
+ for (const candidate of this.documents) {
216
+ if (candidate.tokens.some((t) => t.startsWith(qTerm))) dfVal++;
217
+ }
217
218
  if (dfVal === 0) continue;
218
219
  const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
219
220
  const lenRatio = B * (doc.len / this.safeAvgLen);
@@ -246,6 +247,29 @@ var Bm25Index = class {
246
247
 
247
248
  // src/codebase-index/writer.ts
248
249
  var DB_FILE = "index.db";
250
+ function escapeLike(value) {
251
+ return value.replace(/[\\%_]/g, (char) => `\\${char}`);
252
+ }
253
+ function assignRefsToSymbols(refs, symbols) {
254
+ if (refs.length === 0 || symbols.length === 0) return [];
255
+ const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
256
+ const seen = /* @__PURE__ */ new Set();
257
+ const assigned = [];
258
+ for (const ref of refs) {
259
+ let owner;
260
+ for (const symbol of ordered) {
261
+ if (symbol.line > ref.line) break;
262
+ owner = symbol;
263
+ }
264
+ if (!owner && ref.callType === "import") owner = ordered[0];
265
+ if (!owner || owner.id <= 0) continue;
266
+ const key = `${owner.id}:${ref.toName}:${ref.callType}`;
267
+ if (seen.has(key)) continue;
268
+ seen.add(key);
269
+ assigned.push({ ...ref, fromId: owner.id });
270
+ }
271
+ return assigned;
272
+ }
249
273
  function resolveIndexDir(projectRoot, override) {
250
274
  return override ?? resolveWstackPaths({ projectRoot }).projectCodebaseIndex;
251
275
  }
@@ -299,7 +323,7 @@ function sleepSync(ms) {
299
323
  } catch {
300
324
  }
301
325
  }
302
- var IndexStore = class {
326
+ var IndexStore = class _IndexStore {
303
327
  db;
304
328
  /** Absolute path to this project's index directory. */
305
329
  indexDir;
@@ -351,10 +375,7 @@ var IndexStore = class {
351
375
  const msg = lastError instanceof Error ? lastError.message : String(lastError);
352
376
  throw new LockError(`SQLite lock conflict after ${MAX_LOCK_RETRIES} retries: ${msg}`);
353
377
  }
354
- const delay = Math.min(
355
- LOCK_RETRY_BASE_DELAY_MS * 2 ** attempt,
356
- LOCK_RETRY_MAX_DELAY_MS
357
- );
378
+ const delay = Math.min(LOCK_RETRY_BASE_DELAY_MS * 2 ** attempt, LOCK_RETRY_MAX_DELAY_MS);
358
379
  sleepSync(delay);
359
380
  }
360
381
  }
@@ -436,8 +457,24 @@ var IndexStore = class {
436
457
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)");
437
458
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)");
438
459
  try {
439
- this.db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')");
460
+ this.db.exec(
461
+ "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')"
462
+ );
440
463
  this.ftsAvailable = true;
464
+ const symbolCount = Number(
465
+ this.db.prepare("SELECT COUNT(*) AS n FROM symbols").get()?.n ?? 0
466
+ );
467
+ const ftsCount = Number(
468
+ this.db.prepare("SELECT COUNT(*) AS n FROM symbols_fts").get()?.n ?? 0
469
+ );
470
+ if (symbolCount !== ftsCount) {
471
+ this.db.exec("DELETE FROM symbols_fts");
472
+ const insert = this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)");
473
+ const rows = this.db.prepare("SELECT id, name, signature, doc_comment FROM symbols ORDER BY id").all();
474
+ for (const row of rows) {
475
+ insert.run(row.id, buildIndexableText(row.name, row.signature, row.doc_comment));
476
+ }
477
+ }
441
478
  } catch {
442
479
  this.ftsAvailable = false;
443
480
  }
@@ -495,7 +532,9 @@ var IndexStore = class {
495
532
  deleteSymbolsForFile(file) {
496
533
  this.runWithRetry(() => {
497
534
  if (this.ftsAvailable) {
498
- this.stmt("DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
535
+ this.stmt(
536
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
537
+ ).run(file);
499
538
  }
500
539
  this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
501
540
  });
@@ -507,9 +546,21 @@ var IndexStore = class {
507
546
  */
508
547
  deleteFile(file) {
509
548
  this.runWithRetry(() => {
510
- this.deleteRefsForFile(file);
511
- this.deleteSymbolsForFile(file);
512
- this.stmt("DELETE FROM files WHERE file = ?").run(file);
549
+ this.db.exec("BEGIN IMMEDIATE");
550
+ try {
551
+ if (this.ftsAvailable) {
552
+ this.db.prepare(
553
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
554
+ ).run(file);
555
+ }
556
+ this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
557
+ this.db.prepare("DELETE FROM symbols WHERE file_fk = ?").run(file);
558
+ this.db.prepare("DELETE FROM files WHERE file = ?").run(file);
559
+ this.db.exec("COMMIT");
560
+ } catch (err) {
561
+ this.db.exec("ROLLBACK");
562
+ throw err;
563
+ }
513
564
  });
514
565
  }
515
566
  // ─── File metadata ──────────────────────────────────────────────────────────
@@ -532,14 +583,22 @@ var IndexStore = class {
532
583
  ).all(file);
533
584
  if (!rows.length) return null;
534
585
  const r = expectDefined(rows[0]);
535
- return { file: r.file, lang: r.lang, mtimeMs: r.mtime_ms, symbolCount: r.symbol_count, lastIndexed: r.last_indexed };
586
+ return {
587
+ file: r.file,
588
+ lang: r.lang,
589
+ mtimeMs: r.mtime_ms,
590
+ symbolCount: r.symbol_count,
591
+ lastIndexed: r.last_indexed
592
+ };
536
593
  }
537
594
  getAllFileMetas() {
538
- return this.db.prepare(
539
- "SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files"
540
- ).all().map(
541
- (r) => ({ file: r.file, lang: r.lang, mtimeMs: r.mtime_ms, symbolCount: r.symbol_count, lastIndexed: r.last_indexed })
542
- );
595
+ return this.db.prepare("SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files").all().map((r) => ({
596
+ file: r.file,
597
+ lang: r.lang,
598
+ mtimeMs: r.mtime_ms,
599
+ symbolCount: r.symbol_count,
600
+ lastIndexed: r.last_indexed
601
+ }));
543
602
  }
544
603
  // ─── Search ──────────────────────────────────────────────────────────────────
545
604
  search(query, filter) {
@@ -563,8 +622,8 @@ var IndexStore = class {
563
622
  values.push(filter.lang);
564
623
  }
565
624
  if (filter?.file) {
566
- conditions.push("file LIKE ?");
567
- values.push(`%${filter.file}%`);
625
+ conditions.push("replace(file, '\\', '/') LIKE ? ESCAPE '\\'");
626
+ values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
568
627
  }
569
628
  if (query.trim()) {
570
629
  const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
@@ -602,9 +661,11 @@ var IndexStore = class {
602
661
  * "users", camelCase-split text makes "complex" find "complexOperation").
603
662
  */
604
663
  searchRanked(query, filter, limit) {
664
+ const rawLimit = Number.isFinite(limit) ? Math.trunc(limit) : 20;
665
+ const safeLimit = Math.max(1, Math.min(rawLimit, 100));
605
666
  const tokens = tokenise(query);
606
667
  if (tokens.length === 0 || !this.ftsAvailable) {
607
- return this.searchRankedFallback(query, filter, limit);
668
+ return this.searchRankedFallback(query, filter, safeLimit);
608
669
  }
609
670
  let effectiveKind = filter?.kind;
610
671
  if (filter?.lspKind !== void 0) {
@@ -624,11 +685,13 @@ var IndexStore = class {
624
685
  values.push(filter.lang);
625
686
  }
626
687
  if (filter?.file) {
627
- conditions.push("s.file LIKE ?");
628
- values.push(`%${filter.file}%`);
688
+ conditions.push("replace(s.file, '\\', '/') LIKE ? ESCAPE '\\'");
689
+ values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
629
690
  }
630
691
  const where = conditions.join(" AND ");
631
- const countRows = this.db.prepare(`SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`).all(...values);
692
+ const countRows = this.db.prepare(
693
+ `SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`
694
+ ).all(...values);
632
695
  const total = countRows[0] ? Number(countRows[0].n) : 0;
633
696
  if (total === 0) return { results: [], total: 0 };
634
697
  const rows = this.db.prepare(
@@ -637,9 +700,13 @@ var IndexStore = class {
637
700
  snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
638
701
  FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
639
702
  WHERE ${where}
640
- ORDER BY bm25(symbols_fts)
703
+ ORDER BY
704
+ CASE WHEN lower(s.name) = lower(?) THEN 0
705
+ WHEN lower(s.name) LIKE lower(?) ESCAPE '\\' THEN 1
706
+ ELSE 2 END,
707
+ bm25(symbols_fts), lower(s.name), s.file, s.line, s.col, s.id
641
708
  LIMIT ?`
642
- ).all(...values, limit);
709
+ ).all(...values, query.trim(), `${escapeLike(query.trim())}%`, safeLimit);
643
710
  return {
644
711
  results: rows.map((r) => ({
645
712
  id: r.id,
@@ -669,10 +736,28 @@ var IndexStore = class {
669
736
  }
670
737
  const candidateById = new Map(candidates.map((c) => [c.id, c]));
671
738
  const bm25 = buildBm25Index(
672
- candidates.map((c) => ({ id: c.id, text: buildIndexableText(c.name, c.signature, c.docComment) }))
739
+ candidates.map((c) => ({
740
+ id: c.id,
741
+ text: buildIndexableText(c.name, c.signature, c.docComment)
742
+ }))
673
743
  );
674
744
  const scored = bm25.score(query, (id) => candidateById.has(id));
675
- scored.sort((a, b) => b.score - a.score);
745
+ const q = query.trim().toLowerCase();
746
+ const rank = (id) => {
747
+ const name = candidateById.get(id)?.name.toLowerCase() ?? "";
748
+ if (name === q) return 0;
749
+ if (name.startsWith(q)) return 1;
750
+ return 2;
751
+ };
752
+ scored.sort((a, b) => {
753
+ const rankDiff = rank(a.id) - rank(b.id);
754
+ if (rankDiff !== 0) return rankDiff;
755
+ const scoreDiff = b.score - a.score;
756
+ if (scoreDiff !== 0) return scoreDiff;
757
+ const left = expectDefined(candidateById.get(a.id));
758
+ const right = expectDefined(candidateById.get(b.id));
759
+ return left.name.localeCompare(right.name) || left.file.localeCompare(right.file) || left.line - right.line || left.col - right.col || left.id - right.id;
760
+ });
676
761
  const qTokens = tokenise(query);
677
762
  const results = scored.slice(0, limit).map(({ id, score }) => {
678
763
  const c = expectDefined(candidateById.get(id));
@@ -681,9 +766,7 @@ var IndexStore = class {
681
766
  return { results, total: candidates.length };
682
767
  }
683
768
  getAllIndexable() {
684
- return this.db.prepare("SELECT id, text FROM symbols").all().map(
685
- ({ id, text }) => ({ id, text })
686
- );
769
+ return this.db.prepare("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
687
770
  }
688
771
  /**
689
772
  * Largest symbol id currently in the table (0 when empty). New ids must be
@@ -699,22 +782,16 @@ var IndexStore = class {
699
782
  // ─── Stats ───────────────────────────────────────────────────────────────────
700
783
  getStats() {
701
784
  const sizeBytes = this.sizeBytes();
702
- const lastRows = this.db.prepare(
703
- "SELECT value FROM metadata WHERE key = 'last_indexed'"
704
- ).all();
785
+ const lastRows = this.db.prepare("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
705
786
  const lastIndexed = lastRows.length ? Number(lastRows[0]?.value) : null;
706
787
  const totalRows = this.db.prepare("SELECT COUNT(*) FROM symbols").all();
707
788
  const totalSymbols = totalRows[0] ? Number(totalRows[0]["COUNT(*)"]) : 0;
708
789
  const fileRows = this.db.prepare("SELECT COUNT(*) FROM files").all();
709
790
  const totalFiles = fileRows[0] ? Number(fileRows[0]["COUNT(*)"]) : 0;
710
- const langRows = this.db.prepare(
711
- "SELECT lang, COUNT(*) FROM symbols GROUP BY lang"
712
- ).all();
791
+ const langRows = this.db.prepare("SELECT lang, COUNT(*) FROM symbols GROUP BY lang").all();
713
792
  const byLang = {};
714
793
  for (const row of langRows) byLang[row.lang] = Number(row["COUNT(*)"]);
715
- const kindRows = this.db.prepare(
716
- "SELECT kind, COUNT(*) FROM symbols GROUP BY kind"
717
- ).all();
794
+ const kindRows = this.db.prepare("SELECT kind, COUNT(*) FROM symbols GROUP BY kind").all();
718
795
  const byKind = {};
719
796
  for (const row of kindRows) byKind[row.kind] = Number(row["COUNT(*)"]);
720
797
  return {
@@ -730,17 +807,31 @@ var IndexStore = class {
730
807
  }
731
808
  setLastIndexed(ts2) {
732
809
  this.runWithRetry(() => {
733
- this.db.prepare(
734
- "INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)"
735
- ).run(String(ts2));
810
+ this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(String(ts2));
811
+ });
812
+ }
813
+ getMetadata(key) {
814
+ const rows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all(key);
815
+ return rows[0]?.value;
816
+ }
817
+ setMetadata(key, value) {
818
+ this.runWithRetry(() => {
819
+ this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES(?, ?)").run(key, value);
736
820
  });
737
821
  }
738
822
  clearAll() {
739
823
  this.runWithRetry(() => {
740
- this.db.exec("DELETE FROM symbols");
741
- this.db.exec("DELETE FROM files");
742
- this.db.exec("DELETE FROM refs");
743
- if (this.ftsAvailable) this.db.exec("DELETE FROM symbols_fts");
824
+ this.db.exec("BEGIN IMMEDIATE");
825
+ try {
826
+ this.db.exec("DELETE FROM refs");
827
+ this.db.exec("DELETE FROM symbols");
828
+ this.db.exec("DELETE FROM files");
829
+ if (this.ftsAvailable) this.db.exec("DELETE FROM symbols_fts");
830
+ this.db.exec("COMMIT");
831
+ } catch (err) {
832
+ this.db.exec("ROLLBACK");
833
+ throw err;
834
+ }
744
835
  });
745
836
  }
746
837
  // ─── Ref CRUD ────────────────────────────────────────────────────────────────
@@ -831,6 +922,7 @@ var IndexStore = class {
831
922
  const allInserted = [];
832
923
  const refsToInsert = [];
833
924
  for (const entry of entries) {
925
+ const insertedForEntry = [];
834
926
  for (const s of entry.symbols) {
835
927
  const id = nextId++;
836
928
  symStmt.run(
@@ -848,9 +940,11 @@ var IndexStore = class {
848
940
  s.file
849
941
  );
850
942
  ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
851
- allInserted.push({ ...s, id });
943
+ const inserted = { ...s, id };
944
+ allInserted.push(inserted);
945
+ insertedForEntry.push(inserted);
852
946
  }
853
- for (const r of entry.refs) refsToInsert.push(r);
947
+ refsToInsert.push(...assignRefsToSymbols(entry.refs, insertedForEntry));
854
948
  }
855
949
  if (refsToInsert.length > 0) {
856
950
  const refStmt = this.db.prepare(
@@ -888,9 +982,7 @@ var IndexStore = class {
888
982
  */
889
983
  deleteRefsForFile(file) {
890
984
  this.runWithRetry(() => {
891
- this.db.prepare(
892
- "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)"
893
- ).run(file);
985
+ this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(file);
894
986
  });
895
987
  }
896
988
  /**
@@ -931,9 +1023,7 @@ var IndexStore = class {
931
1023
  * Find all references FROM a given symbol (what does this symbol call/use?).
932
1024
  */
933
1025
  findRefsFrom(symbolId) {
934
- return this.db.prepare(
935
- "SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?"
936
- ).all(symbolId).map((r) => ({
1026
+ return this.db.prepare("SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?").all(symbolId).map((r) => ({
937
1027
  id: r.id,
938
1028
  fromId: r.from_id,
939
1029
  toName: r.to_name,
@@ -950,6 +1040,325 @@ var IndexStore = class {
950
1040
  return 0;
951
1041
  }
952
1042
  }
1043
+ // ─── CodeMap graph aggregation ──────────────────────────────────────────────
1044
+ /**
1045
+ * Derive a monorepo package name from an absolute file path.
1046
+ * Handles both `packages/<name>/...` (workspace) and `src/<name>/...` layouts.
1047
+ * Returns `undefined` for files outside any recognisable package structure.
1048
+ */
1049
+ static derivePackage(filePath) {
1050
+ const f = filePath.replace(/\\/g, "/");
1051
+ const pkgsIdx = f.indexOf("/packages/");
1052
+ if (pkgsIdx !== -1) {
1053
+ const rest = f.slice(pkgsIdx + "/packages/".length);
1054
+ const seg = rest.split("/")[0];
1055
+ return seg ? `@wrongstack/${seg}` : void 0;
1056
+ }
1057
+ const appsIdx = f.indexOf("/apps/");
1058
+ if (appsIdx !== -1) {
1059
+ const rest = f.slice(appsIdx + "/apps/".length);
1060
+ const seg = rest.split("/")[0];
1061
+ return seg ? `app:${seg}` : void 0;
1062
+ }
1063
+ return void 0;
1064
+ }
1065
+ static packageFromImport(moduleName) {
1066
+ if (!moduleName.startsWith("@wrongstack/")) return void 0;
1067
+ const parts = moduleName.split("/");
1068
+ return parts[1] ? `@wrongstack/${parts[1]}` : void 0;
1069
+ }
1070
+ static resolveRelativeImport(fromFile, moduleName, indexedFiles) {
1071
+ if (!moduleName.startsWith(".")) return void 0;
1072
+ const normalizedFrom = fromFile.replace(/\\/g, "/");
1073
+ const absolute = path.posix.normalize(
1074
+ path.posix.join(path.posix.dirname(normalizedFrom), moduleName)
1075
+ );
1076
+ const extension = path.posix.extname(absolute);
1077
+ const base = extension ? absolute.slice(0, -extension.length) : absolute;
1078
+ const candidates = [
1079
+ absolute,
1080
+ ...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"].map((ext) => `${base}${ext}`),
1081
+ ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path.posix.join(absolute, `index${ext}`)),
1082
+ ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path.posix.join(base, `index${ext}`))
1083
+ ];
1084
+ const indexedByPortablePath = new Map(
1085
+ [...indexedFiles].map((file) => [file.replace(/\\/g, "/").toLocaleLowerCase(), file])
1086
+ );
1087
+ for (const candidate of candidates) {
1088
+ const indexed = indexedByPortablePath.get(candidate.toLocaleLowerCase());
1089
+ if (indexed) return indexed;
1090
+ }
1091
+ return void 0;
1092
+ }
1093
+ /**
1094
+ * Package-level graph: each workspace package is a node; edges are derived
1095
+ * from cross-package symbol references (a symbol in package A references a
1096
+ * symbol resolved in package B). Node metadata includes symbol/file counts.
1097
+ */
1098
+ getPackageGraph() {
1099
+ const symbols = this.db.prepare("SELECT file, id, name, kind, lang, line FROM symbols ORDER BY id").all();
1100
+ const pkgNodes = /* @__PURE__ */ new Map();
1101
+ const fileToPkg = /* @__PURE__ */ new Map();
1102
+ const symbolToPkg = /* @__PURE__ */ new Map();
1103
+ for (const s of symbols) {
1104
+ const pkg = _IndexStore.derivePackage(s.file) ?? "(root)";
1105
+ fileToPkg.set(s.file, pkg);
1106
+ symbolToPkg.set(s.id, pkg);
1107
+ const node = pkgNodes.get(pkg);
1108
+ if (node) {
1109
+ node.symbolCount = (node.symbolCount ?? 0) + 1;
1110
+ } else {
1111
+ pkgNodes.set(pkg, {
1112
+ id: `pkg:${pkg}`,
1113
+ label: pkg,
1114
+ kind: "package",
1115
+ package: pkg,
1116
+ symbolCount: 1,
1117
+ fileCount: 0
1118
+ });
1119
+ }
1120
+ }
1121
+ const files = this.db.prepare("SELECT DISTINCT file FROM files").all();
1122
+ for (const { file } of files) {
1123
+ const pkg = _IndexStore.derivePackage(file) ?? "(root)";
1124
+ const node = pkgNodes.get(pkg);
1125
+ if (node) node.fileCount = (node.fileCount ?? 0) + 1;
1126
+ }
1127
+ const refRows = this.db.prepare(
1128
+ `SELECT r.from_id, r.to_id, r.call_type
1129
+ FROM refs r
1130
+ WHERE r.to_id IS NOT NULL`
1131
+ ).all();
1132
+ const edgeMap = /* @__PURE__ */ new Map();
1133
+ for (const r of refRows) {
1134
+ if (r.call_type === "import") continue;
1135
+ const fromPkg = symbolToPkg.get(r.from_id);
1136
+ const toPkg = symbolToPkg.get(r.to_id);
1137
+ if (!fromPkg || !toPkg || fromPkg === toPkg) continue;
1138
+ const key = `${fromPkg}\0${toPkg}`;
1139
+ let e = edgeMap.get(key);
1140
+ if (!e) {
1141
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
1142
+ edgeMap.set(key, e);
1143
+ }
1144
+ e.weight++;
1145
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
1146
+ }
1147
+ const importRows = this.db.prepare(
1148
+ `SELECT r.from_id, r.to_name
1149
+ FROM refs r
1150
+ WHERE r.call_type = 'import'`
1151
+ ).all();
1152
+ for (const r of importRows) {
1153
+ const fromPkg = symbolToPkg.get(r.from_id);
1154
+ const toPkg = _IndexStore.packageFromImport(r.to_name);
1155
+ if (!fromPkg || !toPkg || fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
1156
+ const key = `${fromPkg}\0${toPkg}`;
1157
+ let edge = edgeMap.get(key);
1158
+ if (!edge) {
1159
+ edge = { weight: 0, types: /* @__PURE__ */ new Map() };
1160
+ edgeMap.set(key, edge);
1161
+ }
1162
+ edge.weight++;
1163
+ edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
1164
+ }
1165
+ const edges = [];
1166
+ for (const [key, e] of edgeMap) {
1167
+ const [source, target] = key.split("\0");
1168
+ let bestType = "call";
1169
+ let bestCount = 0;
1170
+ for (const [t, c] of e.types) {
1171
+ if (c > bestCount) {
1172
+ bestType = t;
1173
+ bestCount = c;
1174
+ }
1175
+ }
1176
+ edges.push({
1177
+ source: `pkg:${source}`,
1178
+ target: `pkg:${target}`,
1179
+ weight: e.weight,
1180
+ refType: bestType
1181
+ });
1182
+ }
1183
+ return { nodes: [...pkgNodes.values()], edges };
1184
+ }
1185
+ /**
1186
+ * File-level graph for a single package: each file is a node; edges are
1187
+ * derived from cross-file symbol references within the package.
1188
+ */
1189
+ getFileGraph(packageFilter) {
1190
+ const allSymbols = this.db.prepare("SELECT file, id, name, kind, lang, line FROM symbols ORDER BY id").all();
1191
+ const pkgSyms = allSymbols.filter(
1192
+ (s) => (_IndexStore.derivePackage(s.file) ?? "(root)") === packageFilter
1193
+ );
1194
+ if (pkgSyms.length === 0) return { nodes: [], edges: [] };
1195
+ const fileNodes = /* @__PURE__ */ new Map();
1196
+ const symToFile = /* @__PURE__ */ new Map();
1197
+ const fileStats = /* @__PURE__ */ new Map();
1198
+ for (const s of allSymbols) {
1199
+ symToFile.set(s.id, s.file);
1200
+ const current = fileStats.get(s.file);
1201
+ fileStats.set(s.file, {
1202
+ count: (current?.count ?? 0) + 1,
1203
+ lang: current?.lang ?? s.lang
1204
+ });
1205
+ }
1206
+ const localFiles = new Set(pkgSyms.map((s) => s.file));
1207
+ const indexedFiles = new Set(allSymbols.map((s) => s.file));
1208
+ const ensureFileNode = (file) => {
1209
+ if (fileNodes.has(file)) return;
1210
+ const stats = fileStats.get(file);
1211
+ fileNodes.set(file, {
1212
+ id: `file:${file}`,
1213
+ label: file.replace(/\\/g, "/").split("/").pop() ?? file,
1214
+ kind: "file",
1215
+ package: _IndexStore.derivePackage(file) ?? "(root)",
1216
+ file,
1217
+ symbolCount: stats?.count ?? 0,
1218
+ lang: stats?.lang,
1219
+ external: !localFiles.has(file)
1220
+ });
1221
+ };
1222
+ for (const file of localFiles) {
1223
+ ensureFileNode(file);
1224
+ }
1225
+ const refRows = this.db.prepare(
1226
+ `SELECT r.from_id, r.to_id, r.call_type
1227
+ FROM refs r
1228
+ WHERE r.to_id IS NOT NULL`
1229
+ ).all();
1230
+ const edgeMap = /* @__PURE__ */ new Map();
1231
+ for (const r of refRows) {
1232
+ if (r.call_type === "import") continue;
1233
+ const fromFile = symToFile.get(r.from_id);
1234
+ const toFile = symToFile.get(r.to_id);
1235
+ if (!fromFile || !toFile || fromFile === toFile) continue;
1236
+ if (!localFiles.has(fromFile) && !localFiles.has(toFile)) continue;
1237
+ ensureFileNode(fromFile);
1238
+ ensureFileNode(toFile);
1239
+ const key = `${fromFile}\0${toFile}`;
1240
+ let e = edgeMap.get(key);
1241
+ if (!e) {
1242
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
1243
+ edgeMap.set(key, e);
1244
+ }
1245
+ e.weight++;
1246
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
1247
+ }
1248
+ const importRows = this.db.prepare(
1249
+ `SELECT r.from_id, r.to_name
1250
+ FROM refs r
1251
+ WHERE r.call_type = 'import'`
1252
+ ).all();
1253
+ for (const r of importRows) {
1254
+ const fromFile = symToFile.get(r.from_id);
1255
+ if (!fromFile || !localFiles.has(fromFile)) continue;
1256
+ const toFile = _IndexStore.resolveRelativeImport(fromFile, r.to_name, indexedFiles);
1257
+ if (!toFile || fromFile === toFile) continue;
1258
+ ensureFileNode(fromFile);
1259
+ ensureFileNode(toFile);
1260
+ const key = `${fromFile}\0${toFile}`;
1261
+ let edge = edgeMap.get(key);
1262
+ if (!edge) {
1263
+ edge = { weight: 0, types: /* @__PURE__ */ new Map() };
1264
+ edgeMap.set(key, edge);
1265
+ }
1266
+ edge.weight++;
1267
+ edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
1268
+ }
1269
+ const edges = [];
1270
+ for (const [key, e] of edgeMap) {
1271
+ const [source, target] = key.split("\0");
1272
+ let bestType = "call";
1273
+ let bestCount = 0;
1274
+ for (const [t, c] of e.types) {
1275
+ if (c > bestCount) {
1276
+ bestType = t;
1277
+ bestCount = c;
1278
+ }
1279
+ }
1280
+ edges.push({
1281
+ source: `file:${source}`,
1282
+ target: `file:${target}`,
1283
+ weight: e.weight,
1284
+ refType: bestType
1285
+ });
1286
+ }
1287
+ return { nodes: [...fileNodes.values()], edges };
1288
+ }
1289
+ /**
1290
+ * Symbol-level graph for a single file: each symbol is a node; edges are
1291
+ * derived from intra-file and cross-file symbol references (who calls whom).
1292
+ */
1293
+ getSymbolGraph(fileFilter) {
1294
+ const allSymbols = this.db.prepare(
1295
+ "SELECT id, name, kind, lang, file, line, signature, scope FROM symbols ORDER BY file, line, id"
1296
+ ).all();
1297
+ const syms = allSymbols.filter((symbol) => symbol.file === fileFilter);
1298
+ if (syms.length === 0) return { nodes: [], edges: [] };
1299
+ const symById = new Map(allSymbols.map((symbol) => [symbol.id, symbol]));
1300
+ const relatedIds = new Set(syms.map((symbol) => symbol.id));
1301
+ const toGraphNode = (s) => ({
1302
+ id: `sym:${s.id}`,
1303
+ label: s.name,
1304
+ kind: "symbol",
1305
+ symbolId: s.id,
1306
+ symbolKind: s.kind,
1307
+ file: s.file,
1308
+ package: _IndexStore.derivePackage(s.file) ?? "(root)",
1309
+ lang: s.lang,
1310
+ line: s.line,
1311
+ signature: s.signature,
1312
+ scope: s.scope,
1313
+ external: s.file !== fileFilter
1314
+ });
1315
+ const symIds = new Set(syms.map((s) => s.id));
1316
+ const refRows = this.db.prepare(
1317
+ `SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
1318
+ FROM refs r
1319
+ WHERE r.from_id IN (SELECT id FROM symbols WHERE file = ?)
1320
+ OR r.to_id IN (SELECT id FROM symbols WHERE file = ?)`
1321
+ ).all(fileFilter, fileFilter);
1322
+ const edgeMap = /* @__PURE__ */ new Map();
1323
+ for (const r of refRows) {
1324
+ if (r.to_id === null) continue;
1325
+ if (!symIds.has(r.from_id) && !symIds.has(r.to_id)) continue;
1326
+ relatedIds.add(r.from_id);
1327
+ relatedIds.add(r.to_id);
1328
+ const key = `${r.from_id}\0${r.to_id}`;
1329
+ let e = edgeMap.get(key);
1330
+ if (!e) {
1331
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
1332
+ edgeMap.set(key, e);
1333
+ }
1334
+ e.weight++;
1335
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
1336
+ }
1337
+ const edges = [];
1338
+ for (const [key, e] of edgeMap) {
1339
+ const [fromId, toId] = key.split("\0").map(Number);
1340
+ let bestType = "call";
1341
+ let bestCount = 0;
1342
+ for (const [t, c] of e.types) {
1343
+ if (c > bestCount) {
1344
+ bestType = t;
1345
+ bestCount = c;
1346
+ }
1347
+ }
1348
+ edges.push({
1349
+ source: `sym:${fromId}`,
1350
+ target: `sym:${toId}`,
1351
+ weight: e.weight,
1352
+ refType: bestType
1353
+ });
1354
+ }
1355
+ const nodes = [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
1356
+ const aExternal = a.file === fileFilter ? 0 : 1;
1357
+ const bExternal = b.file === fileFilter ? 0 : 1;
1358
+ return aExternal - bExternal || a.file.localeCompare(b.file) || a.line - b.line || a.id - b.id;
1359
+ }).map(toGraphNode);
1360
+ return { nodes, edges };
1361
+ }
953
1362
  close() {
954
1363
  this.stmtCache.clear();
955
1364
  try {
@@ -1044,6 +1453,14 @@ function getJsDoc(node, sourceFile) {
1044
1453
  }
1045
1454
  return "";
1046
1455
  }
1456
+ function hasFunctionLikeAncestor(node) {
1457
+ let current = node.parent;
1458
+ while (current) {
1459
+ if (ts.isFunctionLike(current)) return true;
1460
+ current = current.parent;
1461
+ }
1462
+ return false;
1463
+ }
1047
1464
  function buildScope(node) {
1048
1465
  const parts = [];
1049
1466
  let current = node.parent;
@@ -1071,6 +1488,10 @@ function parseSymbols(opts) {
1071
1488
  function visit(node) {
1072
1489
  const kind = kindOf(node);
1073
1490
  if (kind) {
1491
+ if ((kind === "const" || kind === "let" || kind === "var" || kind === "parameter") && hasFunctionLikeAncestor(node)) {
1492
+ ts.forEachChild(node, visit);
1493
+ return;
1494
+ }
1074
1495
  const nameNode = node.name;
1075
1496
  if (!nameNode || !ts.isIdentifier(nameNode)) return;
1076
1497
  const name = nameNode.text;
@@ -2038,9 +2459,9 @@ function parseSymbols5(opts) {
2038
2459
  function regexParse2(opts) {
2039
2460
  const { file, content, lang } = opts;
2040
2461
  const symbols = [];
2041
- const basename2 = path6.basename(file).toLowerCase();
2042
- const isPackageJson = basename2 === "package.json";
2043
- const isTsconfig = basename2 === "tsconfig.json" || basename2 === "tsconfig.build.json";
2462
+ const basename3 = path6.basename(file).toLowerCase();
2463
+ const isPackageJson = basename3 === "package.json";
2464
+ const isTsconfig = basename3 === "tsconfig.json" || basename3 === "tsconfig.build.json";
2044
2465
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
2045
2466
  const isOpenApi = content.includes("openapi") || content.includes("swagger");
2046
2467
  const lines = content.split("\n");
@@ -2442,9 +2863,7 @@ function yieldEventLoop() {
2442
2863
  function throwIfAborted(signal) {
2443
2864
  if (!signal?.aborted) return;
2444
2865
  if (signal.reason instanceof Error) throw signal.reason;
2445
- throw new Error(
2446
- typeof signal.reason === "string" ? signal.reason : "Indexing cancelled"
2447
- );
2866
+ throw new Error(typeof signal.reason === "string" ? signal.reason : "Indexing cancelled");
2448
2867
  }
2449
2868
  function isAbortError(err) {
2450
2869
  return err instanceof DOMException && err.name === "AbortError";
@@ -2460,8 +2879,20 @@ var DEFAULT_IGNORE = [
2460
2879
  "__snapshots__",
2461
2880
  ".nyc_output"
2462
2881
  ];
2882
+ var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "pnpm-lock.yml"]);
2883
+ var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
2884
+ function isWithinProject(projectRoot, file) {
2885
+ const rel = path8.relative(projectRoot, file);
2886
+ return rel !== "" && !rel.startsWith(`..${path8.sep}`) && rel !== ".." && !path8.isAbsolute(rel);
2887
+ }
2888
+ function isMissingPathError(err) {
2889
+ const code = err?.code;
2890
+ return code === "ENOENT" || code === "ENOTDIR";
2891
+ }
2463
2892
  async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
2464
2893
  const results = [];
2894
+ const errors = [];
2895
+ let complete = true;
2465
2896
  const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
2466
2897
  const globs = [
2467
2898
  { ext: ".ts", pat: compileGlob2("**/*.ts") },
@@ -2485,7 +2916,9 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
2485
2916
  let entries;
2486
2917
  try {
2487
2918
  entries = await fs7.readdir(dir, { withFileTypes: true });
2488
- } catch {
2919
+ } catch (err) {
2920
+ complete = false;
2921
+ errors.push(`scan error: ${dir}: ${err instanceof Error ? err.message : String(err)}`);
2489
2922
  return;
2490
2923
  }
2491
2924
  dirCount++;
@@ -2497,8 +2930,8 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
2497
2930
  if (isGitIgnored(rel, true)) continue;
2498
2931
  await walk(full);
2499
2932
  } else if (e.isFile()) {
2500
- if (isGitIgnored(rel, false)) continue;
2501
- const ext = path8.extname(e.name);
2933
+ if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
2934
+ const ext = path8.extname(e.name).toLowerCase();
2502
2935
  for (const { ext: extName, pat } of globs) {
2503
2936
  if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
2504
2937
  results.push(full);
@@ -2509,7 +2942,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
2509
2942
  }
2510
2943
  };
2511
2944
  await walk(projectRoot);
2512
- return results;
2945
+ return { files: results, complete, errors };
2513
2946
  }
2514
2947
  async function parseFile(file, content, lang) {
2515
2948
  switch (lang) {
@@ -2532,6 +2965,26 @@ async function parseFile(file, content, lang) {
2532
2965
  return { file, lang, symbols: [], mtimeMs: Date.now() };
2533
2966
  }
2534
2967
  }
2968
+ function assignRefsToSymbols2(refs, symbols) {
2969
+ if (refs.length === 0 || symbols.length === 0) return [];
2970
+ const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
2971
+ const seen = /* @__PURE__ */ new Set();
2972
+ const assigned = [];
2973
+ for (const ref of refs) {
2974
+ let owner;
2975
+ for (const symbol of ordered) {
2976
+ if (symbol.line > ref.line) break;
2977
+ owner = symbol;
2978
+ }
2979
+ if (!owner && ref.callType === "import") owner = ordered[0];
2980
+ if (!owner || owner.id <= 0) continue;
2981
+ const key = `${owner.id}:${ref.toName}:${ref.callType}`;
2982
+ if (seen.has(key)) continue;
2983
+ seen.add(key);
2984
+ assigned.push({ ...ref, fromId: owner.id });
2985
+ }
2986
+ return assigned;
2987
+ }
2535
2988
  async function runIndexer(_ctx, opts) {
2536
2989
  const store = new IndexStore(opts.projectRoot, { indexDir: opts.indexDir });
2537
2990
  try {
@@ -2544,7 +2997,9 @@ async function runIndexer(_ctx, opts) {
2544
2997
  }
2545
2998
  }
2546
2999
  async function runIndexerWithStore(store, opts) {
2547
- const { projectRoot, force = false, langs, ignore = [], signal } = opts;
3000
+ const { projectRoot, langs, ignore = [], signal } = opts;
3001
+ const relationGraphVersion = "2";
3002
+ const force = (opts.force ?? false) || store.getMetadata("relation_graph_version") !== relationGraphVersion;
2548
3003
  const startMs = Date.now();
2549
3004
  const errors = [];
2550
3005
  const langStats = {};
@@ -2553,10 +3008,18 @@ async function runIndexerWithStore(store, opts) {
2553
3008
  const isGitIgnored = await loadGitignoreMatcher(projectRoot);
2554
3009
  let files;
2555
3010
  let discoveredFiles = null;
3011
+ let discoveryComplete = true;
2556
3012
  if (opts.files && opts.files.length > 0) {
2557
- files = opts.files.map((f) => path8.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path8.relative(projectRoot, f).replace(/\\/g, "/"), false));
3013
+ files = opts.files.map((f) => path8.resolve(projectRoot, f)).filter((f) => {
3014
+ if (!isWithinProject(projectRoot, f)) return false;
3015
+ const rel = path8.relative(projectRoot, f).replace(/\\/g, "/");
3016
+ return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path8.basename(f)) && !isGitIgnored(rel, false);
3017
+ });
2558
3018
  } else {
2559
- files = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
3019
+ const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
3020
+ files = discovery.files;
3021
+ errors.push(...discovery.errors);
3022
+ discoveryComplete = discovery.complete;
2560
3023
  discoveredFiles = new Set(files);
2561
3024
  }
2562
3025
  if (langs && langs.length > 0) {
@@ -2581,36 +3044,66 @@ async function runIndexerWithStore(store, opts) {
2581
3044
  }
2582
3045
  const statOpts = signal ? { signal } : {};
2583
3046
  const statReadParse = await Promise.allSettled(
2584
- batchFiles.map(async (file) => {
2585
- let stat2;
2586
- try {
2587
- stat2 = await fs7.stat(file, statOpts);
2588
- } catch (e) {
2589
- if (isAbortError(e)) throw e;
2590
- return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
2591
- }
2592
- if (!stat2.isFile()) return { file, stat: stat2, lang: "", parsed: null };
2593
- const lang = detectLang(file);
2594
- if (!lang) return { file, stat: stat2, lang: "", parsed: null };
2595
- const meta = existingMeta.get(file);
2596
- if (!force && meta && meta.mtimeMs === Math.floor(stat2.mtimeMs)) {
2597
- return { file, stat: stat2, lang, parsed: null, skippedMeta: meta };
2598
- }
2599
- let content;
2600
- try {
2601
- content = await fs7.readFile(file, { encoding: "utf8", signal });
2602
- } catch (e) {
2603
- if (isAbortError(e)) throw e;
2604
- return { file, stat: stat2, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
2605
- }
2606
- let parsed;
2607
- try {
2608
- parsed = await parseFile(file, content, lang);
2609
- } catch (e) {
2610
- return { file, stat: stat2, lang, parsed: null, error: `parse error: ${e instanceof Error ? e.message : String(e)}` };
3047
+ batchFiles.map(
3048
+ async (file) => {
3049
+ let stat2;
3050
+ try {
3051
+ stat2 = await fs7.stat(file, statOpts);
3052
+ } catch (e) {
3053
+ if (isAbortError(e)) throw e;
3054
+ return {
3055
+ file,
3056
+ stat: null,
3057
+ lang: "",
3058
+ parsed: null,
3059
+ error: `stat error: ${e instanceof Error ? e.message : String(e)}`,
3060
+ missing: isMissingPathError(e)
3061
+ };
3062
+ }
3063
+ if (!stat2.isFile()) return { file, stat: stat2, lang: "", parsed: null };
3064
+ const lang = detectLang(file);
3065
+ if (!lang) return { file, stat: stat2, lang: "", parsed: null };
3066
+ if (stat2.size > MAX_INDEX_FILE_BYTES) {
3067
+ return {
3068
+ file,
3069
+ stat: stat2,
3070
+ lang,
3071
+ parsed: null,
3072
+ error: `file too large (${stat2.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
3073
+ };
3074
+ }
3075
+ const meta = existingMeta.get(file);
3076
+ if (!force && meta && meta.mtimeMs === Math.floor(stat2.mtimeMs)) {
3077
+ return { file, stat: stat2, lang, parsed: null, skippedMeta: meta };
3078
+ }
3079
+ let content;
3080
+ try {
3081
+ content = await fs7.readFile(file, { encoding: "utf8", signal });
3082
+ } catch (e) {
3083
+ if (isAbortError(e)) throw e;
3084
+ return {
3085
+ file,
3086
+ stat: stat2,
3087
+ lang,
3088
+ parsed: null,
3089
+ error: `read error: ${e instanceof Error ? e.message : String(e)}`
3090
+ };
3091
+ }
3092
+ let parsed;
3093
+ try {
3094
+ parsed = await parseFile(file, content, lang);
3095
+ } catch (e) {
3096
+ return {
3097
+ file,
3098
+ stat: stat2,
3099
+ lang,
3100
+ parsed: null,
3101
+ error: `parse error: ${e instanceof Error ? e.message : String(e)}`
3102
+ };
3103
+ }
3104
+ return { file, stat: stat2, lang, parsed, content };
2611
3105
  }
2612
- return { file, stat: stat2, lang, parsed, content };
2613
- })
3106
+ )
2614
3107
  );
2615
3108
  const batchEntries = [];
2616
3109
  const deleteForFiles = [];
@@ -2625,8 +3118,8 @@ async function runIndexerWithStore(store, opts) {
2625
3118
  }
2626
3119
  const result = settled.value;
2627
3120
  if (result.error) {
2628
- if (result.stat) store.deleteFile(file);
2629
- if (result.error.includes("error:")) errors.push(result.error);
3121
+ if (result.missing) store.deleteFile(file);
3122
+ errors.push(`${file}: ${result.error}`);
2630
3123
  continue;
2631
3124
  }
2632
3125
  const { stat: stat2, lang, parsed } = result;
@@ -2650,6 +3143,8 @@ async function runIndexerWithStore(store, opts) {
2650
3143
  continue;
2651
3144
  }
2652
3145
  if (parsed.symbols.length === 0) {
3146
+ store.deleteRefsForFile(file);
3147
+ store.deleteSymbolsForFile(file);
2653
3148
  store.upsertFile({
2654
3149
  file,
2655
3150
  lang,
@@ -2660,15 +3155,11 @@ async function runIndexerWithStore(store, opts) {
2660
3155
  filesIndexed++;
2661
3156
  continue;
2662
3157
  }
2663
- const refs = [];
2664
- if (parsed.refs && parsed.refs.length > 0) {
2665
- for (const r of parsed.refs) refs.push({ ...r, fromId: 0 });
2666
- }
2667
3158
  batchEntries.push({
2668
3159
  file,
2669
3160
  lang,
2670
3161
  symbols: parsed.symbols,
2671
- refs,
3162
+ refs: parsed.refs ?? [],
2672
3163
  mtimeMs: Math.floor(stat2.mtimeMs),
2673
3164
  symbolCount: parsed.symbols.length
2674
3165
  });
@@ -2676,34 +3167,12 @@ async function runIndexerWithStore(store, opts) {
2676
3167
  }
2677
3168
  if (batchEntries.length > 0) {
2678
3169
  try {
2679
- const inserted = store.commitBatch(batchEntries, { deleteForFiles });
2680
- let cursor = 0;
3170
+ store.commitBatch(batchEntries, { deleteForFiles });
2681
3171
  for (const entry of batchEntries) {
2682
3172
  const count = entry.symbols.length;
2683
- const symbolsWithIds = inserted.slice(cursor, cursor + count);
2684
- cursor += count;
2685
3173
  symbolsIndexed += count;
2686
3174
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
2687
3175
  filesIndexed++;
2688
- if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
2689
- const refsByLine = /* @__PURE__ */ new Map();
2690
- for (let i = 0; i < symbolsWithIds.length; i++) {
2691
- const sym = symbolsWithIds[i];
2692
- let arr = refsByLine.get(sym.line);
2693
- if (!arr) {
2694
- arr = [];
2695
- refsByLine.set(sym.line, arr);
2696
- }
2697
- arr.push(i);
2698
- }
2699
- for (const ref of entry.refs) {
2700
- const indices = refsByLine.get(ref.line);
2701
- if (indices && indices.length > 0) {
2702
- const idx = indices.shift();
2703
- ref.fromId = symbolsWithIds[idx].id;
2704
- }
2705
- }
2706
- }
2707
3176
  }
2708
3177
  } catch (err) {
2709
3178
  const message = err instanceof Error ? err.message : String(err);
@@ -2717,23 +3186,7 @@ async function runIndexerWithStore(store, opts) {
2717
3186
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
2718
3187
  filesIndexed++;
2719
3188
  if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
2720
- const refsByLine = /* @__PURE__ */ new Map();
2721
- for (const sym of symbolsWithIds) {
2722
- let arr = refsByLine.get(sym.line);
2723
- if (!arr) {
2724
- arr = [];
2725
- refsByLine.set(sym.line, arr);
2726
- }
2727
- arr.push(sym);
2728
- }
2729
- const fallbackBatch = [];
2730
- for (const ref of entry.refs) {
2731
- const syms = refsByLine.get(ref.line);
2732
- if (syms && syms.length > 0) {
2733
- const sym = syms.shift();
2734
- fallbackBatch.push({ ...ref, fromId: sym.id });
2735
- }
2736
- }
3189
+ const fallbackBatch = assignRefsToSymbols2(entry.refs, symbolsWithIds);
2737
3190
  if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
2738
3191
  }
2739
3192
  store.upsertFile({
@@ -2744,19 +3197,23 @@ async function runIndexerWithStore(store, opts) {
2744
3197
  lastIndexed: Date.now()
2745
3198
  });
2746
3199
  } catch (innerErr) {
2747
- errors.push(`fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`);
3200
+ errors.push(
3201
+ `fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
3202
+ );
2748
3203
  }
2749
3204
  }
2750
3205
  }
2751
3206
  }
2752
3207
  }
2753
- if (discoveredFiles) {
3208
+ if (discoveredFiles && discoveryComplete) {
2754
3209
  for (const [file_] of existingMeta) {
2755
3210
  if (!discoveredFiles.has(file_)) {
2756
3211
  store.deleteFile(file_);
2757
3212
  }
2758
3213
  }
2759
3214
  }
3215
+ store.resolveRefs();
3216
+ store.setMetadata("relation_graph_version", relationGraphVersion);
2760
3217
  const durationMs = Date.now() - startMs;
2761
3218
  store.setLastIndexed(Date.now());
2762
3219
  return {
@@ -2816,11 +3273,35 @@ function statsService(args) {
2816
3273
  store.close();
2817
3274
  }
2818
3275
  }
3276
+ function packageGraphService(args) {
3277
+ const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
3278
+ try {
3279
+ return store.getPackageGraph();
3280
+ } finally {
3281
+ store.close();
3282
+ }
3283
+ }
3284
+ function fileGraphService(args) {
3285
+ const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
3286
+ try {
3287
+ return store.getFileGraph(args.packageFilter);
3288
+ } finally {
3289
+ store.close();
3290
+ }
3291
+ }
3292
+ function symbolGraphService(args) {
3293
+ const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
3294
+ try {
3295
+ return store.getSymbolGraph(args.fileFilter);
3296
+ } finally {
3297
+ store.close();
3298
+ }
3299
+ }
2819
3300
 
2820
3301
  // src/codebase-index/background-indexer.ts
2821
3302
  var DEFAULT_FULL_INDEX_TIMEOUT_MS = 24e4;
2822
3303
  var DEFAULT_INCREMENTAL_TIMEOUT_MS = 6e4;
2823
- var DEFAULT_QUERY_TIMEOUT_MS = 8e3;
3304
+ var DEFAULT_QUERY_TIMEOUT_MS = 3e4;
2824
3305
  var _ready = false;
2825
3306
  var _indexing = false;
2826
3307
  var _currentFile = 0;
@@ -2898,14 +3379,22 @@ function ensureWorker() {
2898
3379
  if (!entry) return;
2899
3380
  pending.delete(msg.id);
2900
3381
  if (msg.ok) entry.resolve(msg.result);
2901
- else entry.reject(new Error(msg.error));
3382
+ else {
3383
+ const error = msg.errorName === "LockError" ? new LockError(msg.error) : new Error(msg.error);
3384
+ if (msg.errorName && msg.errorName !== "Error") {
3385
+ error.name = msg.errorName;
3386
+ }
3387
+ entry.reject(error);
3388
+ }
2902
3389
  });
2903
3390
  w.on("error", (err) => {
3391
+ if (worker !== w) return;
2904
3392
  worker = null;
2905
3393
  failAllPending(err);
2906
3394
  });
2907
3395
  w.on("exit", () => {
2908
- if (worker === w) worker = null;
3396
+ if (worker !== w) return;
3397
+ worker = null;
2909
3398
  failAllPending(new Error("codebase-index worker exited"));
2910
3399
  });
2911
3400
  worker = w;
@@ -2930,6 +3419,11 @@ function shutdownCodebaseIndexHost() {
2930
3419
  function callIndexOp(op, args, opts) {
2931
3420
  const w = ensureWorker();
2932
3421
  if (!w) return callInline(op, args, opts);
3422
+ if (opts.signal?.aborted) {
3423
+ return Promise.reject(
3424
+ opts.signal.reason instanceof Error ? opts.signal.reason : new Error("Indexing cancelled")
3425
+ );
3426
+ }
2933
3427
  return new Promise((resolve2, reject) => {
2934
3428
  const id = nextRpcId++;
2935
3429
  const timer = setTimeout(() => {
@@ -2944,8 +3438,7 @@ function callIndexOp(op, args, opts) {
2944
3438
  const onAbort = () => {
2945
3439
  w.postMessage({ type: "cancel", id });
2946
3440
  };
2947
- if (opts.signal?.aborted) onAbort();
2948
- else opts.signal?.addEventListener("abort", onAbort, { once: true });
3441
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
2949
3442
  const cleanup = () => {
2950
3443
  clearTimeout(timer);
2951
3444
  opts.signal?.removeEventListener("abort", onAbort);
@@ -3133,7 +3626,7 @@ var codebaseIndexTool = {
3133
3626
  category: "Project",
3134
3627
  icon: "index",
3135
3628
  description: "Build or incrementally update the project-wide symbol index. This powers fast codebase search and understanding. By default it only processes files that have changed since the last indexing run.",
3136
- usageHint: "IMPORTANT FOR LARGE CODEBASES:\n\n- First run (or after major changes): consider `force: true` for a clean rebuild.\n- Normal usage: call without arguments for fast incremental updates.\n- Use `langs` to restrict to specific languages if you only care about certain parts of the project.\nThis tool is relatively expensive \u2014 do not call it on every turn. Use it when the index is stale or before heavy codebase-search sessions.",
3629
+ usageHint: "CREATE OR REFRESH THE SEARCH INDEX:\n\n- When `codebase-stats` or `codebase-search` reports no persisted index, call this without arguments, then retry the search.\n- Normal and first-time usage: call without arguments for an incremental build.\n- Use `force: true` only for a corrupt/stale index or an explicitly requested clean rebuild.\n- Use `langs` to restrict to specific languages if you only care about certain parts of the project.\nThis tool is relatively expensive \u2014 do not call it on every turn. Use it when the index is stale or before heavy codebase-search sessions.",
3137
3630
  permission: "confirm",
3138
3631
  mutating: true,
3139
3632
  capabilities: ["fs.write.outside-project"],
@@ -3189,12 +3682,14 @@ var codebaseSearchTool = {
3189
3682
  name: "codebase-search",
3190
3683
  category: "Project",
3191
3684
  icon: "index",
3192
- description: "Search code symbols using a fast SQLite+BM25 index, with optional LSP fallback. Much more powerful and structured than raw `grep` for finding code by name or concept. Set `preferLsp: true` for live precision when the LSP plugin is active (supersedes codebase-lsp-search).",
3193
- usageHint: "PREFERRED FOR CODE UNDERSTANDING:\n\n- Use when you need to find where something is defined or used by name.\n- `kind` filter is very useful (e.g. only functions or only interfaces).\n- Combine with `file` filter to scope to a specific directory or module.\nThis is generally better than `grep` when you are looking for symbols rather than arbitrary text patterns.",
3685
+ description: "Search code symbols using a fast SQLite+BM25 index, with optional LSP fallback. Prefer this before broad `tree`, `glob`, or `grep` exploration when finding code by name or concept. Set `preferLsp: true` for live precision when the LSP plugin is active (supersedes codebase-lsp-search).",
3686
+ usageHint: "FIRST CHOICE FOR INDEXABLE CODE UNDERSTANDING:\n\n- Call before broad `tree`, `glob`, or `grep` exploration when locating symbols, concepts, definitions, or candidate modules.\n- `kind` filter is very useful (e.g. only functions or only interfaces).\n- Combine with `file` filter to scope to a specific directory or module.\n- If `indexStatus` reports no persisted data, run `codebase-index` and retry.\nUse `grep` afterwards for exact text, regexes, unsupported content, or concrete usage sites.",
3194
3687
  permission: "auto",
3195
3688
  mutating: false,
3196
3689
  capabilities: ["fs.read"],
3197
- timeoutMs: 1e4,
3690
+ // The index host has its own 30s read watchdog. Leave enough headroom for
3691
+ // worker teardown and structured timeout reporting.
3692
+ timeoutMs: 35e3,
3198
3693
  inputSchema: {
3199
3694
  type: "object",
3200
3695
  properties: {
@@ -3204,11 +3699,34 @@ var codebaseSearchTool = {
3204
3699
  },
3205
3700
  kind: {
3206
3701
  type: "string",
3207
- description: "Filter by symbol kind: class, function, interface, method, const, let, var, property, type, enum"
3702
+ enum: [
3703
+ "class",
3704
+ "interface",
3705
+ "enum",
3706
+ "type",
3707
+ "function",
3708
+ "method",
3709
+ "var",
3710
+ "const",
3711
+ "let",
3712
+ "property",
3713
+ "parameter",
3714
+ "namespace",
3715
+ "object",
3716
+ "literal",
3717
+ "schema",
3718
+ "struct",
3719
+ "trait",
3720
+ "impl",
3721
+ "static",
3722
+ "mod"
3723
+ ],
3724
+ description: "Filter by indexed symbol kind"
3208
3725
  },
3209
3726
  lang: {
3210
3727
  type: "string",
3211
- description: "Filter by language: ts, tsx, js, jsx"
3728
+ enum: ["ts", "tsx", "js", "jsx", "go", "py", "rs", "json", "yaml"],
3729
+ description: "Filter by indexed language"
3212
3730
  },
3213
3731
  lspKind: {
3214
3732
  type: "integer",
@@ -3233,20 +3751,12 @@ var codebaseSearchTool = {
3233
3751
  },
3234
3752
  async execute(input, ctx, execOpts) {
3235
3753
  const state = getIndexState();
3236
- if (!state.ready) {
3237
- return {
3238
- results: [],
3239
- total: 0,
3240
- query: input.query,
3241
- indexStatus: state.indexing ? `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.` : "Index not yet built. The codebase is being indexed at startup \u2014 search will be available shortly."
3242
- };
3243
- }
3244
- if (state.indexing) {
3754
+ if (state.indexing && !state.ready) {
3245
3755
  return {
3246
3756
  results: [],
3247
3757
  total: 0,
3248
3758
  query: input.query,
3249
- indexStatus: `Index refresh in progress (${state.currentFile}/${state.totalFiles} files). Results may be incomplete.`
3759
+ indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
3250
3760
  };
3251
3761
  }
3252
3762
  if (state.lastError) {
@@ -3259,21 +3769,37 @@ var codebaseSearchTool = {
3259
3769
  indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
3260
3770
  };
3261
3771
  }
3262
- const limit = Math.min(input.limit ?? 20, 100);
3772
+ const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 20), 100));
3263
3773
  const { results, total } = await searchCodebaseIndex(
3264
3774
  {
3265
3775
  projectRoot: ctx.projectRoot,
3266
3776
  indexDir: codebaseIndexDirOverride(ctx),
3267
3777
  query: input.query,
3268
- kind: input.kind,
3269
- lang: input.lang,
3778
+ kind: input.kind?.toLowerCase(),
3779
+ lang: input.lang?.toLowerCase(),
3270
3780
  file: input.file,
3271
3781
  lspKind: input.lspKind,
3272
3782
  limit
3273
3783
  },
3274
3784
  { signal: execOpts?.signal }
3275
3785
  );
3276
- return { results, total, query: input.query };
3786
+ let hasPersistedIndex = state.ready || total > 0;
3787
+ if (!hasPersistedIndex) {
3788
+ try {
3789
+ const stats = await codebaseIndexStats(
3790
+ { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
3791
+ { signal: execOpts?.signal }
3792
+ );
3793
+ hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
3794
+ } catch {
3795
+ }
3796
+ }
3797
+ return {
3798
+ results,
3799
+ total,
3800
+ query: input.query,
3801
+ ...hasPersistedIndex ? {} : { indexStatus: "No persisted index data found. Run codebase-index to build it." }
3802
+ };
3277
3803
  }
3278
3804
  };
3279
3805
 
@@ -3282,12 +3808,15 @@ var codebaseStatsTool = {
3282
3808
  name: "codebase-stats",
3283
3809
  category: "Project",
3284
3810
  icon: "index",
3285
- description: "Return health and statistics about the current symbol index (total symbols, files, language/kind breakdown, size, last update). Useful to decide whether to re-index.",
3286
- usageHint: "CALL BEFORE HEAVY CODEBASE-SEARCH WORK:\n\n- Use to see if the index is up-to-date or needs a refresh.\n- No arguments required.\n- Helps avoid wasting tokens on searches against a stale index.\nLightweight and safe to call frequently.",
3811
+ description: "Check whether a persisted codebase index exists and report its health and statistics (symbols, files, language/kind breakdown, size, last update).",
3812
+ usageHint: "CHECK ONCE BEFORE BROAD CODE EXPLORATION:\n\n- Call before broad `tree`, `glob`, or `grep` exploration to see whether index-backed search is available.\n- If it reports no persisted data, run `codebase-index`, then use `codebase-search`.\n- No arguments required.\nLightweight and safe to call frequently.",
3287
3813
  permission: "auto",
3288
3814
  mutating: false,
3289
3815
  capabilities: ["fs.read"],
3290
- timeoutMs: 5e3,
3816
+ // The index host has its own 30s read watchdog. Keep the outer tool budget
3817
+ // longer so the host can return a structured timeout instead of being
3818
+ // pre-empted by the generic tool executor.
3819
+ timeoutMs: 35e3,
3291
3820
  inputSchema: {
3292
3821
  type: "object",
3293
3822
  properties: {},
@@ -3295,7 +3824,8 @@ var codebaseStatsTool = {
3295
3824
  },
3296
3825
  async execute(_input, ctx, execOpts) {
3297
3826
  const idxState = getIndexState();
3298
- if (!idxState.ready) {
3827
+ const indexPath = resolveIndexDir(ctx.projectRoot, codebaseIndexDirOverride(ctx));
3828
+ if (idxState.indexing) {
3299
3829
  return {
3300
3830
  totalSymbols: 0,
3301
3831
  totalFiles: 0,
@@ -3303,22 +3833,47 @@ var codebaseStatsTool = {
3303
3833
  byKind: {},
3304
3834
  lastIndexed: null,
3305
3835
  sizeBytes: 0,
3306
- indexPath: "",
3836
+ indexPath,
3307
3837
  version: SCHEMA_VERSION,
3308
- indexStatus: idxState.indexing ? `Indexing in progress (${idxState.currentFile}/${idxState.totalFiles} files).` : "Index not yet built."
3838
+ statsAvailable: false,
3839
+ indexing: {
3840
+ currentFile: idxState.currentFile,
3841
+ totalFiles: idxState.totalFiles
3842
+ },
3843
+ indexStatus: idxState.ready ? `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). The persisted snapshot remains available to codebase-search; retry stats after refresh.` : `Startup indexing in progress (${idxState.currentFile}/${idxState.totalFiles} files). Do not start another index; retry stats or codebase-search after completion.`
3309
3844
  };
3310
3845
  }
3311
- const stats = await codebaseIndexStats(
3312
- { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
3313
- { signal: execOpts?.signal }
3314
- );
3315
- if (idxState.indexing) {
3846
+ let stats;
3847
+ try {
3848
+ stats = await codebaseIndexStats(
3849
+ { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
3850
+ { signal: execOpts?.signal }
3851
+ );
3852
+ } catch (err) {
3853
+ if (!(err instanceof IndexTimeoutError) && err?.name !== "IndexTimeoutError") {
3854
+ throw err;
3855
+ }
3316
3856
  return {
3317
- ...stats,
3318
- indexStatus: `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). Stats may be incomplete.`
3857
+ totalSymbols: 0,
3858
+ totalFiles: 0,
3859
+ byLang: {},
3860
+ byKind: {},
3861
+ lastIndexed: null,
3862
+ sizeBytes: 0,
3863
+ indexPath,
3864
+ version: SCHEMA_VERSION,
3865
+ statsAvailable: false,
3866
+ indexStatus: "Index statistics timed out. Do not repeatedly retry stats; try codebase-search directly or use the appropriate grep/glob/tree fallback."
3319
3867
  };
3320
3868
  }
3321
3869
  const circuit = idxState.circuit;
3870
+ const hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
3871
+ let indexStatus;
3872
+ if (circuit.state === "open") {
3873
+ indexStatus = `${hasPersistedIndex ? "Indexing" : "No persisted index data found, and indexing"} is paused after repeated failures (last: ${circuit.lastFailure ?? "unknown"}); auto-retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s, or run /codebase-reindex. ` + (hasPersistedIndex ? "Stats reflect the last successful build." : "Build the index after recovery.");
3874
+ } else if (!hasPersistedIndex) {
3875
+ indexStatus = "No persisted index data found. Run codebase-index to build it before broad code exploration.";
3876
+ }
3322
3877
  return {
3323
3878
  totalSymbols: stats.totalSymbols,
3324
3879
  totalFiles: stats.totalFiles,
@@ -3328,9 +3883,8 @@ var codebaseStatsTool = {
3328
3883
  sizeBytes: stats.sizeBytes,
3329
3884
  indexPath: stats.indexPath,
3330
3885
  version: stats.version,
3331
- ...circuit.state === "open" ? {
3332
- indexStatus: `Indexing is paused after repeated failures (last: ${circuit.lastFailure ?? "unknown"}); auto-retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s, or run /codebase-reindex. Stats reflect the last successful build.`
3333
- } : {}
3886
+ statsAvailable: true,
3887
+ ...indexStatus ? { indexStatus } : {}
3334
3888
  };
3335
3889
  }
3336
3890
  };
@@ -3349,6 +3903,7 @@ export {
3349
3903
  codebaseSearchTool,
3350
3904
  codebaseStatsTool,
3351
3905
  enqueueReindex,
3906
+ fileGraphService,
3352
3907
  getIndexState,
3353
3908
  indexCircuitBreaker,
3354
3909
  internalKindToLspKind,
@@ -3357,12 +3912,14 @@ export {
3357
3912
  isIndexing,
3358
3913
  lspKindToInternalKind,
3359
3914
  onIndexStateChange,
3915
+ packageGraphService,
3360
3916
  resetIndexCircuitBreaker,
3361
3917
  resolveIndexDir,
3362
3918
  runIndexer,
3363
3919
  runStartupIndex,
3364
3920
  searchCodebaseIndex,
3365
3921
  shutdownCodebaseIndexHost,
3922
+ symbolGraphService,
3366
3923
  tokenise
3367
3924
  };
3368
3925
  //# sourceMappingURL=index.js.map