@wrongstack/tools 0.296.2 → 0.296.4

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 (38) hide show
  1. package/dist/browser/artifacts.d.ts +2 -0
  2. package/dist/browser/artifacts.d.ts.map +1 -1
  3. package/dist/browser/index.js +41 -0
  4. package/dist/browser/index.js.map +2 -2
  5. package/dist/builtin.js +1812 -1426
  6. package/dist/builtin.js.map +4 -4
  7. package/dist/codebase-index/background-indexer.d.ts.map +1 -1
  8. package/dist/codebase-index/dead-code-scan.d.ts +2 -2
  9. package/dist/codebase-index/dead-code-scan.d.ts.map +1 -1
  10. package/dist/codebase-index/index.d.ts +1 -0
  11. package/dist/codebase-index/index.d.ts.map +1 -1
  12. package/dist/codebase-index/index.js +1827 -1542
  13. package/dist/codebase-index/index.js.map +4 -4
  14. package/dist/codebase-index/project-server-client.d.ts +13 -1
  15. package/dist/codebase-index/project-server-client.d.ts.map +1 -1
  16. package/dist/codebase-index/project-server-endpoint.d.ts +29 -1
  17. package/dist/codebase-index/project-server-endpoint.d.ts.map +1 -1
  18. package/dist/codebase-index/project-server.js +226 -152
  19. package/dist/codebase-index/project-server.js.map +4 -4
  20. package/dist/codebase-index/refs-extractor.d.ts.map +1 -1
  21. package/dist/codebase-index/ts-parser.d.ts.map +1 -1
  22. package/dist/codebase-index/worker.js +214 -145
  23. package/dist/codebase-index/worker.js.map +3 -3
  24. package/dist/codebase-index/writer.d.ts +19 -1
  25. package/dist/codebase-index/writer.d.ts.map +1 -1
  26. package/dist/index.d.ts +2 -2
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +1851 -1462
  29. package/dist/index.js.map +4 -4
  30. package/dist/pack.js +1812 -1426
  31. package/dist/pack.js.map +4 -4
  32. package/dist/read.d.ts +31 -0
  33. package/dist/read.d.ts.map +1 -1
  34. package/dist/read.js +5465 -78
  35. package/dist/read.js.map +4 -4
  36. package/dist/tool-tier.js +1812 -1426
  37. package/dist/tool-tier.js.map +4 -4
  38. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -4896,16 +4896,16 @@ var init_legacy_bridge = __esm({
4896
4896
  });
4897
4897
 
4898
4898
  // src/codebase-index/languages.ts
4899
- import * as path14 from "node:path";
4899
+ import * as path18 from "node:path";
4900
4900
  function detectLang(file) {
4901
- const base = path14.basename(file);
4901
+ const base = path18.basename(file);
4902
4902
  const lowerBase = base.toLowerCase();
4903
4903
  if (lowerBase.endsWith(".d.ts") || lowerBase.endsWith(".d.mts") || lowerBase.endsWith(".d.cts")) {
4904
4904
  return "ts";
4905
4905
  }
4906
4906
  const special = SPECIAL_FILENAMES[lowerBase];
4907
4907
  if (special) return special;
4908
- const ext = path14.extname(base).toLowerCase();
4908
+ const ext = path18.extname(base).toLowerCase();
4909
4909
  if (!ext) return null;
4910
4910
  return EXT_TO_LANG[ext] ?? null;
4911
4911
  }
@@ -5137,11 +5137,18 @@ async function parseSymbols(opts) {
5137
5137
  } else if (ts.isHeritageClause(node)) {
5138
5138
  for (const t of node.types) {
5139
5139
  const name = getTypeName(t.expression);
5140
- if (name) refs.push({ fromId: 0, toName: name, callType: node.token === ts.SyntaxKind.ExtendsKeyword ? "inherit" : "implement", line: lineNum });
5140
+ if (name)
5141
+ refs.push({
5142
+ fromId: 0,
5143
+ toName: name,
5144
+ callType: node.token === ts.SyntaxKind.ExtendsKeyword ? "inherit" : "implement",
5145
+ line: lineNum
5146
+ });
5141
5147
  }
5142
5148
  } else if (ts.isImportDeclaration(node)) {
5143
- const moduleName = getModuleName(node);
5144
- if (moduleName) refs.push({ fromId: 0, toName: moduleName, callType: "import", line: lineNum });
5149
+ emitImportSpecifierRefs(node, refs, lineNum);
5150
+ } else if (ts.isExportDeclaration(node) && node.moduleSpecifier) {
5151
+ emitExportSpecifierRefs(node, refs, lineNum);
5145
5152
  }
5146
5153
  const scopeIdx = scopeParts.length;
5147
5154
  pushScopeName(node, scopeParts);
@@ -5157,11 +5164,6 @@ function getTypeName(name) {
5157
5164
  if (ts.isQualifiedName(name)) return `${getTypeName(name.left)}.${name.right.text}`;
5158
5165
  return "";
5159
5166
  }
5160
- function getModuleName(node) {
5161
- const moduleSpecifier = node.moduleSpecifier;
5162
- if (ts.isStringLiteral(moduleSpecifier)) return moduleSpecifier.text;
5163
- return "";
5164
- }
5165
5167
  function deduplicateRefs(refs) {
5166
5168
  const seen = /* @__PURE__ */ new Set();
5167
5169
  return refs.filter((r) => {
@@ -5171,6 +5173,44 @@ function deduplicateRefs(refs) {
5171
5173
  return true;
5172
5174
  });
5173
5175
  }
5176
+ function getImportSpecifierName(spec) {
5177
+ return spec.propertyName?.text ?? spec.name.text;
5178
+ }
5179
+ function emitImportSpecifierRefs(node, refs, lineNum) {
5180
+ const clause = node.importClause;
5181
+ if (!clause) return;
5182
+ if (clause.name) {
5183
+ refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum });
5184
+ }
5185
+ const bindings2 = clause.namedBindings;
5186
+ if (!bindings2) return;
5187
+ if (ts.isNamedImports(bindings2)) {
5188
+ for (const element of bindings2.elements) {
5189
+ refs.push({
5190
+ fromId: 0,
5191
+ toName: getImportSpecifierName(element),
5192
+ callType: "import",
5193
+ line: lineNum
5194
+ });
5195
+ }
5196
+ } else if (ts.isNamespaceImport(bindings2)) {
5197
+ refs.push({ fromId: 0, toName: bindings2.name.text, callType: "import", line: lineNum });
5198
+ }
5199
+ }
5200
+ function emitExportSpecifierRefs(node, refs, lineNum) {
5201
+ const clause = node.exportClause;
5202
+ if (clause && ts.isNamespaceExport(clause)) {
5203
+ refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum });
5204
+ return;
5205
+ }
5206
+ if (clause && ts.isNamedExports(clause)) {
5207
+ for (const element of clause.elements) {
5208
+ const originalName = element.propertyName?.text ?? element.name.text;
5209
+ refs.push({ fromId: 0, toName: originalName, callType: "import", line: lineNum });
5210
+ }
5211
+ return;
5212
+ }
5213
+ }
5174
5214
  var ts, tsLoad, kindMapCache;
5175
5215
  var init_ts_parser = __esm({
5176
5216
  "src/codebase-index/ts-parser.ts"() {
@@ -5204,10 +5244,10 @@ __export(go_parser_exports, {
5204
5244
  detectLang: () => detectLang,
5205
5245
  parseSymbols: () => parseSymbols2
5206
5246
  });
5207
- import { spawn as spawn4 } from "node:child_process";
5208
- import * as os5 from "node:os";
5209
- import * as path15 from "node:path";
5210
- import * as fs10 from "node:fs/promises";
5247
+ import { spawn as spawn5 } from "node:child_process";
5248
+ import * as os6 from "node:os";
5249
+ import * as path19 from "node:path";
5250
+ import * as fs14 from "node:fs/promises";
5211
5251
  async function parseSymbols2(opts) {
5212
5252
  const { file, content, lang } = opts;
5213
5253
  try {
@@ -5279,16 +5319,16 @@ async function syncGoParse(filePath, content, lang) {
5279
5319
  try {
5280
5320
  let scriptPath = _cachedGoScriptPath;
5281
5321
  if (!scriptPath) {
5282
- const tmpDir = await fs10.mkdtemp(path15.join(os5.tmpdir(), "ws-go-parse-"));
5283
- scriptPath = path15.join(tmpDir, "parse.go");
5284
- await fs10.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
5322
+ const tmpDir = await fs14.mkdtemp(path19.join(os6.tmpdir(), "ws-go-parse-"));
5323
+ scriptPath = path19.join(tmpDir, "parse.go");
5324
+ await fs14.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
5285
5325
  _cachedGoScriptPath = scriptPath;
5286
5326
  }
5287
5327
  const goBinary = resolveWin32Command("go");
5288
5328
  const goResult = await new Promise(
5289
5329
  (resolve17, reject) => {
5290
5330
  let settled = false;
5291
- const proc = spawn4(goBinary, ["run", scriptPath], {
5331
+ const proc = spawn5(goBinary, ["run", scriptPath], {
5292
5332
  stdio: ["pipe", "pipe", "pipe"],
5293
5333
  windowsHide: true
5294
5334
  });
@@ -5891,10 +5931,10 @@ __export(py_parser_exports, {
5891
5931
  detectLang: () => detectLang,
5892
5932
  parseSymbols: () => parseSymbols4
5893
5933
  });
5894
- import { spawn as spawn5 } from "node:child_process";
5895
- import * as fs11 from "node:fs/promises";
5896
- import * as os6 from "node:os";
5897
- import * as path16 from "node:path";
5934
+ import { spawn as spawn6 } from "node:child_process";
5935
+ import * as fs15 from "node:fs/promises";
5936
+ import * as os7 from "node:os";
5937
+ import * as path20 from "node:path";
5898
5938
  async function parseSymbols4(opts) {
5899
5939
  const { file, content, lang } = opts;
5900
5940
  try {
@@ -5916,7 +5956,7 @@ async function resolvePython() {
5916
5956
  function commandIsAvailable(command) {
5917
5957
  return new Promise((resolve17) => {
5918
5958
  let settled = false;
5919
- const proc = spawn5(command, ["--version"], {
5959
+ const proc = spawn6(command, ["--version"], {
5920
5960
  stdio: "ignore",
5921
5961
  windowsHide: true
5922
5962
  });
@@ -5938,7 +5978,7 @@ function commandIsAvailable(command) {
5938
5978
  function spawnPyParser(pyBinary, scriptPath, filePath, content) {
5939
5979
  return new Promise((resolve17, reject) => {
5940
5980
  let settled = false;
5941
- const proc = spawn5(pyBinary, [scriptPath, filePath], {
5981
+ const proc = spawn6(pyBinary, [scriptPath, filePath], {
5942
5982
  stdio: ["pipe", "pipe", "pipe"],
5943
5983
  windowsHide: true
5944
5984
  });
@@ -5972,10 +6012,10 @@ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
5972
6012
  async function syncPyParse(filePath, content, lang) {
5973
6013
  try {
5974
6014
  if (!_cachedScriptPath) {
5975
- const tmpDir = path16.join(os6.tmpdir(), "ws-py-parse");
5976
- await fs11.mkdir(tmpDir, { recursive: true });
5977
- _cachedScriptPath = path16.join(tmpDir, "parse.py");
5978
- await fs11.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
6015
+ const tmpDir = path20.join(os7.tmpdir(), "ws-py-parse");
6016
+ await fs15.mkdir(tmpDir, { recursive: true });
6017
+ _cachedScriptPath = path20.join(tmpDir, "parse.py");
6018
+ await fs15.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
5979
6019
  }
5980
6020
  cachedPyBinary ??= resolvePython();
5981
6021
  const pyBinary = await cachedPyBinary;
@@ -6229,10 +6269,10 @@ __export(rs_parser_exports, {
6229
6269
  detectLang: () => detectLang,
6230
6270
  parseSymbols: () => parseSymbols5
6231
6271
  });
6232
- import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
6233
- import { execFile, spawn as spawn6 } from "node:child_process";
6234
- import * as fs12 from "node:fs/promises";
6235
- import * as path17 from "node:path";
6272
+ import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
6273
+ import { execFile, spawn as spawn7 } from "node:child_process";
6274
+ import * as fs16 from "node:fs/promises";
6275
+ import * as path21 from "node:path";
6236
6276
  async function parseSymbols5(opts) {
6237
6277
  const { file, content, lang } = opts;
6238
6278
  const nativeAvailable = await checkNativeParser();
@@ -6254,7 +6294,7 @@ function checkNativeParser() {
6254
6294
  nativeParserAvailability ??= (async () => {
6255
6295
  try {
6256
6296
  await probe("rustc", ["--version"]);
6257
- const toolsDir = path17.join(process.cwd(), "tools");
6297
+ const toolsDir = path21.join(process.cwd(), "tools");
6258
6298
  await probe(
6259
6299
  "cargo",
6260
6300
  [
@@ -6263,7 +6303,7 @@ function checkNativeParser() {
6263
6303
  "--format-version",
6264
6304
  "1",
6265
6305
  "--manifest-path",
6266
- path17.join(toolsDir, "Cargo.toml")
6306
+ path21.join(toolsDir, "Cargo.toml")
6267
6307
  ]
6268
6308
  );
6269
6309
  return true;
@@ -6275,17 +6315,17 @@ function checkNativeParser() {
6275
6315
  }
6276
6316
  async function tryNativeParse(file, content) {
6277
6317
  try {
6278
- const toolsDir = path17.join(process.cwd(), "tools");
6279
- const crateDir = path17.join(toolsDir, "syn-parser");
6280
- const tmpFile = path17.join(crateDir, "src", "input.rs");
6281
- await fs12.writeFile(tmpFile, content, "utf8");
6318
+ const toolsDir = path21.join(process.cwd(), "tools");
6319
+ const crateDir = path21.join(toolsDir, "syn-parser");
6320
+ const tmpFile = path21.join(crateDir, "src", "input.rs");
6321
+ await fs16.writeFile(tmpFile, content, "utf8");
6282
6322
  const cargoBinary = resolveWin32Command("cargo");
6283
6323
  const result = await new Promise(
6284
6324
  (resolve17, reject) => {
6285
6325
  let settled = false;
6286
- const proc = spawn6(
6326
+ const proc = spawn7(
6287
6327
  cargoBinary,
6288
- ["run", "--manifest-path", path17.join(toolsDir, "Cargo.toml")],
6328
+ ["run", "--manifest-path", path21.join(toolsDir, "Cargo.toml")],
6289
6329
  {
6290
6330
  cwd: process.cwd(),
6291
6331
  stdio: ["pipe", "pipe", "pipe"],
@@ -6344,7 +6384,7 @@ function regexParse(opts) {
6344
6384
  let hi = lineOffsets2.length - 1;
6345
6385
  while (lo < hi) {
6346
6386
  const mid = lo + hi + 1 >>> 1;
6347
- if (expectDefined2(lineOffsets2[mid]) <= offset) lo = mid;
6387
+ if (expectDefined3(lineOffsets2[mid]) <= offset) lo = mid;
6348
6388
  else hi = mid - 1;
6349
6389
  }
6350
6390
  return lo + 1;
@@ -6356,7 +6396,7 @@ function regexParse(opts) {
6356
6396
  for (const pattern of RS_PATTERNS) {
6357
6397
  pattern.regex.lastIndex = 0;
6358
6398
  for (let match = pattern.regex.exec(content); match !== null; match = pattern.regex.exec(content)) {
6359
- const name = expectDefined2(match[1]);
6399
+ const name = expectDefined3(match[1]);
6360
6400
  const offset = match.index ?? 0;
6361
6401
  const line = lineFromOffset(offset);
6362
6402
  const col = offset - (lineOffsets2[line - 1] ?? 0);
@@ -6413,8 +6453,8 @@ __export(json_parser_exports, {
6413
6453
  detectLang: () => detectLang,
6414
6454
  parseSymbols: () => parseSymbols6
6415
6455
  });
6416
- import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
6417
- import * as path18 from "node:path";
6456
+ import { expectDefined as expectDefined4 } from "@wrongstack/core/utils";
6457
+ import * as path22 from "node:path";
6418
6458
  function parseSymbols6(opts) {
6419
6459
  const { file, content, lang } = opts;
6420
6460
  try {
@@ -6426,7 +6466,7 @@ function parseSymbols6(opts) {
6426
6466
  function regexParse2(opts) {
6427
6467
  const { file, content, lang } = opts;
6428
6468
  const symbols = [];
6429
- const basename13 = path18.basename(file).toLowerCase();
6469
+ const basename13 = path22.basename(file).toLowerCase();
6430
6470
  const isPackageJson = basename13 === "package.json";
6431
6471
  const isTsconfig = basename13 === "tsconfig.json" || basename13 === "tsconfig.build.json";
6432
6472
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
@@ -6441,22 +6481,22 @@ function regexParse2(opts) {
6441
6481
  let hi = lineOffsets2.length - 1;
6442
6482
  while (lo < hi) {
6443
6483
  const mid = lo + hi + 1 >>> 1;
6444
- if (expectDefined3(lineOffsets2[mid]) <= offset) lo = mid;
6484
+ if (expectDefined4(lineOffsets2[mid]) <= offset) lo = mid;
6445
6485
  else hi = mid - 1;
6446
6486
  }
6447
6487
  return lo + 1;
6448
6488
  }
6449
6489
  const rootMatch = content.match(/^\s*\{/m);
6450
6490
  if (rootMatch) {
6451
- const offset = expectDefined3(rootMatch.index);
6491
+ const offset = expectDefined4(rootMatch.index);
6452
6492
  const line = lineFromOffset(offset);
6453
6493
  symbols.push(
6454
6494
  makeSymbol({
6455
- name: path18.basename(file),
6495
+ name: path22.basename(file),
6456
6496
  kind: "object",
6457
6497
  line,
6458
6498
  col: 0,
6459
- signature: `"${path18.basename(file)}" = { ... }`,
6499
+ signature: `"${path22.basename(file)}" = { ... }`,
6460
6500
  file,
6461
6501
  lang
6462
6502
  })
@@ -6464,7 +6504,7 @@ function regexParse2(opts) {
6464
6504
  }
6465
6505
  const topLevelKeyRegex = /^\s*"([^"]+)"\s*:/gm;
6466
6506
  for (let match = topLevelKeyRegex.exec(content); match !== null; match = topLevelKeyRegex.exec(content)) {
6467
- const key = expectDefined3(match[1]);
6507
+ const key = expectDefined4(match[1]);
6468
6508
  const offset = match.index ?? 0;
6469
6509
  const line = lineFromOffset(offset);
6470
6510
  const col = offset - (lineOffsets2[line - 1] ?? 0);
@@ -6511,7 +6551,7 @@ function regexParse2(opts) {
6511
6551
  const defsRegex = /"\$defs"\s*:|"\$defs"\s*:/g;
6512
6552
  const defsMatch = defsRegex.exec(content);
6513
6553
  if (defsMatch !== null) {
6514
- const offset = expectDefined3(defsMatch.index);
6554
+ const offset = expectDefined4(defsMatch.index);
6515
6555
  const line = lineFromOffset(offset);
6516
6556
  symbols.push(
6517
6557
  makeSymbol({
@@ -6536,7 +6576,7 @@ function regexParse2(opts) {
6536
6576
  for (let match = pat.exec(content); match !== null; match = pat.exec(content)) {
6537
6577
  const offset = match.index ?? 0;
6538
6578
  const line = lineFromOffset(offset);
6539
- const key = match[0]?.match(/"([^"]+)"/)?.[1] ?? expectDefined3(match[0]);
6579
+ const key = match[0]?.match(/"([^"]+)"/)?.[1] ?? expectDefined4(match[0]);
6540
6580
  symbols.push(
6541
6581
  makeSymbol({
6542
6582
  name: key,
@@ -6555,12 +6595,12 @@ function regexParse2(opts) {
6555
6595
  function extractPackageScripts(content, symbols, file, lang, lineOffsets2, lineFromOffset) {
6556
6596
  const scriptsBlockRegex = /"scripts"\s*:\s*\{([^}]+)\}/g;
6557
6597
  for (let match = scriptsBlockRegex.exec(content); match !== null; match = scriptsBlockRegex.exec(content)) {
6558
- const blockContent = expectDefined3(match[0]);
6598
+ const blockContent = expectDefined4(match[0]);
6559
6599
  const blockOffset = match.index ?? 0;
6560
6600
  const scriptKeyRegex = /"(\w[\w-]*)"\s*:/g;
6561
6601
  for (let scriptMatch = scriptKeyRegex.exec(blockContent); scriptMatch !== null; scriptMatch = scriptKeyRegex.exec(blockContent)) {
6562
- const key = expectDefined3(scriptMatch[1]);
6563
- const keyOffset = blockOffset + expectDefined3(scriptMatch.index);
6602
+ const key = expectDefined4(scriptMatch[1]);
6603
+ const keyOffset = blockOffset + expectDefined4(scriptMatch.index);
6564
6604
  const line = lineFromOffset(keyOffset);
6565
6605
  symbols.push(
6566
6606
  makeSymbol({
@@ -6579,12 +6619,12 @@ function extractPackageScripts(content, symbols, file, lang, lineOffsets2, lineF
6579
6619
  function extractCompilerOptions(content, symbols, file, lang, lineOffsets2, parentLine, lineFromOffset) {
6580
6620
  const optsBlockRegex = /"compilerOptions"\s*:\s*\{([^}]+)\}/g;
6581
6621
  for (let match = optsBlockRegex.exec(content); match !== null; match = optsBlockRegex.exec(content)) {
6582
- const blockContent = expectDefined3(match[0]);
6622
+ const blockContent = expectDefined4(match[0]);
6583
6623
  const blockOffset = match.index ?? 0;
6584
6624
  const optKeyRegex = /"(\w[\w]*)"\s*:/g;
6585
6625
  for (let optMatch = optKeyRegex.exec(blockContent); optMatch !== null; optMatch = optKeyRegex.exec(blockContent)) {
6586
- const key = expectDefined3(optMatch[1]);
6587
- const keyOffset = blockOffset + expectDefined3(optMatch.index);
6626
+ const key = expectDefined4(optMatch[1]);
6627
+ const keyOffset = blockOffset + expectDefined4(optMatch.index);
6588
6628
  const line = lineFromOffset(keyOffset);
6589
6629
  if (line <= parentLine) continue;
6590
6630
  symbols.push(
@@ -6629,7 +6669,7 @@ __export(yaml_parser_exports, {
6629
6669
  detectLang: () => detectLang,
6630
6670
  parseSymbols: () => parseSymbols7
6631
6671
  });
6632
- import { expectDefined as expectDefined4, truncate } from "@wrongstack/core/utils";
6672
+ import { expectDefined as expectDefined5, truncate } from "@wrongstack/core/utils";
6633
6673
  function parseSymbols7(opts) {
6634
6674
  const { file, content, lang } = opts;
6635
6675
  try {
@@ -6651,14 +6691,14 @@ function regexParse3(opts) {
6651
6691
  let hi = lineOffsets2.length - 1;
6652
6692
  while (lo < hi) {
6653
6693
  const mid = lo + hi + 1 >>> 1;
6654
- if (expectDefined4(lineOffsets2[mid]) <= offset) lo = mid;
6694
+ if (expectDefined5(lineOffsets2[mid]) <= offset) lo = mid;
6655
6695
  else hi = mid - 1;
6656
6696
  }
6657
6697
  return lo + 1;
6658
6698
  }
6659
6699
  const anchorRegex = /&(\w[\w-]*)/g;
6660
6700
  for (let match = anchorRegex.exec(content); match !== null; match = anchorRegex.exec(content)) {
6661
- const name = expectDefined4(match[1]);
6701
+ const name = expectDefined5(match[1]);
6662
6702
  const offset = match.index ?? 0;
6663
6703
  const line = lineFromOffset(offset);
6664
6704
  const col = offset - (lineOffsets2[line - 1] ?? 0);
@@ -6676,7 +6716,7 @@ function regexParse3(opts) {
6676
6716
  }
6677
6717
  const aliasRegex = /\*(\w[\w-]*)/g;
6678
6718
  for (let match = aliasRegex.exec(content); match !== null; match = aliasRegex.exec(content)) {
6679
- const name = expectDefined4(match[1]);
6719
+ const name = expectDefined5(match[1]);
6680
6720
  const offset = match.index ?? 0;
6681
6721
  const line = lineFromOffset(offset);
6682
6722
  const col = offset - (lineOffsets2[line - 1] ?? 0);
@@ -6711,7 +6751,7 @@ function regexParse3(opts) {
6711
6751
  }
6712
6752
  const listItemRegex = /^-(\s+)([^:#\s][^:#\s]*)\s*:/gm;
6713
6753
  for (let match = listItemRegex.exec(content); match !== null; match = listItemRegex.exec(content)) {
6714
- const key = expectDefined4(match[2]);
6754
+ const key = expectDefined5(match[2]);
6715
6755
  const offset = match.index ?? 0;
6716
6756
  const line = lineFromOffset(offset);
6717
6757
  const col = offset - (lineOffsets2[line - 1] ?? 0);
@@ -6731,7 +6771,7 @@ function regexParse3(opts) {
6731
6771
  }
6732
6772
  const blockScalarRegex = /^(\s*)([^:#\s][^:#\s]*)\s*:\s*[|>](\s|$)/gm;
6733
6773
  for (let match = blockScalarRegex.exec(content); match !== null; match = blockScalarRegex.exec(content)) {
6734
- const key = expectDefined4(match[2]);
6774
+ const key = expectDefined5(match[2]);
6735
6775
  const offset = match.index ?? 0;
6736
6776
  const line = lineFromOffset(offset);
6737
6777
  const col = offset - (lineOffsets2[line - 1] ?? 0);
@@ -8766,9 +8806,50 @@ import { createReadStream } from "node:fs";
8766
8806
  import * as fs7 from "node:fs/promises";
8767
8807
  import * as path10 from "node:path";
8768
8808
  import { atomicWrite, ulid } from "@wrongstack/core/utils";
8809
+ var ARTIFACT_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
8810
+ var sweptRoots = /* @__PURE__ */ new Set();
8811
+ function sweepOldArtifacts(root) {
8812
+ if (sweptRoots.has(root)) return;
8813
+ sweptRoots.add(root);
8814
+ void (async () => {
8815
+ const cutoff = Date.now() - ARTIFACT_RETENTION_MS;
8816
+ let sessionDirs;
8817
+ try {
8818
+ const entries = await fs7.readdir(root, { withFileTypes: true });
8819
+ sessionDirs = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
8820
+ } catch {
8821
+ return;
8822
+ }
8823
+ for (const sessionDir of sessionDirs) {
8824
+ const dir = path10.join(root, sessionDir);
8825
+ let names;
8826
+ try {
8827
+ names = await fs7.readdir(dir);
8828
+ } catch {
8829
+ continue;
8830
+ }
8831
+ let removed = 0;
8832
+ for (const name of names) {
8833
+ const target = path10.join(dir, name);
8834
+ try {
8835
+ const stat18 = await fs7.stat(target);
8836
+ if (stat18.isFile() && stat18.mtimeMs < cutoff) {
8837
+ await fs7.rm(target, { force: true });
8838
+ removed++;
8839
+ }
8840
+ } catch {
8841
+ }
8842
+ }
8843
+ if (removed === names.length && names.length > 0) {
8844
+ await fs7.rmdir(dir).catch(() => void 0);
8845
+ }
8846
+ }
8847
+ })();
8848
+ }
8769
8849
  var BrowserArtifactStore = class {
8770
8850
  constructor(root) {
8771
8851
  this.root = root;
8852
+ sweepOldArtifacts(root);
8772
8853
  }
8773
8854
  root;
8774
8855
  async write(sessionId, kind, extension, mimeType, content) {
@@ -10014,10 +10095,12 @@ async function shutdownBrowserTools() {
10014
10095
  await Promise.all(active.map((manager) => manager.dispose()));
10015
10096
  }
10016
10097
 
10017
- // src/codebase-index/background-indexer.ts
10018
- import * as fs18 from "node:fs";
10019
- import { fileURLToPath as fileURLToPath3 } from "node:url";
10020
- import { Worker } from "node:worker_threads";
10098
+ // src/codebase-index/project-server-client.ts
10099
+ import { spawn as spawn4 } from "node:child_process";
10100
+ import * as fs12 from "node:fs";
10101
+ import * as net3 from "node:net";
10102
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
10103
+ import { checkUnixSocketPath } from "@wrongstack/core/utils";
10021
10104
 
10022
10105
  // src/codebase-index/circuit-breaker.ts
10023
10106
  var CircuitOpenError = class extends Error {
@@ -10102,153 +10185,18 @@ function resetIndexCircuitBreaker() {
10102
10185
  indexCircuitBreaker.reset();
10103
10186
  }
10104
10187
 
10105
- // src/codebase-index/indexer.ts
10106
- import { expectDefined as expectDefined6 } from "@wrongstack/core/utils";
10107
- import { execFile as execFile2 } from "node:child_process";
10108
- import * as fs15 from "node:fs/promises";
10109
- import { availableParallelism } from "node:os";
10110
- import * as path22 from "node:path";
10111
- import {
10112
- DEFAULT_WALK_IGNORE_DIRS,
10113
- indexParallelBatchSize,
10114
- isFrugalPerf
10115
- } from "@wrongstack/core/utils";
10116
-
10117
- // src/codebase-index/gitignore.ts
10118
- import * as fs9 from "node:fs/promises";
10119
- import * as path13 from "node:path";
10120
- import { compileGlob } from "@wrongstack/core/utils";
10121
- function globBody(glob) {
10122
- return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
10123
- }
10124
- function compileGitignore(lines) {
10125
- const rules = [];
10126
- for (const raw of lines) {
10127
- let line = raw.replace(/\r$/, "");
10128
- if (!line.trim() || line.trimStart().startsWith("#")) continue;
10129
- line = line.trim();
10130
- let negated = false;
10131
- if (line.startsWith("!")) {
10132
- negated = true;
10133
- line = line.slice(1);
10134
- }
10135
- let dirOnly = false;
10136
- if (line.endsWith("/")) {
10137
- dirOnly = true;
10138
- line = line.slice(0, -1);
10139
- }
10140
- if (!line) continue;
10141
- const anchored = line.startsWith("/") || line.includes("/");
10142
- if (line.startsWith("/")) line = line.slice(1);
10143
- const body = globBody(line);
10144
- const prefix = anchored ? "^" : "(?:^|.*/)";
10145
- rules.push({
10146
- eqOrUnder: new RegExp(`${prefix}${body}(?:/.*)?$`),
10147
- under: new RegExp(`${prefix}${body}/.*$`),
10148
- negated,
10149
- dirOnly
10150
- });
10151
- }
10152
- return (relPath, isDir) => {
10153
- const p = relPath.replace(/\\/g, "/").replace(/^\/+/, "");
10154
- let ignored = false;
10155
- for (const r of rules) {
10156
- const re = r.dirOnly && !isDir ? r.under : r.eqOrUnder;
10157
- if (re.test(p)) ignored = !r.negated;
10158
- }
10159
- return ignored;
10160
- };
10161
- }
10162
- async function loadGitignoreMatcher(projectRoot) {
10163
- let lines = [];
10164
- try {
10165
- const raw = await fs9.readFile(path13.join(projectRoot, ".gitignore"), "utf8");
10166
- lines = raw.split("\n");
10167
- } catch {
10168
- }
10169
- return compileGitignore(lines);
10170
- }
10171
-
10172
- // src/codebase-index/indexer.ts
10173
- init_languages2();
10174
-
10175
- // src/codebase-index/parser-dispatch.ts
10176
- async function parseFileContent(file, content, lang) {
10177
- switch (lang) {
10178
- case "ts":
10179
- case "tsx":
10180
- case "js":
10181
- case "jsx": {
10182
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_ts_parser(), ts_parser_exports));
10183
- return parseSymbols8({ file, content, lang });
10184
- }
10185
- case "go": {
10186
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_go_parser(), go_parser_exports));
10187
- return parseSymbols8({ file, content, lang: "go" });
10188
- }
10189
- case "py": {
10190
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_py_parser(), py_parser_exports));
10191
- return parseSymbols8({ file, content, lang: "py" });
10192
- }
10193
- case "rs": {
10194
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_rs_parser(), rs_parser_exports));
10195
- return parseSymbols8({ file, content, lang: "rs" });
10196
- }
10197
- case "json": {
10198
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_json_parser(), json_parser_exports));
10199
- return parseSymbols8({ file, content, lang: "json" });
10200
- }
10201
- case "yaml": {
10202
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_yaml_parser(), yaml_parser_exports));
10203
- return parseSymbols8({ file, content, lang: "yaml" });
10204
- }
10205
- default: {
10206
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_generic_parser(), generic_parser_exports));
10207
- return parseSymbols8({ file, content, lang });
10208
- }
10209
- }
10210
- }
10188
+ // src/codebase-index/project-server-endpoint.ts
10189
+ import { createHash as createHash4 } from "node:crypto";
10190
+ import * as fs11 from "node:fs";
10191
+ import * as os5 from "node:os";
10192
+ import * as path16 from "node:path";
10193
+ import { fileURLToPath } from "node:url";
10194
+ import { assertUnixSocketPathWithinLimit } from "@wrongstack/core/utils";
10211
10195
 
10212
10196
  // src/codebase-index/writer.ts
10213
- import { expectDefined as expectDefined5 } from "@wrongstack/core/utils";
10214
- import * as fs14 from "node:fs";
10215
- import * as path21 from "node:path";
10216
-
10217
- // src/codebase-index/schema.ts
10218
- var SCHEMA_VERSION = 3;
10219
-
10220
- // src/codebase-index/lsp-kind.ts
10221
- function lspKindToInternalKind(k) {
10222
- switch (k) {
10223
- case 5 /* Class */:
10224
- return "class";
10225
- case 6 /* Method */:
10226
- return "method";
10227
- case 7 /* Property */:
10228
- case 8 /* Field */:
10229
- return "property";
10230
- case 9 /* Constructor */:
10231
- return "class";
10232
- case 10 /* Enum */:
10233
- return "enum";
10234
- case 11 /* Interface */:
10235
- return "interface";
10236
- case 12 /* Function */:
10237
- return "function";
10238
- case 13 /* Variable */:
10239
- return "var";
10240
- case 14 /* Constant */:
10241
- return "const";
10242
- case 22 /* EnumMember */:
10243
- return "enum";
10244
- case 26 /* TypeParameter */:
10245
- return "type";
10246
- case 3 /* Namespace */:
10247
- return "namespace";
10248
- default:
10249
- return null;
10250
- }
10251
- }
10197
+ import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
10198
+ import * as fs10 from "node:fs";
10199
+ import * as path15 from "node:path";
10252
10200
 
10253
10201
  // src/codebase-index/bm25.ts
10254
10202
  var K1 = 1.5;
@@ -10339,6 +10287,42 @@ var Bm25Index = class {
10339
10287
  }
10340
10288
  };
10341
10289
 
10290
+ // src/codebase-index/lsp-kind.ts
10291
+ function lspKindToInternalKind(k) {
10292
+ switch (k) {
10293
+ case 5 /* Class */:
10294
+ return "class";
10295
+ case 6 /* Method */:
10296
+ return "method";
10297
+ case 7 /* Property */:
10298
+ case 8 /* Field */:
10299
+ return "property";
10300
+ case 9 /* Constructor */:
10301
+ return "class";
10302
+ case 10 /* Enum */:
10303
+ return "enum";
10304
+ case 11 /* Interface */:
10305
+ return "interface";
10306
+ case 12 /* Function */:
10307
+ return "function";
10308
+ case 13 /* Variable */:
10309
+ return "var";
10310
+ case 14 /* Constant */:
10311
+ return "const";
10312
+ case 22 /* EnumMember */:
10313
+ return "enum";
10314
+ case 26 /* TypeParameter */:
10315
+ return "type";
10316
+ case 3 /* Namespace */:
10317
+ return "namespace";
10318
+ default:
10319
+ return null;
10320
+ }
10321
+ }
10322
+
10323
+ // src/codebase-index/schema.ts
10324
+ var SCHEMA_VERSION = 3;
10325
+
10342
10326
  // src/codebase-index/sqlite-runtime.ts
10343
10327
  import { createRequire } from "node:module";
10344
10328
  import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
@@ -10406,99 +10390,9 @@ function runSqliteWithRetry(fn) {
10406
10390
  throw lastError;
10407
10391
  }
10408
10392
 
10409
- // src/codebase-index/writer-helpers.ts
10410
- import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core/utils";
10411
- function escapeLike(value) {
10412
- return value.replace(/[\\%_]/g, (char) => `\\${char}`);
10413
- }
10414
- function assignRefsToSymbols(refs, symbols) {
10415
- if (refs.length === 0 || symbols.length === 0) return [];
10416
- const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
10417
- const seen = /* @__PURE__ */ new Set();
10418
- const assigned = [];
10419
- for (const ref of refs) {
10420
- let owner2;
10421
- for (const symbol of ordered) {
10422
- if (symbol.line > ref.line) break;
10423
- owner2 = symbol;
10424
- }
10425
- if (!owner2 && ref.callType === "import") owner2 = ordered[0];
10426
- if (!owner2 || owner2.id <= 0) continue;
10427
- const key = `${owner2.id}:${ref.toName}:${ref.callType}`;
10428
- if (seen.has(key)) continue;
10429
- seen.add(key);
10430
- assigned.push({ ...ref, fromId: owner2.id });
10431
- }
10432
- return assigned;
10433
- }
10434
- function resolveIndexDir(projectRoot, override) {
10435
- return override ?? resolveWstackPaths2({ projectRoot }).projectCodebaseIndex;
10436
- }
10437
- function codebaseIndexDirOverride(ctx) {
10438
- const v = ctx.meta?.["codebaseIndexDir"];
10439
- return typeof v === "string" ? v : void 0;
10440
- }
10441
-
10442
- // src/codebase-index/writer-schema.ts
10443
- var METADATA_TABLE_SQL = `
10444
- CREATE TABLE IF NOT EXISTS metadata (
10445
- key TEXT PRIMARY KEY,
10446
- value TEXT NOT NULL
10447
- );
10448
- `;
10449
- var CORE_TABLES_SQL = `
10450
- CREATE TABLE IF NOT EXISTS files (
10451
- file TEXT PRIMARY KEY,
10452
- lang TEXT NOT NULL,
10453
- mtime_ms INTEGER NOT NULL,
10454
- symbol_count INTEGER NOT NULL DEFAULT 0,
10455
- last_indexed INTEGER NOT NULL
10456
- );
10457
- CREATE TABLE IF NOT EXISTS symbols (
10458
- id INTEGER PRIMARY KEY,
10459
- lang TEXT NOT NULL,
10460
- kind TEXT NOT NULL,
10461
- name TEXT NOT NULL,
10462
- file TEXT NOT NULL,
10463
- line INTEGER NOT NULL,
10464
- col INTEGER NOT NULL,
10465
- signature TEXT NOT NULL DEFAULT '',
10466
- doc_comment TEXT NOT NULL DEFAULT '',
10467
- scope TEXT NOT NULL DEFAULT '',
10468
- text TEXT NOT NULL DEFAULT '',
10469
- file_fk TEXT NOT NULL
10470
- );
10471
- `;
10472
- var SYMBOL_INDEX_SQL = [
10473
- "CREATE INDEX IF NOT EXISTS idx_s_name ON symbols(name)",
10474
- "CREATE INDEX IF NOT EXISTS idx_s_kind ON symbols(kind)",
10475
- "CREATE INDEX IF NOT EXISTS idx_s_lang ON symbols(lang)",
10476
- "CREATE INDEX IF NOT EXISTS idx_s_file ON symbols(file)",
10477
- "CREATE INDEX IF NOT EXISTS idx_s_lang_kind ON symbols(lang, kind)",
10478
- "CREATE INDEX IF NOT EXISTS idx_s_file_fk ON symbols(file_fk)",
10479
- "CREATE INDEX IF NOT EXISTS idx_s_name_id ON symbols(name, id)"
10480
- ];
10481
- var REFS_TABLE_SQL = `
10482
- CREATE TABLE IF NOT EXISTS refs (
10483
- id INTEGER PRIMARY KEY,
10484
- from_id INTEGER NOT NULL,
10485
- to_name TEXT NOT NULL,
10486
- to_id INTEGER,
10487
- call_type TEXT NOT NULL,
10488
- line INTEGER NOT NULL
10489
- );
10490
- `;
10491
- var REFS_INDEX_SQL = [
10492
- "CREATE INDEX IF NOT EXISTS idx_r_from ON refs(from_id)",
10493
- "CREATE INDEX IF NOT EXISTS idx_r_to_id ON refs(to_id)",
10494
- "CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)",
10495
- "CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)"
10496
- ];
10497
- var SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')";
10498
-
10499
10393
  // src/codebase-index/writer-admin.ts
10500
- import * as fs13 from "node:fs";
10501
- import * as path19 from "node:path";
10394
+ import * as fs9 from "node:fs";
10395
+ import * as path13 from "node:path";
10502
10396
  var DB_FILE = "index.db";
10503
10397
  function getAllIndexableWithStatement(stmt) {
10504
10398
  return stmt("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
@@ -10557,7 +10451,7 @@ function getAllFileMetasWithStatement(stmt) {
10557
10451
  }
10558
10452
  function getIndexDbSizeBytes(indexDir) {
10559
10453
  try {
10560
- return fs13.statSync(path19.join(indexDir, DB_FILE)).size;
10454
+ return fs9.statSync(path13.join(indexDir, DB_FILE)).size;
10561
10455
  } catch {
10562
10456
  return 0;
10563
10457
  }
@@ -10624,7 +10518,7 @@ function bulkInsertRefsWithStatement(stmt, maxSqlVars, refs) {
10624
10518
  }
10625
10519
 
10626
10520
  // src/codebase-index/writer-graph-helpers.ts
10627
- import * as path20 from "node:path";
10521
+ import * as path14 from "node:path";
10628
10522
  function derivePackage(filePath) {
10629
10523
  const f = filePath.replace(/\\/g, "/");
10630
10524
  const pkgsIdx = f.indexOf("/packages/");
@@ -10739,16 +10633,16 @@ function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
10739
10633
  function resolveRelativeImport(fromFile, moduleName, indexedFiles) {
10740
10634
  if (!moduleName.startsWith(".")) return void 0;
10741
10635
  const normalizedFrom = fromFile.replace(/\\/g, "/");
10742
- const absolute = path20.posix.normalize(
10743
- path20.posix.join(path20.posix.dirname(normalizedFrom), moduleName)
10636
+ const absolute = path14.posix.normalize(
10637
+ path14.posix.join(path14.posix.dirname(normalizedFrom), moduleName)
10744
10638
  );
10745
- const extension = path20.posix.extname(absolute);
10639
+ const extension = path14.posix.extname(absolute);
10746
10640
  const base = extension ? absolute.slice(0, -extension.length) : absolute;
10747
10641
  const candidates = [
10748
10642
  absolute,
10749
10643
  ...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"].map((ext) => `${base}${ext}`),
10750
- ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path20.posix.join(absolute, `index${ext}`)),
10751
- ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path20.posix.join(base, `index${ext}`))
10644
+ ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path14.posix.join(absolute, `index${ext}`)),
10645
+ ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path14.posix.join(base, `index${ext}`))
10752
10646
  ];
10753
10647
  const indexedByPortablePath = new Map(
10754
10648
  [...indexedFiles].map((file) => [file.replace(/\\/g, "/").toLocaleLowerCase(), file])
@@ -10968,6 +10862,39 @@ function getSymbolGraphWithStatement(stmt, fileFilter) {
10968
10862
  return { nodes, edges };
10969
10863
  }
10970
10864
 
10865
+ // src/codebase-index/writer-helpers.ts
10866
+ import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core/utils";
10867
+ function escapeLike(value) {
10868
+ return value.replace(/[\\%_]/g, (char) => `\\${char}`);
10869
+ }
10870
+ function assignRefsToSymbols(refs, symbols) {
10871
+ if (refs.length === 0 || symbols.length === 0) return [];
10872
+ const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
10873
+ const seen = /* @__PURE__ */ new Set();
10874
+ const assigned = [];
10875
+ for (const ref of refs) {
10876
+ let owner2;
10877
+ for (const symbol of ordered) {
10878
+ if (symbol.line > ref.line) break;
10879
+ owner2 = symbol;
10880
+ }
10881
+ if (!owner2 && ref.callType === "import") owner2 = ordered[0];
10882
+ if (!owner2 || owner2.id <= 0) continue;
10883
+ const key = `${owner2.id}:${ref.toName}:${ref.callType}`;
10884
+ if (seen.has(key)) continue;
10885
+ seen.add(key);
10886
+ assigned.push({ ...ref, fromId: owner2.id });
10887
+ }
10888
+ return assigned;
10889
+ }
10890
+ function resolveIndexDir(projectRoot, override) {
10891
+ return override ?? resolveWstackPaths2({ projectRoot }).projectCodebaseIndex;
10892
+ }
10893
+ function codebaseIndexDirOverride(ctx) {
10894
+ const v = ctx.meta?.["codebaseIndexDir"];
10895
+ return typeof v === "string" ? v : void 0;
10896
+ }
10897
+
10971
10898
  // src/codebase-index/writer-pragmas.ts
10972
10899
  import { sqliteCachePragmas } from "@wrongstack/core/utils";
10973
10900
  function applyIndexStorePragmas(db) {
@@ -10986,6 +10913,63 @@ function applyIndexStorePragmas(db) {
10986
10913
  }
10987
10914
  }
10988
10915
 
10916
+ // src/codebase-index/writer-schema.ts
10917
+ var METADATA_TABLE_SQL = `
10918
+ CREATE TABLE IF NOT EXISTS metadata (
10919
+ key TEXT PRIMARY KEY,
10920
+ value TEXT NOT NULL
10921
+ );
10922
+ `;
10923
+ var CORE_TABLES_SQL = `
10924
+ CREATE TABLE IF NOT EXISTS files (
10925
+ file TEXT PRIMARY KEY,
10926
+ lang TEXT NOT NULL,
10927
+ mtime_ms INTEGER NOT NULL,
10928
+ symbol_count INTEGER NOT NULL DEFAULT 0,
10929
+ last_indexed INTEGER NOT NULL
10930
+ );
10931
+ CREATE TABLE IF NOT EXISTS symbols (
10932
+ id INTEGER PRIMARY KEY,
10933
+ lang TEXT NOT NULL,
10934
+ kind TEXT NOT NULL,
10935
+ name TEXT NOT NULL,
10936
+ file TEXT NOT NULL,
10937
+ line INTEGER NOT NULL,
10938
+ col INTEGER NOT NULL,
10939
+ signature TEXT NOT NULL DEFAULT '',
10940
+ doc_comment TEXT NOT NULL DEFAULT '',
10941
+ scope TEXT NOT NULL DEFAULT '',
10942
+ text TEXT NOT NULL DEFAULT '',
10943
+ file_fk TEXT NOT NULL
10944
+ );
10945
+ `;
10946
+ var SYMBOL_INDEX_SQL = [
10947
+ "CREATE INDEX IF NOT EXISTS idx_s_name ON symbols(name)",
10948
+ "CREATE INDEX IF NOT EXISTS idx_s_kind ON symbols(kind)",
10949
+ "CREATE INDEX IF NOT EXISTS idx_s_lang ON symbols(lang)",
10950
+ "CREATE INDEX IF NOT EXISTS idx_s_file ON symbols(file)",
10951
+ "CREATE INDEX IF NOT EXISTS idx_s_lang_kind ON symbols(lang, kind)",
10952
+ "CREATE INDEX IF NOT EXISTS idx_s_file_fk ON symbols(file_fk)",
10953
+ "CREATE INDEX IF NOT EXISTS idx_s_name_id ON symbols(name, id)"
10954
+ ];
10955
+ var REFS_TABLE_SQL = `
10956
+ CREATE TABLE IF NOT EXISTS refs (
10957
+ id INTEGER PRIMARY KEY,
10958
+ from_id INTEGER NOT NULL,
10959
+ to_name TEXT NOT NULL,
10960
+ to_id INTEGER,
10961
+ call_type TEXT NOT NULL,
10962
+ line INTEGER NOT NULL
10963
+ );
10964
+ `;
10965
+ var REFS_INDEX_SQL = [
10966
+ "CREATE INDEX IF NOT EXISTS idx_r_from ON refs(from_id)",
10967
+ "CREATE INDEX IF NOT EXISTS idx_r_to_id ON refs(to_id)",
10968
+ "CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)",
10969
+ "CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)"
10970
+ ];
10971
+ var SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')";
10972
+
10989
10973
  // src/codebase-index/writer-search-helpers.ts
10990
10974
  function normalizeSearchLimit(limit) {
10991
10975
  return typeof limit === "number" && Number.isFinite(limit) ? Math.max(0, Math.trunc(limit)) : void 0;
@@ -11174,9 +11158,9 @@ var IndexStore = class _IndexStore {
11174
11158
  }
11175
11159
  constructor(projectRoot, opts = {}) {
11176
11160
  this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
11177
- fs14.mkdirSync(this.indexDir, { recursive: true });
11161
+ fs10.mkdirSync(this.indexDir, { recursive: true });
11178
11162
  const Database = loadDatabaseSync();
11179
- this.db = new Database(path21.join(this.indexDir, DB_FILE2));
11163
+ this.db = new Database(path15.join(this.indexDir, DB_FILE2));
11180
11164
  applyIndexStorePragmas(this.db);
11181
11165
  this.initSchema();
11182
11166
  }
@@ -11194,9 +11178,15 @@ var IndexStore = class _IndexStore {
11194
11178
  DROP TABLE IF EXISTS refs;
11195
11179
  `);
11196
11180
  this.db.exec("DROP TABLE IF EXISTS symbols_fts");
11197
- this.stmt("UPDATE metadata SET value = ? WHERE key = ?").run(String(SCHEMA_VERSION), "version");
11181
+ this.stmt("UPDATE metadata SET value = ? WHERE key = ?").run(
11182
+ String(SCHEMA_VERSION),
11183
+ "version"
11184
+ );
11198
11185
  } else if (storedVersion === null) {
11199
- this.stmt("INSERT INTO metadata(key, value) VALUES (?, ?)").run("version", String(SCHEMA_VERSION));
11186
+ this.stmt("INSERT INTO metadata(key, value) VALUES (?, ?)").run(
11187
+ "version",
11188
+ String(SCHEMA_VERSION)
11189
+ );
11200
11190
  }
11201
11191
  this.db.exec(CORE_TABLES_SQL);
11202
11192
  for (const sql of SYMBOL_INDEX_SQL) this.db.exec(sql);
@@ -11213,7 +11203,9 @@ var IndexStore = class _IndexStore {
11213
11203
  );
11214
11204
  if (symbolCount !== ftsCount) {
11215
11205
  this.db.exec("DELETE FROM symbols_fts");
11216
- const rows = this.stmt("SELECT id, name, signature, doc_comment FROM symbols ORDER BY id").all();
11206
+ const rows = this.stmt(
11207
+ "SELECT id, name, signature, doc_comment FROM symbols ORDER BY id"
11208
+ ).all();
11217
11209
  bulkInsertFtsWithStatement(
11218
11210
  (sql) => this.stmt(sql),
11219
11211
  _IndexStore.MAX_SQL_VARS,
@@ -11435,7 +11427,9 @@ var IndexStore = class _IndexStore {
11435
11427
  const limitSql = limit !== void 0 ? " LIMIT ?" : "";
11436
11428
  const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment, text FROM symbols ${where}${limitSql}`;
11437
11429
  const binds = limit !== void 0 ? [...values, limit] : values;
11438
- const rows = this.stmt(sql).all(...binds);
11430
+ const rows = this.stmt(sql).all(
11431
+ ...binds
11432
+ );
11439
11433
  return rows.map((row) => mapWriterSearchRow(row, filter?.lspKind));
11440
11434
  }
11441
11435
  /** Shared WHERE builder for {@link search} / empty-query ranked totals. */
@@ -11581,13 +11575,13 @@ var IndexStore = class _IndexStore {
11581
11575
  if (rankDiff !== 0) return rankDiff;
11582
11576
  const scoreDiff = b.score - a.score;
11583
11577
  if (scoreDiff !== 0) return scoreDiff;
11584
- const left = expectDefined5(candidateById.get(a.id));
11585
- const right = expectDefined5(candidateById.get(b.id));
11578
+ const left = expectDefined2(candidateById.get(a.id));
11579
+ const right = expectDefined2(candidateById.get(b.id));
11586
11580
  return left.name.localeCompare(right.name) || left.file.localeCompare(right.file) || left.line - right.line || left.col - right.col || left.id - right.id;
11587
11581
  });
11588
11582
  const qTokens = tokenise(query);
11589
11583
  const results = scored.slice(0, limit).map(({ id, score }) => {
11590
- const c = expectDefined5(candidateById.get(id));
11584
+ const c = expectDefined2(candidateById.get(id));
11591
11585
  return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
11592
11586
  });
11593
11587
  return { results, total: candidates.length };
@@ -11611,7 +11605,9 @@ var IndexStore = class _IndexStore {
11611
11605
  }
11612
11606
  setLastIndexed(ts2) {
11613
11607
  this.runWithRetry(() => {
11614
- this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(String(ts2));
11608
+ this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(
11609
+ String(ts2)
11610
+ );
11615
11611
  });
11616
11612
  }
11617
11613
  getMetadata(key) {
@@ -11722,7 +11718,9 @@ var IndexStore = class _IndexStore {
11722
11718
  this.stmt(
11723
11719
  `DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
11724
11720
  ).run(...options.deleteForFiles);
11725
- this.stmt(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
11721
+ this.stmt(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(
11722
+ ...options.deleteForFiles
11723
+ );
11726
11724
  }
11727
11725
  const totalSymbols = entries.reduce((n, e) => n + e.symbols.length, 0);
11728
11726
  let nextId = this.allocateSymbolIds(totalSymbols);
@@ -11766,11 +11764,7 @@ var IndexStore = class _IndexStore {
11766
11764
  this.ftsAvailable,
11767
11765
  ftsRows
11768
11766
  );
11769
- bulkInsertRefsWithStatement(
11770
- (sql) => this.stmt(sql),
11771
- _IndexStore.MAX_SQL_VARS,
11772
- refsToInsert
11773
- );
11767
+ bulkInsertRefsWithStatement((sql) => this.stmt(sql), _IndexStore.MAX_SQL_VARS, refsToInsert);
11774
11768
  const upsertStmt = this.stmt(
11775
11769
  `INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
11776
11770
  VALUES (?, ?, ?, ?, ?)
@@ -11799,7 +11793,9 @@ var IndexStore = class _IndexStore {
11799
11793
  */
11800
11794
  deleteRefsForFile(file) {
11801
11795
  this.runWithRetry(() => {
11802
- this.stmt("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(file);
11796
+ this.stmt("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(
11797
+ file
11798
+ );
11803
11799
  });
11804
11800
  }
11805
11801
  /**
@@ -11954,9 +11950,7 @@ var IndexStore = class _IndexStore {
11954
11950
  * build the full symbol universe for the reachability scan.
11955
11951
  */
11956
11952
  getAllSymbols() {
11957
- return this.stmt(
11958
- "SELECT id, name, file, kind, line FROM symbols ORDER BY id"
11959
- ).all().map((r) => ({ ...r, kind: r.kind }));
11953
+ return this.stmt("SELECT id, name, file, kind, line FROM symbols ORDER BY id").all().map((r) => ({ ...r, kind: r.kind }));
11960
11954
  }
11961
11955
  /**
11962
11956
  * Returns every resolved reference (to_id IS NOT NULL). Used by
@@ -11968,6 +11962,25 @@ var IndexStore = class _IndexStore {
11968
11962
  "SELECT from_id AS fromId, to_id AS toId, call_type AS callType FROM refs WHERE to_id IS NOT NULL"
11969
11963
  ).all();
11970
11964
  }
11965
+ /**
11966
+ * Returns ALL import refs (including unresolved) with their source-file
11967
+ * path and resolved target id. Used by the dead-code scan's file-level
11968
+ * graph traversal to handle barrel-only entry points where no symbol
11969
+ * carries the ref.
11970
+ *
11971
+ * Refs whose `from_id` doesn't match a known symbol (e.g. pure-barrel
11972
+ * files with no declarations) will have `sourceFile === null`.
11973
+ */
11974
+ getAllImportRefs() {
11975
+ return this.stmt(
11976
+ `SELECT s.file AS sourceFile, r.to_name AS toName, r.to_id AS toId,
11977
+ r.call_type AS callType, r.line
11978
+ FROM refs r
11979
+ LEFT JOIN symbols s ON r.from_id = s.id
11980
+ WHERE r.call_type = 'import'
11981
+ ORDER BY r.line`
11982
+ ).all();
11983
+ }
11971
11984
  close() {
11972
11985
  this.stmtCache.clear();
11973
11986
  this.bm25Dirty = true;
@@ -11982,1174 +11995,1285 @@ var indexStorePool = new StorePool(
11982
11995
  (projectRoot, opts) => new IndexStore(projectRoot, opts)
11983
11996
  );
11984
11997
 
11985
- // src/codebase-index/indexer.ts
11986
- var YIELD_EVERY_N = 50;
11987
- function resolveParallelBatch() {
11988
- return indexParallelBatchSize(availableParallelism());
11989
- }
11990
- function yieldEventLoop() {
11991
- return new Promise((resolve17) => setImmediate(resolve17));
11998
+ // src/codebase-index/project-server-endpoint.ts
11999
+ var PROJECT_INDEX_SERVER_PROTOCOL_VERSION = 1;
12000
+ var PROJECT_INDEX_SERVER_METADATA_FILE = "server.json";
12001
+ var PROJECT_INDEX_SERVER_SOCKET_DIR = `wsci-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}`;
12002
+ var buildIdCache;
12003
+ function projectIndexServerBuildId(entrypoint) {
12004
+ const file = entrypoint instanceof URL || entrypoint.startsWith("file:") ? fileURLToPath(entrypoint) : path16.resolve(entrypoint);
12005
+ try {
12006
+ const stat18 = fs11.statSync(file);
12007
+ if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat18.mtimeMs && buildIdCache.size === stat18.size) {
12008
+ return buildIdCache.buildId;
12009
+ }
12010
+ const buildId = createHash4("sha256").update(fs11.readFileSync(file)).digest("hex").slice(0, 24);
12011
+ buildIdCache = { file, mtimeMs: stat18.mtimeMs, size: stat18.size, buildId };
12012
+ return buildId;
12013
+ } catch {
12014
+ return `unreadable:${path16.basename(file)}`;
12015
+ }
11992
12016
  }
11993
- function throwIfAborted(signal) {
11994
- if (!signal?.aborted) return;
11995
- if (signal.reason instanceof Error) throw signal.reason;
11996
- throw new Error(typeof signal.reason === "string" ? signal.reason : "Indexing cancelled");
12017
+ function normalizeLocalPath(value) {
12018
+ const resolved = path16.resolve(value);
12019
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
11997
12020
  }
11998
- function isAbortError(err) {
11999
- return err instanceof DOMException && err.name === "AbortError";
12021
+ function projectIndexServerKey(projectRoot, indexDir) {
12022
+ const resolvedIndexDir = normalizeLocalPath(resolveIndexDir(projectRoot, indexDir));
12023
+ return createHash4("sha256").update(resolvedIndexDir).digest("hex").slice(0, 24);
12000
12024
  }
12001
- var DEFAULT_IGNORE = DEFAULT_WALK_IGNORE_DIRS;
12002
- var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "pnpm-lock.yml"]);
12003
- var INDEXABLE_EXTENSION_SET = new Set(INDEXABLE_EXTENSIONS);
12004
- var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
12005
- var MAX_GIT_FILE_LIST_BYTES = 64 * 1024 * 1024;
12006
- function isWithinProject(projectRoot, file) {
12007
- const rel = path22.relative(projectRoot, file);
12008
- return rel !== "" && !rel.startsWith(`..${path22.sep}`) && rel !== ".." && !path22.isAbsolute(rel);
12025
+ function projectIndexServerEndpoint(projectRoot, indexDir) {
12026
+ const key = projectIndexServerKey(projectRoot, indexDir);
12027
+ if (process.platform === "win32") {
12028
+ return `\\\\.\\pipe\\wrongstack-codebase-index-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}-${key}`;
12029
+ }
12030
+ return path16.join(os5.tmpdir(), PROJECT_INDEX_SERVER_SOCKET_DIR, `${key}.sock`);
12009
12031
  }
12010
- function isMissingPathError(err) {
12011
- const code = err?.code;
12012
- return code === "ENOENT" || code === "ENOTDIR";
12032
+ function projectIndexServerMetadataPath(projectRoot, indexDir) {
12033
+ return path16.join(
12034
+ path16.resolve(resolveIndexDir(projectRoot, indexDir)),
12035
+ PROJECT_INDEX_SERVER_METADATA_FILE
12036
+ );
12013
12037
  }
12014
- function normalizeComparablePath(value) {
12015
- const resolved = path22.resolve(value);
12016
- return process.platform === "win32" ? resolved.toLowerCase() : resolved;
12038
+
12039
+ // src/codebase-index/project-server-protocol.ts
12040
+ var PROJECT_INDEX_SERVER_MAX_FRAME_CHARS = 64 * 1024 * 1024;
12041
+ function encodeProjectServerMessage(message) {
12042
+ return `${JSON.stringify(message)}
12043
+ `;
12017
12044
  }
12018
- function gitOutput(projectRoot, args) {
12019
- return new Promise((resolve17, reject) => {
12020
- execFile2(
12021
- "git",
12022
- ["-C", projectRoot, ...args],
12023
- {
12024
- encoding: "buffer",
12025
- maxBuffer: MAX_GIT_FILE_LIST_BYTES,
12026
- windowsHide: true
12027
- },
12028
- (error, stdout) => {
12029
- if (error) reject(error);
12030
- else resolve17(Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout));
12031
- }
12032
- );
12033
- });
12034
- }
12035
- async function findGitSourceFiles(projectRoot, ignore, signal) {
12036
- try {
12037
- throwIfAborted(signal);
12038
- const topLevel = (await gitOutput(projectRoot, ["rev-parse", "--show-toplevel"])).toString("utf8").trim();
12039
- if (normalizeComparablePath(topLevel) !== normalizeComparablePath(projectRoot)) return null;
12040
- throwIfAborted(signal);
12041
- const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
12042
- const [output, statusOutput] = await Promise.all([
12043
- gitOutput(projectRoot, ["ls-files", "--cached", "--others", "--exclude-standard", "-z"]),
12044
- gitOutput(projectRoot, [
12045
- "status",
12046
- "--porcelain=v1",
12047
- "-z",
12048
- "--untracked-files=all",
12049
- "--ignored=no"
12050
- ])
12051
- ]);
12052
- throwIfAborted(signal);
12053
- const dirty = /* @__PURE__ */ new Set();
12054
- const deleted = /* @__PURE__ */ new Set();
12055
- const statusRecords = statusOutput.toString("utf8").split("\0");
12056
- for (let i = 0; i < statusRecords.length; i++) {
12057
- const record = statusRecords[i];
12058
- if (!record) continue;
12059
- const status = record.slice(0, 2);
12060
- const changedPath = path22.resolve(projectRoot, record.slice(3));
12061
- dirty.add(changedPath);
12062
- if (status.includes("D")) deleted.add(changedPath);
12063
- if (status.includes("R") || status.includes("C")) {
12064
- const source = statusRecords[++i];
12065
- if (source) dirty.add(path22.resolve(projectRoot, source));
12045
+
12046
+ // src/codebase-index/project-server-client.ts
12047
+ var CONNECT_ATTEMPT_TIMEOUT_MS = 750;
12048
+ var SERVER_START_TIMEOUT_MS = 1e4;
12049
+ var SERVER_CONTROL_TIMEOUT_MS = 5e3;
12050
+ var SERVER_HEALTH_TIMEOUT_MS = 3e3;
12051
+ var SERVER_HEARTBEAT_INTERVAL_MS = 1e4;
12052
+ var StaleProjectIndexServerError = class extends Error {
12053
+ constructor(message, pid) {
12054
+ super(message);
12055
+ this.pid = pid;
12056
+ }
12057
+ pid;
12058
+ name = "StaleProjectIndexServerError";
12059
+ };
12060
+ var connectionStates = /* @__PURE__ */ new Map();
12061
+ var connectionStateListeners = /* @__PURE__ */ new Set();
12062
+ var latestConnectionState = {
12063
+ status: "offline",
12064
+ connected: false
12065
+ };
12066
+ function resolveProjectIndexDaemonAvailability(projectRoot, indexDir) {
12067
+ if (process.env["WRONGSTACK_INDEX_INLINE"] || process.env["WRONGSTACK_INDEX_SERVER"] === "0") {
12068
+ return { kind: "inline-requested" };
12069
+ }
12070
+ let builtUrl = null;
12071
+ for (const rel of ["./project-server.js", "./codebase-index/project-server.js"]) {
12072
+ try {
12073
+ const url = new URL(rel, import.meta.url);
12074
+ if (url.protocol === "file:" && fs12.existsSync(fileURLToPath2(url))) {
12075
+ builtUrl = url;
12076
+ break;
12066
12077
  }
12078
+ } catch {
12067
12079
  }
12068
- const files = [];
12069
- for (const relative13 of output.toString("utf8").split("\0")) {
12070
- if (!relative13) continue;
12071
- const portable = relative13.replace(/\\/g, "/");
12072
- if (portable.split("/").some((segment) => ignoreSet.has(segment)) || DEFAULT_IGNORE_FILES.has(path22.posix.basename(portable))) {
12073
- continue;
12074
- }
12075
- const full = path22.resolve(projectRoot, relative13);
12076
- if (deleted.has(full)) continue;
12077
- const ext = path22.extname(relative13).toLowerCase();
12078
- if (INDEXABLE_EXTENSION_SET.has(ext) || detectLang(full) !== null) files.push(full);
12080
+ }
12081
+ if (builtUrl === null) return { kind: "missing-build" };
12082
+ if (projectRoot !== void 0) {
12083
+ const endpoint = projectIndexServerEndpoint(projectRoot, indexDir);
12084
+ const check = checkUnixSocketPath(endpoint);
12085
+ if (!check.ok) {
12086
+ return {
12087
+ kind: "endpoint-invalid",
12088
+ endpoint,
12089
+ byteLength: check.byteLength,
12090
+ maxBytes: check.maxBytes
12091
+ };
12079
12092
  }
12080
- return {
12081
- files,
12082
- trustedUnchanged: new Set(files.filter((file) => !dirty.has(file)))
12083
- };
12084
- } catch {
12085
- return null;
12086
12093
  }
12094
+ return { kind: "available", url: builtUrl };
12087
12095
  }
12088
- async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
12089
- const gitFiles = await findGitSourceFiles(projectRoot, ignore, signal);
12090
- if (gitFiles) {
12096
+ function resolveProjectServerUrl() {
12097
+ const availability = resolveProjectIndexDaemonAvailability();
12098
+ return availability.kind === "available" ? availability.url : null;
12099
+ }
12100
+ function projectIndexServerExpectedBuildId() {
12101
+ const override = process.env["WRONGSTACK_INDEX_SERVER_BUILD_ID"]?.trim();
12102
+ if (override) return override;
12103
+ const url = resolveProjectServerUrl();
12104
+ return url ? projectIndexServerBuildId(url) : null;
12105
+ }
12106
+ function isProjectIndexServerAvailable() {
12107
+ return resolveProjectServerUrl() !== null;
12108
+ }
12109
+ function publishConnectionState(endpoint, state) {
12110
+ connectionStates.set(endpoint, state);
12111
+ latestConnectionState = state;
12112
+ for (const listener of connectionStateListeners) listener(state);
12113
+ }
12114
+ function getProjectIndexServerConnectionState(projectRoot, indexDir) {
12115
+ if (projectRoot) {
12116
+ const endpoint = projectIndexServerEndpoint(projectRoot, indexDir);
12117
+ const existing = connectionStates.get(endpoint);
12118
+ if (existing) return existing;
12119
+ if (!isProjectIndexServerAvailable()) {
12120
+ return { status: "unavailable", connected: false };
12121
+ }
12091
12122
  return {
12092
- files: gitFiles.files,
12093
- complete: true,
12094
- errors: [],
12095
- trustedUnchanged: gitFiles.trustedUnchanged
12123
+ status: "offline",
12124
+ connected: false,
12125
+ projectRoot,
12126
+ indexDir,
12127
+ endpoint
12096
12128
  };
12097
12129
  }
12098
- const results = [];
12099
- const errors = [];
12100
- let complete = true;
12101
- const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
12102
- const indexableExts = new Set(INDEXABLE_EXTENSIONS);
12103
- let dirCount = 0;
12104
- const walk2 = async (dir) => {
12105
- throwIfAborted(signal);
12106
- if (dirCount > 0 && dirCount % YIELD_EVERY_N === 0) {
12107
- await yieldEventLoop();
12108
- throwIfAborted(signal);
12109
- }
12110
- let entries;
12111
- try {
12112
- entries = await fs15.readdir(dir, { withFileTypes: true });
12113
- } catch (err) {
12114
- complete = false;
12115
- errors.push(`scan error: ${dir}: ${err instanceof Error ? err.message : String(err)}`);
12116
- return;
12117
- }
12118
- dirCount++;
12119
- for (const e of entries) {
12120
- if (ignoreSet.has(e.name)) continue;
12121
- const full = path22.join(dir, e.name);
12122
- const rel = path22.relative(projectRoot, full).replace(/\\/g, "/");
12123
- if (e.isDirectory()) {
12124
- if (isGitIgnored(rel, true)) continue;
12125
- await walk2(full);
12126
- } else if (e.isFile()) {
12127
- if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
12128
- const ext = path22.extname(e.name).toLowerCase();
12129
- if (indexableExts.has(ext) || detectLang(full) !== null) {
12130
- results.push(full);
12131
- }
12132
- }
12133
- }
12134
- };
12135
- await walk2(projectRoot);
12136
- return { files: results, complete, errors };
12130
+ if (latestConnectionState.endpoint) return latestConnectionState;
12131
+ if (!isProjectIndexServerAvailable()) return { status: "unavailable", connected: false };
12132
+ return latestConnectionState;
12137
12133
  }
12138
- function assignRefsToSymbols2(refs, symbols) {
12139
- if (refs.length === 0 || symbols.length === 0) return [];
12140
- const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
12141
- const seen = /* @__PURE__ */ new Set();
12142
- const assigned = [];
12143
- for (const ref of refs) {
12144
- let owner2;
12145
- for (const symbol of ordered) {
12146
- if (symbol.line > ref.line) break;
12147
- owner2 = symbol;
12148
- }
12149
- if (!owner2 && ref.callType === "import") owner2 = ordered[0];
12150
- if (!owner2 || owner2.id <= 0) continue;
12151
- const key = `${owner2.id}:${ref.toName}:${ref.callType}`;
12152
- if (seen.has(key)) continue;
12153
- seen.add(key);
12154
- assigned.push({ ...ref, fromId: owner2.id });
12155
- }
12156
- return assigned;
12134
+ function onProjectIndexServerConnectionStateChange(listener) {
12135
+ connectionStateListeners.add(listener);
12136
+ return () => connectionStateListeners.delete(listener);
12157
12137
  }
12158
- async function runIndexerWithStore(store, opts) {
12159
- const { projectRoot, langs, ignore = [], signal } = opts;
12160
- const relationGraphVersion = "2";
12161
- const refResolutionVersion = "2";
12162
- const force = (opts.force ?? false) || store.getMetadata("relation_graph_version") !== relationGraphVersion;
12163
- const needsFullRefResolution = force || store.getMetadata("ref_resolution_version") !== refResolutionVersion;
12164
- const startMs = Date.now();
12165
- const errors = [];
12166
- const langStats = {};
12167
- let filesIndexed = 0;
12168
- let symbolsIndexed = 0;
12169
- const isGitIgnored = await loadGitignoreMatcher(projectRoot);
12170
- let files;
12171
- let discoveredFiles = null;
12172
- let discoveryComplete = true;
12173
- let trustedUnchanged;
12174
- if (opts.files && opts.files.length > 0) {
12175
- files = opts.files.map((f) => path22.resolve(projectRoot, f)).filter((f) => {
12176
- if (!isWithinProject(projectRoot, f)) return false;
12177
- const rel = path22.relative(projectRoot, f).replace(/\\/g, "/");
12178
- return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path22.basename(f)) && !isGitIgnored(rel, false);
12179
- });
12180
- } else {
12181
- const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
12182
- files = discovery.files;
12183
- errors.push(...discovery.errors);
12184
- discoveryComplete = discovery.complete;
12185
- discoveredFiles = new Set(files);
12186
- trustedUnchanged = discovery.trustedUnchanged;
12187
- }
12188
- if (langs && langs.length > 0) {
12189
- const langSet = new Set(langs);
12190
- files = files.filter((f) => {
12191
- const lang = detectLang(f);
12192
- return lang ? langSet.has(lang) : false;
12193
- });
12194
- }
12195
- if (force) store.clearAll();
12196
- const existingMeta = /* @__PURE__ */ new Map();
12197
- if (!force) {
12198
- for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
12138
+ function remoteError(message, name) {
12139
+ if (name === "LockError") return new LockError(message);
12140
+ if (name === "IndexTimeoutError") return new IndexTimeoutError(message);
12141
+ const error = new Error(message);
12142
+ if (name && name !== "Error") error.name = name;
12143
+ return error;
12144
+ }
12145
+ function isProjectIndexServerHealth(value) {
12146
+ if (!value || typeof value !== "object") return false;
12147
+ const health = value;
12148
+ const memory = health.memory && typeof health.memory === "object" ? health.memory : void 0;
12149
+ const activity = health.activity && typeof health.activity === "object" ? health.activity : void 0;
12150
+ return typeof health.checkedAt === "number" && typeof health.uptimeMs === "number" && typeof memory?.rss === "number" && typeof memory.heapUsed === "number" && typeof memory.heapTotal === "number" && typeof memory.external === "number" && typeof health.clients === "number" && typeof health.activeRequests === "number" && typeof health.activeWrites === "number" && typeof health.queuedWrites === "number" && typeof health.pendingExternalFiles === "number" && typeof health.watchingExternal === "boolean" && typeof activity?.indexing === "boolean" && typeof activity.currentFile === "number" && typeof activity.totalFiles === "number" && typeof activity.generation === "number";
12151
+ }
12152
+ function delay(ms) {
12153
+ return new Promise((resolve17) => {
12154
+ const timer = setTimeout(resolve17, ms);
12155
+ timer.unref?.();
12156
+ });
12157
+ }
12158
+ function cancellationError(signal) {
12159
+ return signal.reason instanceof Error ? signal.reason : new Error("Indexing cancelled");
12160
+ }
12161
+ var ProjectServerConnection = class {
12162
+ constructor(projectRoot, indexDir, endpoint) {
12163
+ this.projectRoot = projectRoot;
12164
+ this.indexDir = indexDir;
12165
+ this.endpoint = endpoint;
12166
+ this.transition("offline");
12199
12167
  }
12200
- const totalFilesForProgress = files.length;
12201
- let filesPreSkipped = 0;
12202
- if (!force && trustedUnchanged) {
12203
- files = files.filter((file) => {
12204
- const meta = existingMeta.get(file);
12205
- if (!meta || !trustedUnchanged.has(file)) return true;
12206
- langStats[meta.lang] = (langStats[meta.lang] ?? 0) + meta.symbolCount;
12207
- symbolsIndexed += meta.symbolCount;
12208
- filesIndexed++;
12209
- filesPreSkipped++;
12210
- return false;
12168
+ projectRoot;
12169
+ indexDir;
12170
+ endpoint;
12171
+ socket = null;
12172
+ buffer = "";
12173
+ info = null;
12174
+ activity = null;
12175
+ health = null;
12176
+ healthCheck = null;
12177
+ connecting = null;
12178
+ connectResolve = null;
12179
+ connectReject = null;
12180
+ nextId = 1;
12181
+ pending = /* @__PURE__ */ new Map();
12182
+ transition(status, options = {}) {
12183
+ const previous = connectionStates.get(this.endpoint);
12184
+ const pid = options.pid ?? (status === "connected" ? this.info?.pid : void 0);
12185
+ const lastError = options.error === void 0 ? status === "error" || status === "degraded" || status === "unresponsive" ? previous?.lastError : void 0 : options.error instanceof Error ? options.error.message : String(options.error);
12186
+ publishConnectionState(this.endpoint, {
12187
+ status,
12188
+ connected: status === "connected" || status === "degraded" || status === "unresponsive",
12189
+ projectRoot: this.projectRoot,
12190
+ indexDir: this.indexDir,
12191
+ endpoint: this.endpoint,
12192
+ pid,
12193
+ lastError,
12194
+ ...this.activity ? { activity: this.activity } : {},
12195
+ ...this.health ? { health: this.health } : {}
12211
12196
  });
12212
- if (filesPreSkipped > 0) opts.onProgress?.(filesPreSkipped, totalFilesForProgress);
12213
12197
  }
12214
- const parallelBatch = resolveParallelBatch();
12215
- let filesSinceLastYield = 0;
12216
- for (let batchStart = 0; batchStart < files.length; batchStart += parallelBatch) {
12217
- const batchEnd = Math.min(batchStart + parallelBatch, files.length);
12218
- const batchFiles = files.slice(batchStart, batchEnd);
12219
- opts.onProgress?.(filesPreSkipped + batchEnd, totalFilesForProgress);
12220
- filesSinceLastYield += batchFiles.length;
12221
- if (filesSinceLastYield >= YIELD_EVERY_N) {
12222
- filesSinceLastYield = 0;
12223
- await yieldEventLoop();
12224
- if (isFrugalPerf()) {
12225
- await new Promise((r) => setTimeout(r, 8));
12226
- }
12227
- throwIfAborted(signal);
12198
+ isConnected() {
12199
+ return this.socket !== null && !this.socket.destroyed && this.info !== null;
12200
+ }
12201
+ async checkHealth(spawnIfMissing = false, timeoutMs = SERVER_HEALTH_TIMEOUT_MS) {
12202
+ await this.ensureConnected(spawnIfMissing);
12203
+ if (this.healthCheck) return this.healthCheck;
12204
+ const startedAt = Date.now();
12205
+ this.healthCheck = this.request({ type: "ping" }, { timeoutMs }).then((server) => {
12206
+ const now2 = Date.now();
12207
+ this.health = {
12208
+ status: "healthy",
12209
+ checkedAt: now2,
12210
+ lastHealthyAt: now2,
12211
+ latencyMs: Math.max(0, now2 - startedAt),
12212
+ missedHeartbeats: 0,
12213
+ ...isProjectIndexServerHealth(server) ? { server } : {}
12214
+ };
12215
+ this.transition("connected", { pid: this.info?.pid });
12216
+ return this.health;
12217
+ }).catch((error) => {
12218
+ if (!this.isConnected()) throw error;
12219
+ if ((this.health?.lastHealthyAt ?? 0) > startedAt) return this.health;
12220
+ const missedHeartbeats = (this.health?.missedHeartbeats ?? 0) + 1;
12221
+ const status = missedHeartbeats >= 3 ? "unresponsive" : "degraded";
12222
+ this.health = {
12223
+ status,
12224
+ checkedAt: Date.now(),
12225
+ lastHealthyAt: this.health?.lastHealthyAt ?? null,
12226
+ latencyMs: null,
12227
+ missedHeartbeats,
12228
+ ...this.health?.server ? { server: this.health.server } : {}
12229
+ };
12230
+ this.transition(status, { pid: this.info?.pid, error });
12231
+ return this.health;
12232
+ }).finally(() => {
12233
+ this.healthCheck = null;
12234
+ });
12235
+ return this.healthCheck;
12236
+ }
12237
+ markResponsive() {
12238
+ const now2 = Date.now();
12239
+ this.health = {
12240
+ status: "healthy",
12241
+ checkedAt: now2,
12242
+ lastHealthyAt: now2,
12243
+ latencyMs: this.health?.latencyMs ?? null,
12244
+ missedHeartbeats: 0,
12245
+ ...this.health?.server ? { server: this.health.server } : {}
12246
+ };
12247
+ }
12248
+ async call(op, args, options) {
12249
+ if (options.signal?.aborted) throw cancellationError(options.signal);
12250
+ await this.ensureConnected(true);
12251
+ if (options.signal?.aborted) throw cancellationError(options.signal);
12252
+ return this.request({ type: "request", op, args }, options);
12253
+ }
12254
+ async shutdownRemote(reason) {
12255
+ try {
12256
+ await this.ensureConnected(false);
12257
+ } catch {
12258
+ return { stopped: false, reason: "not-running" };
12228
12259
  }
12229
- const statOpts = signal ? { signal } : {};
12230
- const statReadParse = await Promise.allSettled(
12231
- batchFiles.map(
12232
- async (file) => {
12233
- let stat18;
12234
- try {
12235
- stat18 = await fs15.stat(file, statOpts);
12236
- } catch (e) {
12237
- if (isAbortError(e)) throw e;
12238
- return {
12239
- file,
12240
- stat: null,
12241
- lang: "",
12242
- parsed: null,
12243
- error: `stat error: ${e instanceof Error ? e.message : String(e)}`,
12244
- missing: isMissingPathError(e)
12245
- };
12246
- }
12247
- if (!stat18.isFile()) return { file, stat: stat18, lang: "", parsed: null };
12248
- const lang = detectLang(file);
12249
- if (!lang) return { file, stat: stat18, lang: "", parsed: null };
12250
- if (stat18.size > MAX_INDEX_FILE_BYTES) {
12251
- return {
12252
- file,
12253
- stat: stat18,
12254
- lang,
12255
- parsed: null,
12256
- error: `file too large (${stat18.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
12257
- };
12258
- }
12259
- const meta = existingMeta.get(file);
12260
- if (!force && meta && meta.mtimeMs === Math.floor(stat18.mtimeMs)) {
12261
- return { file, stat: stat18, lang, parsed: null, skippedMeta: meta };
12262
- }
12263
- let content;
12264
- try {
12265
- content = await fs15.readFile(file, { encoding: "utf8", signal });
12266
- } catch (e) {
12267
- if (isAbortError(e)) throw e;
12268
- return {
12269
- file,
12270
- stat: stat18,
12271
- lang,
12272
- parsed: null,
12273
- error: `read error: ${e instanceof Error ? e.message : String(e)}`
12274
- };
12275
- }
12276
- let parsed;
12277
- try {
12278
- parsed = await parseFileContent(file, content, lang);
12279
- } catch (e) {
12280
- return {
12281
- file,
12282
- stat: stat18,
12283
- lang,
12284
- parsed: null,
12285
- error: `parse error: ${e instanceof Error ? e.message : String(e)}`
12286
- };
12287
- }
12288
- return { file, stat: stat18, lang, parsed, content };
12289
- }
12290
- )
12260
+ const pid = this.info?.pid;
12261
+ try {
12262
+ this.transition("stopping", { pid });
12263
+ await this.request(
12264
+ { type: "shutdown", reason },
12265
+ { timeoutMs: SERVER_CONTROL_TIMEOUT_MS }
12266
+ );
12267
+ return { stopped: true, pid };
12268
+ } catch (error) {
12269
+ const forceKilled = this.forceKillKnownServer();
12270
+ return {
12271
+ stopped: forceKilled,
12272
+ pid,
12273
+ reason: forceKilled ? `force-killed after graceful shutdown failed: ${error instanceof Error ? error.message : String(error)}` : error instanceof Error ? error.message : String(error)
12274
+ };
12275
+ } finally {
12276
+ this.close();
12277
+ }
12278
+ }
12279
+ async configure(watchExternal, debounceMs) {
12280
+ await this.ensureConnected(true);
12281
+ const startedAt = Date.now();
12282
+ const result = await this.request(
12283
+ { type: "configure", watchExternal, debounceMs },
12284
+ { timeoutMs: SERVER_CONTROL_TIMEOUT_MS }
12291
12285
  );
12292
- const batchEntries = [];
12293
- const deleteForFiles = [];
12294
- for (let fi = 0; fi < statReadParse.length; fi++) {
12295
- const settled = statReadParse[fi];
12296
- const file = expectDefined6(batchFiles[fi]);
12297
- if (settled.status === "rejected") {
12298
- const err = settled.reason;
12299
- if (err instanceof Error && isAbortError(err)) throw err;
12300
- errors.push(`batch error: ${file}: ${err instanceof Error ? err.message : String(err)}`);
12301
- continue;
12302
- }
12303
- const result = settled.value;
12304
- if (result.error) {
12305
- if (result.missing) store.deleteFile(file);
12306
- errors.push(`${file}: ${result.error}`);
12307
- continue;
12308
- }
12309
- const { stat: stat18, lang, parsed } = result;
12310
- if (result.skippedMeta) {
12311
- langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
12312
- symbolsIndexed += result.skippedMeta.symbolCount;
12313
- filesIndexed++;
12314
- continue;
12286
+ if (isProjectIndexServerHealth(result.health)) {
12287
+ const now2 = Date.now();
12288
+ this.health = {
12289
+ status: "healthy",
12290
+ checkedAt: now2,
12291
+ lastHealthyAt: now2,
12292
+ latencyMs: Math.max(0, now2 - startedAt),
12293
+ missedHeartbeats: 0,
12294
+ server: result.health
12295
+ };
12296
+ this.transition("connected", { pid: this.info?.pid });
12297
+ }
12298
+ }
12299
+ close() {
12300
+ const socket = this.socket;
12301
+ this.socket = null;
12302
+ this.info = null;
12303
+ this.activity = null;
12304
+ this.health = null;
12305
+ this.connectReject?.(new Error("codebase-index client disconnected"));
12306
+ this.connectResolve = null;
12307
+ this.connectReject = null;
12308
+ if (socket && !socket.destroyed) socket.destroy();
12309
+ this.rejectPending(new Error("codebase-index client disconnected"));
12310
+ this.transition("offline");
12311
+ maybeStopHeartbeatLoop();
12312
+ }
12313
+ request(message, options) {
12314
+ const socket = this.socket;
12315
+ if (!socket || socket.destroyed) {
12316
+ return Promise.reject(new Error("codebase-index server connection is not available"));
12317
+ }
12318
+ const id = this.nextId++;
12319
+ return new Promise((resolve17, reject) => {
12320
+ const timer = setTimeout(() => {
12321
+ const entry = this.pending.get(id);
12322
+ if (!entry) return;
12323
+ this.pending.delete(id);
12324
+ this.write({ type: "cancel", id });
12325
+ const error = new IndexTimeoutError(
12326
+ `Index ${message.type === "request" ? message.op : message.type} exceeded its ${options.timeoutMs}ms watchdog timeout`
12327
+ );
12328
+ this.cleanupPending(entry);
12329
+ entry.reject(error);
12330
+ }, options.timeoutMs);
12331
+ timer.unref?.();
12332
+ const signal = options.signal;
12333
+ const onAbort = signal ? () => {
12334
+ const entry = this.pending.get(id);
12335
+ if (!entry) return;
12336
+ this.pending.delete(id);
12337
+ this.write({ type: "cancel", id });
12338
+ this.cleanupPending(entry);
12339
+ entry.reject(cancellationError(signal));
12340
+ } : void 0;
12341
+ this.pending.set(id, {
12342
+ resolve: resolve17,
12343
+ reject,
12344
+ timer,
12345
+ signal,
12346
+ onAbort,
12347
+ onProgress: options.onProgress
12348
+ });
12349
+ if (signal && onAbort) {
12350
+ signal.addEventListener("abort", onAbort, { once: true });
12351
+ if (signal.aborted) {
12352
+ onAbort();
12353
+ return;
12354
+ }
12315
12355
  }
12316
- if (!lang || !parsed) {
12317
- if (lang) {
12318
- store.upsertFile({
12319
- file,
12320
- lang,
12321
- mtimeMs: Math.floor(stat18.mtimeMs),
12322
- symbolCount: 0,
12323
- lastIndexed: Date.now()
12324
- });
12325
- filesIndexed++;
12356
+ this.write({ ...message, id });
12357
+ });
12358
+ }
12359
+ async ensureConnected(spawnIfMissing) {
12360
+ if (this.socket && !this.socket.destroyed && this.info) return;
12361
+ if (this.connecting) return this.connecting;
12362
+ this.transition("connecting");
12363
+ this.connecting = this.connectWithElection(spawnIfMissing).catch((error) => {
12364
+ this.transition("error", { error });
12365
+ throw error;
12366
+ }).finally(() => {
12367
+ this.connecting = null;
12368
+ });
12369
+ return this.connecting;
12370
+ }
12371
+ async connectWithElection(spawnIfMissing) {
12372
+ const deadline = Date.now() + (spawnIfMissing ? SERVER_START_TIMEOUT_MS : CONNECT_ATTEMPT_TIMEOUT_MS);
12373
+ let spawned = false;
12374
+ let staleAttempts = 0;
12375
+ let lastError = new Error("codebase-index server unavailable");
12376
+ while (Date.now() < deadline) {
12377
+ try {
12378
+ await this.connectOnce();
12379
+ return;
12380
+ } catch (error) {
12381
+ lastError = error;
12382
+ if (error instanceof StaleProjectIndexServerError) {
12383
+ staleAttempts++;
12384
+ if (!spawnIfMissing) break;
12385
+ if (staleAttempts >= 3) this.forceKillServer(error.pid);
12386
+ spawned = false;
12387
+ await delay(100);
12388
+ continue;
12326
12389
  }
12327
- continue;
12328
12390
  }
12329
- if (parsed.symbols.length === 0) {
12330
- store.replaceEmptyFile({
12331
- file,
12332
- lang,
12333
- mtimeMs: Math.floor(stat18.mtimeMs),
12334
- symbolCount: 0,
12335
- lastIndexed: Date.now()
12336
- });
12337
- filesIndexed++;
12338
- continue;
12391
+ if (!spawnIfMissing) break;
12392
+ if (!spawned) {
12393
+ this.spawnDetachedServer();
12394
+ spawned = true;
12339
12395
  }
12340
- batchEntries.push({
12341
- file,
12342
- lang,
12343
- symbols: parsed.symbols,
12344
- refs: parsed.refs ?? [],
12345
- mtimeMs: Math.floor(stat18.mtimeMs),
12346
- symbolCount: parsed.symbols.length
12347
- });
12348
- deleteForFiles.push(file);
12396
+ await delay(75);
12349
12397
  }
12350
- if (batchEntries.length > 0) {
12351
- try {
12352
- store.commitBatch(batchEntries, { deleteForFiles });
12353
- for (const entry of batchEntries) {
12354
- const count = entry.symbols.length;
12355
- symbolsIndexed += count;
12356
- langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
12357
- filesIndexed++;
12358
- }
12359
- } catch (err) {
12360
- const message = err instanceof Error ? err.message : String(err);
12361
- errors.push(`commitBatch failed: ${message} \u2014 falling back to per-file writes`);
12362
- for (const entry of batchEntries) {
12363
- try {
12364
- store.deleteRefsForFile(entry.file);
12365
- store.deleteSymbolsForFile(entry.file);
12366
- const symbolsWithIds = store.insertSymbols(entry.symbols);
12367
- symbolsIndexed += symbolsWithIds.length;
12368
- langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
12369
- filesIndexed++;
12370
- if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
12371
- const fallbackBatch = assignRefsToSymbols2(entry.refs, symbolsWithIds);
12372
- if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
12373
- }
12374
- store.resolveRefsForNames([
12375
- ...entry.symbols.map((symbol) => symbol.name),
12376
- ...entry.refs.map((ref) => ref.toName)
12377
- ]);
12378
- store.upsertFile({
12379
- file: entry.file,
12380
- lang: entry.lang,
12381
- mtimeMs: entry.mtimeMs,
12382
- symbolCount: entry.symbolCount,
12383
- lastIndexed: Date.now()
12384
- });
12385
- } catch (innerErr) {
12386
- errors.push(
12387
- `fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
12388
- );
12389
- }
12398
+ throw lastError;
12399
+ }
12400
+ connectOnce() {
12401
+ this.socket?.destroy();
12402
+ this.socket = null;
12403
+ this.info = null;
12404
+ this.activity = null;
12405
+ this.health = null;
12406
+ this.buffer = "";
12407
+ return new Promise((resolve17, reject) => {
12408
+ const socket = net3.createConnection(this.endpoint);
12409
+ this.socket = socket;
12410
+ socket.setEncoding("utf8");
12411
+ const timer = setTimeout(() => {
12412
+ reject(new Error("codebase-index server handshake timed out"));
12413
+ socket.destroy();
12414
+ }, CONNECT_ATTEMPT_TIMEOUT_MS);
12415
+ timer.unref?.();
12416
+ const finishResolve = () => {
12417
+ clearTimeout(timer);
12418
+ this.connectResolve = null;
12419
+ this.connectReject = null;
12420
+ resolve17();
12421
+ };
12422
+ const finishReject = (error) => {
12423
+ clearTimeout(timer);
12424
+ this.connectResolve = null;
12425
+ this.connectReject = null;
12426
+ reject(error);
12427
+ };
12428
+ this.connectResolve = finishResolve;
12429
+ this.connectReject = finishReject;
12430
+ socket.on("data", (chunk) => this.onData(socket, chunk));
12431
+ socket.on("error", (error) => {
12432
+ if (!this.info) finishReject(error);
12433
+ });
12434
+ socket.on("close", () => this.onClose(socket));
12435
+ });
12436
+ }
12437
+ onData(socket, chunk) {
12438
+ if (socket !== this.socket) return;
12439
+ this.buffer += chunk;
12440
+ while (true) {
12441
+ const newline = this.buffer.indexOf("\n");
12442
+ if (newline < 0) {
12443
+ if (this.buffer.length > PROJECT_INDEX_SERVER_MAX_FRAME_CHARS) {
12444
+ socket.destroy(new Error("codebase-index server response exceeds the IPC limit"));
12390
12445
  }
12446
+ return;
12447
+ }
12448
+ if (newline > PROJECT_INDEX_SERVER_MAX_FRAME_CHARS) {
12449
+ socket.destroy(new Error("codebase-index server response exceeds the IPC limit"));
12450
+ return;
12391
12451
  }
12452
+ const line = this.buffer.slice(0, newline);
12453
+ this.buffer = this.buffer.slice(newline + 1);
12454
+ if (!line) continue;
12455
+ let message;
12456
+ try {
12457
+ message = JSON.parse(line);
12458
+ } catch {
12459
+ socket.destroy(new Error("invalid codebase-index server response"));
12460
+ return;
12461
+ }
12462
+ this.onMessage(message);
12392
12463
  }
12393
12464
  }
12394
- if (discoveredFiles && discoveryComplete) {
12395
- for (const [file_] of existingMeta) {
12396
- if (!discoveredFiles.has(file_)) {
12397
- store.deleteFile(file_);
12465
+ onMessage(message) {
12466
+ if (message.type === "hello") {
12467
+ if (message.protocolVersion !== PROJECT_INDEX_SERVER_PROTOCOL_VERSION) {
12468
+ this.rejectStaleServer(
12469
+ message,
12470
+ `codebase-index protocol mismatch: client=${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}, server=${message.protocolVersion}`
12471
+ );
12472
+ return;
12473
+ }
12474
+ const expectedBuildId = projectIndexServerExpectedBuildId();
12475
+ if (expectedBuildId && message.buildId !== expectedBuildId) {
12476
+ this.rejectStaleServer(
12477
+ message,
12478
+ `codebase-index build mismatch: client=${expectedBuildId}, server=${message.buildId ?? "legacy"}`
12479
+ );
12480
+ return;
12398
12481
  }
12482
+ this.info = message;
12483
+ this.markResponsive();
12484
+ this.transition("connected", { pid: message.pid });
12485
+ ensureHeartbeatLoop();
12486
+ this.connectResolve?.();
12487
+ return;
12399
12488
  }
12489
+ if (message.type === "index-state") {
12490
+ this.activity = message.state;
12491
+ this.markResponsive();
12492
+ this.transition("connected", { pid: this.info?.pid });
12493
+ return;
12494
+ }
12495
+ const entry = this.pending.get(message.id);
12496
+ if (!entry) return;
12497
+ this.markResponsive();
12498
+ const status = connectionStates.get(this.endpoint)?.status;
12499
+ if (status === "degraded" || status === "unresponsive") {
12500
+ this.transition("connected", { pid: this.info?.pid });
12501
+ }
12502
+ if (message.type === "progress") {
12503
+ entry.onProgress?.(message.current, message.total);
12504
+ return;
12505
+ }
12506
+ this.pending.delete(message.id);
12507
+ this.cleanupPending(entry);
12508
+ if (message.ok) entry.resolve(message.result);
12509
+ else entry.reject(remoteError(message.error, message.errorName));
12400
12510
  }
12401
- if (needsFullRefResolution) store.resolveRefs();
12402
- store.setMetadata("ref_resolution_version", refResolutionVersion);
12403
- store.setMetadata("relation_graph_version", relationGraphVersion);
12404
- if (!opts.files || filesIndexed >= 50) store.optimize();
12405
- store.setLastIndexed(Date.now());
12406
- if (!opts.files) store.compactIfNeeded();
12407
- const durationMs = Date.now() - startMs;
12408
- return {
12409
- filesIndexed,
12410
- symbolsIndexed,
12411
- langStats,
12412
- durationMs,
12413
- errors
12414
- };
12415
- }
12416
-
12417
- // src/codebase-index/index-service.ts
12418
- async function indexService(args, hooks = {}) {
12419
- const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
12420
- try {
12421
- return await runIndexerWithStore(store, {
12422
- projectRoot: args.projectRoot,
12423
- indexDir: args.indexDir,
12424
- files: args.files,
12425
- force: args.force,
12426
- langs: args.langs,
12427
- ignore: args.ignore,
12428
- signal: hooks.signal,
12429
- onProgress: hooks.onProgress
12430
- });
12431
- } finally {
12432
- indexStorePool.release(store);
12433
- }
12434
- }
12435
- function searchService(args) {
12436
- const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
12437
- try {
12438
- return store.searchRanked(
12439
- args.query,
12440
- {
12441
- kind: args.kind,
12442
- lang: args.lang,
12443
- file: args.file,
12444
- lspKind: args.lspKind
12445
- },
12446
- args.limit
12447
- );
12448
- } finally {
12449
- indexStorePool.release(store);
12450
- }
12451
- }
12452
- function statsService(args) {
12453
- const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
12454
- try {
12455
- return store.getStats();
12456
- } finally {
12457
- indexStorePool.release(store);
12458
- }
12459
- }
12460
- function packageGraphService(args) {
12461
- const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
12462
- try {
12463
- return store.getPackageGraph();
12464
- } finally {
12465
- indexStorePool.release(store);
12466
- }
12467
- }
12468
- function fileGraphService(args) {
12469
- const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
12470
- try {
12471
- return store.getFileGraph(args.packageFilter);
12472
- } finally {
12473
- indexStorePool.release(store);
12474
- }
12475
- }
12476
- function symbolGraphService(args) {
12477
- const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
12478
- try {
12479
- return store.getSymbolGraph(args.fileFilter);
12480
- } finally {
12481
- indexStorePool.release(store);
12511
+ onClose(socket) {
12512
+ if (socket !== this.socket) return;
12513
+ const wasConnected = this.info !== null;
12514
+ this.socket = null;
12515
+ this.info = null;
12516
+ this.activity = null;
12517
+ this.health = null;
12518
+ const error = new Error("codebase-index server connection closed");
12519
+ this.connectReject?.(error);
12520
+ this.connectResolve = null;
12521
+ this.connectReject = null;
12522
+ this.rejectPending(error);
12523
+ if (wasConnected) this.transition("error", { error });
12524
+ maybeStopHeartbeatLoop();
12482
12525
  }
12483
- }
12484
-
12485
- // src/codebase-index/background-indexer.ts
12486
- init_languages2();
12487
-
12488
- // src/codebase-index/project-server-client.ts
12489
- import { spawn as spawn7 } from "node:child_process";
12490
- import * as fs17 from "node:fs";
12491
- import * as net3 from "node:net";
12492
- import { fileURLToPath as fileURLToPath2 } from "node:url";
12493
-
12494
- // src/codebase-index/project-server-endpoint.ts
12495
- import { createHash as createHash4 } from "node:crypto";
12496
- import * as fs16 from "node:fs";
12497
- import * as os7 from "node:os";
12498
- import * as path23 from "node:path";
12499
- import { fileURLToPath } from "node:url";
12500
- var PROJECT_INDEX_SERVER_PROTOCOL_VERSION = 1;
12501
- var PROJECT_INDEX_SERVER_METADATA_FILE = "server.json";
12502
- var buildIdCache;
12503
- function projectIndexServerBuildId(entrypoint) {
12504
- const file = entrypoint instanceof URL || entrypoint.startsWith("file:") ? fileURLToPath(entrypoint) : path23.resolve(entrypoint);
12505
- try {
12506
- const stat18 = fs16.statSync(file);
12507
- if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat18.mtimeMs && buildIdCache.size === stat18.size) {
12508
- return buildIdCache.buildId;
12526
+ cleanupPending(entry) {
12527
+ clearTimeout(entry.timer);
12528
+ if (entry.signal && entry.onAbort) {
12529
+ entry.signal.removeEventListener("abort", entry.onAbort);
12509
12530
  }
12510
- const buildId = createHash4("sha256").update(fs16.readFileSync(file)).digest("hex").slice(0, 24);
12511
- buildIdCache = { file, mtimeMs: stat18.mtimeMs, size: stat18.size, buildId };
12512
- return buildId;
12513
- } catch {
12514
- return `unreadable:${path23.basename(file)}`;
12515
- }
12516
- }
12517
- function normalizeLocalPath(value) {
12518
- const resolved = path23.resolve(value);
12519
- return process.platform === "win32" ? resolved.toLowerCase() : resolved;
12520
- }
12521
- function projectIndexServerKey(projectRoot, indexDir) {
12522
- const resolvedIndexDir = normalizeLocalPath(resolveIndexDir(projectRoot, indexDir));
12523
- return createHash4("sha256").update(resolvedIndexDir).digest("hex").slice(0, 24);
12524
- }
12525
- function projectIndexServerEndpoint(projectRoot, indexDir) {
12526
- const key = projectIndexServerKey(projectRoot, indexDir);
12527
- if (process.platform === "win32") {
12528
- return `\\\\.\\pipe\\wrongstack-codebase-index-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}-${key}`;
12529
12531
  }
12530
- return path23.join(
12531
- os7.tmpdir(),
12532
- `wrongstack-codebase-index-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}`,
12533
- `${key}.sock`
12534
- );
12535
- }
12536
- function projectIndexServerMetadataPath(projectRoot, indexDir) {
12537
- return path23.join(
12538
- path23.resolve(resolveIndexDir(projectRoot, indexDir)),
12539
- PROJECT_INDEX_SERVER_METADATA_FILE
12540
- );
12541
- }
12542
-
12543
- // src/codebase-index/project-server-protocol.ts
12544
- var PROJECT_INDEX_SERVER_MAX_FRAME_CHARS = 64 * 1024 * 1024;
12545
- function encodeProjectServerMessage(message) {
12546
- return `${JSON.stringify(message)}
12547
- `;
12548
- }
12549
-
12550
- // src/codebase-index/project-server-client.ts
12551
- var CONNECT_ATTEMPT_TIMEOUT_MS = 750;
12552
- var SERVER_START_TIMEOUT_MS = 1e4;
12553
- var SERVER_CONTROL_TIMEOUT_MS = 5e3;
12554
- var SERVER_HEALTH_TIMEOUT_MS = 3e3;
12555
- var SERVER_HEARTBEAT_INTERVAL_MS = 1e4;
12556
- var StaleProjectIndexServerError = class extends Error {
12557
- constructor(message, pid) {
12558
- super(message);
12559
- this.pid = pid;
12532
+ rejectPending(error) {
12533
+ const entries = [...this.pending.values()];
12534
+ this.pending.clear();
12535
+ for (const entry of entries) {
12536
+ this.cleanupPending(entry);
12537
+ entry.reject(error);
12538
+ }
12560
12539
  }
12561
- pid;
12562
- name = "StaleProjectIndexServerError";
12563
- };
12564
- var connectionStates = /* @__PURE__ */ new Map();
12565
- var connectionStateListeners = /* @__PURE__ */ new Set();
12566
- var latestConnectionState = {
12567
- status: "offline",
12568
- connected: false
12569
- };
12570
- function resolveProjectIndexDaemonAvailability() {
12571
- if (process.env["WRONGSTACK_INDEX_INLINE"] || process.env["WRONGSTACK_INDEX_SERVER"] === "0") {
12572
- return { kind: "inline-requested" };
12540
+ write(message) {
12541
+ const socket = this.socket;
12542
+ if (socket && !socket.destroyed) socket.write(encodeProjectServerMessage(message));
12573
12543
  }
12574
- for (const rel of ["./project-server.js", "./codebase-index/project-server.js"]) {
12544
+ rejectStaleServer(message, reason) {
12545
+ const socket = this.socket;
12546
+ if (socket && !socket.destroyed) {
12547
+ socket.write(
12548
+ encodeProjectServerMessage({
12549
+ type: "shutdown",
12550
+ id: 0,
12551
+ reason: "stale-build-replacement"
12552
+ })
12553
+ );
12554
+ const timer = setTimeout(() => socket.destroy(), 25);
12555
+ timer.unref?.();
12556
+ }
12557
+ this.connectReject?.(new StaleProjectIndexServerError(reason, message.pid));
12558
+ }
12559
+ spawnDetachedServer() {
12560
+ const url = resolveProjectServerUrl();
12561
+ if (!url) throw new Error("built codebase-index project server is unavailable");
12562
+ if (process.platform !== "win32") {
12563
+ try {
12564
+ fs12.rmSync(this.endpoint, { force: true });
12565
+ } catch {
12566
+ }
12567
+ }
12568
+ const args = [fileURLToPath2(url), "--project-root", this.projectRoot];
12569
+ if (this.indexDir) args.push("--index-dir", this.indexDir);
12570
+ const child = spawn4(process.execPath, args, {
12571
+ detached: true,
12572
+ stdio: "ignore",
12573
+ windowsHide: true,
12574
+ env: process.env
12575
+ });
12576
+ child.unref();
12577
+ }
12578
+ forceKillKnownServer() {
12579
+ const pid = this.info?.pid;
12580
+ return pid ? this.forceKillServer(pid) : false;
12581
+ }
12582
+ forceKillServer(pid) {
12583
+ if (pid === process.pid) return false;
12575
12584
  try {
12576
- const url = new URL(rel, import.meta.url);
12577
- if (url.protocol === "file:" && fs17.existsSync(fileURLToPath2(url))) {
12578
- return { kind: "available", url };
12585
+ process.kill(pid);
12586
+ const metadataPath = projectIndexServerMetadataPath(this.projectRoot, this.indexDir);
12587
+ try {
12588
+ const metadata = JSON.parse(fs12.readFileSync(metadataPath, "utf8"));
12589
+ if (metadata.pid === pid) fs12.rmSync(metadataPath, { force: true });
12590
+ } catch {
12579
12591
  }
12592
+ return true;
12580
12593
  } catch {
12594
+ return false;
12581
12595
  }
12582
12596
  }
12583
- return { kind: "missing-build" };
12584
- }
12585
- function resolveProjectServerUrl() {
12586
- const availability = resolveProjectIndexDaemonAvailability();
12587
- return availability.kind === "available" ? availability.url : null;
12588
- }
12589
- function projectIndexServerExpectedBuildId() {
12590
- const override = process.env["WRONGSTACK_INDEX_SERVER_BUILD_ID"]?.trim();
12591
- if (override) return override;
12592
- const url = resolveProjectServerUrl();
12593
- return url ? projectIndexServerBuildId(url) : null;
12594
- }
12595
- function isProjectIndexServerAvailable() {
12596
- return resolveProjectServerUrl() !== null;
12597
+ };
12598
+ var connections = /* @__PURE__ */ new Map();
12599
+ var heartbeatTimer;
12600
+ function ensureHeartbeatLoop() {
12601
+ if (heartbeatTimer) return;
12602
+ heartbeatTimer = setInterval(() => {
12603
+ for (const connection of connections.values()) {
12604
+ if (connection.isConnected()) void connection.checkHealth(false).catch(() => {
12605
+ });
12606
+ }
12607
+ }, SERVER_HEARTBEAT_INTERVAL_MS);
12608
+ heartbeatTimer.unref?.();
12597
12609
  }
12598
- function publishConnectionState(endpoint, state) {
12599
- connectionStates.set(endpoint, state);
12600
- latestConnectionState = state;
12601
- for (const listener of connectionStateListeners) listener(state);
12610
+ function maybeStopHeartbeatLoop() {
12611
+ if (!heartbeatTimer) return;
12612
+ if ([...connections.values()].some((connection) => connection.isConnected())) return;
12613
+ clearInterval(heartbeatTimer);
12614
+ heartbeatTimer = void 0;
12602
12615
  }
12603
- function getProjectIndexServerConnectionState(projectRoot, indexDir) {
12604
- if (projectRoot) {
12605
- const endpoint = projectIndexServerEndpoint(projectRoot, indexDir);
12606
- const existing = connectionStates.get(endpoint);
12607
- if (existing) return existing;
12608
- if (!isProjectIndexServerAvailable()) {
12609
- return { status: "unavailable", connected: false };
12610
- }
12611
- return {
12612
- status: "offline",
12613
- connected: false,
12614
- projectRoot,
12615
- indexDir,
12616
- endpoint
12617
- };
12616
+ function connectionFor(projectRoot, indexDir) {
12617
+ const endpoint = projectIndexServerEndpoint(projectRoot, indexDir);
12618
+ let connection = connections.get(endpoint);
12619
+ if (!connection) {
12620
+ connection = new ProjectServerConnection(projectRoot, indexDir, endpoint);
12621
+ connections.set(endpoint, connection);
12618
12622
  }
12619
- if (latestConnectionState.endpoint) return latestConnectionState;
12620
- if (!isProjectIndexServerAvailable()) return { status: "unavailable", connected: false };
12621
- return latestConnectionState;
12623
+ return connection;
12622
12624
  }
12623
- function onProjectIndexServerConnectionStateChange(listener) {
12624
- connectionStateListeners.add(listener);
12625
- return () => connectionStateListeners.delete(listener);
12625
+ function callProjectIndexServer(op, args, options) {
12626
+ return connectionFor(args.projectRoot, args.indexDir).call(op, args, options);
12626
12627
  }
12627
- function remoteError(message, name) {
12628
- if (name === "LockError") return new LockError(message);
12629
- if (name === "IndexTimeoutError") return new IndexTimeoutError(message);
12630
- const error = new Error(message);
12631
- if (name && name !== "Error") error.name = name;
12632
- return error;
12628
+ function ensureProjectIndexServer(options) {
12629
+ return connectionFor(options.projectRoot, options.indexDir).configure(
12630
+ options.watchExternal,
12631
+ options.debounceMs
12632
+ );
12633
12633
  }
12634
- function isProjectIndexServerHealth(value) {
12635
- if (!value || typeof value !== "object") return false;
12636
- const health = value;
12637
- const memory = health.memory && typeof health.memory === "object" ? health.memory : void 0;
12638
- const activity = health.activity && typeof health.activity === "object" ? health.activity : void 0;
12639
- return typeof health.checkedAt === "number" && typeof health.uptimeMs === "number" && typeof memory?.rss === "number" && typeof memory.heapUsed === "number" && typeof memory.heapTotal === "number" && typeof memory.external === "number" && typeof health.clients === "number" && typeof health.activeRequests === "number" && typeof health.activeWrites === "number" && typeof health.queuedWrites === "number" && typeof health.pendingExternalFiles === "number" && typeof health.watchingExternal === "boolean" && typeof activity?.indexing === "boolean" && typeof activity.currentFile === "number" && typeof activity.totalFiles === "number" && typeof activity.generation === "number";
12634
+ function checkProjectIndexServerHealth(projectRoot, indexDir, options = {}) {
12635
+ return connectionFor(projectRoot, indexDir).checkHealth(
12636
+ false,
12637
+ options.timeoutMs ?? SERVER_HEALTH_TIMEOUT_MS
12638
+ );
12640
12639
  }
12641
- function delay(ms) {
12642
- return new Promise((resolve17) => {
12643
- const timer = setTimeout(resolve17, ms);
12644
- timer.unref?.();
12645
- });
12640
+ async function shutdownProjectIndexServer(projectRoot, indexDir, reason) {
12641
+ const endpoint = projectIndexServerEndpoint(projectRoot, indexDir);
12642
+ const connection = connectionFor(projectRoot, indexDir);
12643
+ try {
12644
+ return await connection.shutdownRemote(reason);
12645
+ } finally {
12646
+ connection.close();
12647
+ connections.delete(endpoint);
12648
+ connectionStates.delete(endpoint);
12649
+ }
12646
12650
  }
12647
- function cancellationError(signal) {
12648
- return signal.reason instanceof Error ? signal.reason : new Error("Indexing cancelled");
12651
+ function closeProjectIndexServerClients() {
12652
+ for (const connection of connections.values()) connection.close();
12653
+ connections.clear();
12654
+ connectionStates.clear();
12655
+ latestConnectionState = {
12656
+ status: isProjectIndexServerAvailable() ? "offline" : "unavailable",
12657
+ connected: false
12658
+ };
12659
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
12660
+ heartbeatTimer = void 0;
12649
12661
  }
12650
- var ProjectServerConnection = class {
12651
- constructor(projectRoot, indexDir, endpoint) {
12652
- this.projectRoot = projectRoot;
12653
- this.indexDir = indexDir;
12654
- this.endpoint = endpoint;
12655
- this.transition("offline");
12656
- }
12657
- projectRoot;
12658
- indexDir;
12659
- endpoint;
12660
- socket = null;
12661
- buffer = "";
12662
- info = null;
12663
- activity = null;
12664
- health = null;
12665
- healthCheck = null;
12666
- connecting = null;
12667
- connectResolve = null;
12668
- connectReject = null;
12669
- nextId = 1;
12670
- pending = /* @__PURE__ */ new Map();
12671
- transition(status, options = {}) {
12672
- const previous = connectionStates.get(this.endpoint);
12673
- const pid = options.pid ?? (status === "connected" ? this.info?.pid : void 0);
12674
- const lastError = options.error === void 0 ? status === "error" || status === "degraded" || status === "unresponsive" ? previous?.lastError : void 0 : options.error instanceof Error ? options.error.message : String(options.error);
12675
- publishConnectionState(this.endpoint, {
12676
- status,
12677
- connected: status === "connected" || status === "degraded" || status === "unresponsive",
12678
- projectRoot: this.projectRoot,
12679
- indexDir: this.indexDir,
12680
- endpoint: this.endpoint,
12681
- pid,
12682
- lastError,
12683
- ...this.activity ? { activity: this.activity } : {},
12684
- ...this.health ? { health: this.health } : {}
12685
- });
12686
- }
12687
- isConnected() {
12688
- return this.socket !== null && !this.socket.destroyed && this.info !== null;
12689
- }
12690
- async checkHealth(spawnIfMissing = false, timeoutMs = SERVER_HEALTH_TIMEOUT_MS) {
12691
- await this.ensureConnected(spawnIfMissing);
12692
- if (this.healthCheck) return this.healthCheck;
12693
- const startedAt = Date.now();
12694
- this.healthCheck = this.request({ type: "ping" }, { timeoutMs }).then((server) => {
12695
- const now2 = Date.now();
12696
- this.health = {
12697
- status: "healthy",
12698
- checkedAt: now2,
12699
- lastHealthyAt: now2,
12700
- latencyMs: Math.max(0, now2 - startedAt),
12701
- missedHeartbeats: 0,
12702
- ...isProjectIndexServerHealth(server) ? { server } : {}
12703
- };
12704
- this.transition("connected", { pid: this.info?.pid });
12705
- return this.health;
12706
- }).catch((error) => {
12707
- if (!this.isConnected()) throw error;
12708
- if ((this.health?.lastHealthyAt ?? 0) > startedAt) return this.health;
12709
- const missedHeartbeats = (this.health?.missedHeartbeats ?? 0) + 1;
12710
- const status = missedHeartbeats >= 3 ? "unresponsive" : "degraded";
12711
- this.health = {
12712
- status,
12713
- checkedAt: Date.now(),
12714
- lastHealthyAt: this.health?.lastHealthyAt ?? null,
12715
- latencyMs: null,
12716
- missedHeartbeats,
12717
- ...this.health?.server ? { server: this.health.server } : {}
12718
- };
12719
- this.transition(status, { pid: this.info?.pid, error });
12720
- return this.health;
12721
- }).finally(() => {
12722
- this.healthCheck = null;
12662
+
12663
+ // src/codebase-index/background-indexer.ts
12664
+ import * as fs18 from "node:fs";
12665
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
12666
+ import { Worker } from "node:worker_threads";
12667
+
12668
+ // src/codebase-index/indexer.ts
12669
+ import { expectDefined as expectDefined6 } from "@wrongstack/core/utils";
12670
+ import { execFile as execFile2 } from "node:child_process";
12671
+ import * as fs17 from "node:fs/promises";
12672
+ import { availableParallelism } from "node:os";
12673
+ import * as path23 from "node:path";
12674
+ import {
12675
+ DEFAULT_WALK_IGNORE_DIRS,
12676
+ indexParallelBatchSize,
12677
+ isFrugalPerf
12678
+ } from "@wrongstack/core/utils";
12679
+
12680
+ // src/codebase-index/gitignore.ts
12681
+ import * as fs13 from "node:fs/promises";
12682
+ import * as path17 from "node:path";
12683
+ import { compileGlob } from "@wrongstack/core/utils";
12684
+ function globBody(glob) {
12685
+ return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
12686
+ }
12687
+ function compileGitignore(lines) {
12688
+ const rules = [];
12689
+ for (const raw of lines) {
12690
+ let line = raw.replace(/\r$/, "");
12691
+ if (!line.trim() || line.trimStart().startsWith("#")) continue;
12692
+ line = line.trim();
12693
+ let negated = false;
12694
+ if (line.startsWith("!")) {
12695
+ negated = true;
12696
+ line = line.slice(1);
12697
+ }
12698
+ let dirOnly = false;
12699
+ if (line.endsWith("/")) {
12700
+ dirOnly = true;
12701
+ line = line.slice(0, -1);
12702
+ }
12703
+ if (!line) continue;
12704
+ const anchored = line.startsWith("/") || line.includes("/");
12705
+ if (line.startsWith("/")) line = line.slice(1);
12706
+ const body = globBody(line);
12707
+ const prefix = anchored ? "^" : "(?:^|.*/)";
12708
+ rules.push({
12709
+ eqOrUnder: new RegExp(`${prefix}${body}(?:/.*)?$`),
12710
+ under: new RegExp(`${prefix}${body}/.*$`),
12711
+ negated,
12712
+ dirOnly
12723
12713
  });
12724
- return this.healthCheck;
12725
- }
12726
- markResponsive() {
12727
- const now2 = Date.now();
12728
- this.health = {
12729
- status: "healthy",
12730
- checkedAt: now2,
12731
- lastHealthyAt: now2,
12732
- latencyMs: this.health?.latencyMs ?? null,
12733
- missedHeartbeats: 0,
12734
- ...this.health?.server ? { server: this.health.server } : {}
12735
- };
12736
12714
  }
12737
- async call(op, args, options) {
12738
- if (options.signal?.aborted) throw cancellationError(options.signal);
12739
- await this.ensureConnected(true);
12740
- if (options.signal?.aborted) throw cancellationError(options.signal);
12741
- return this.request({ type: "request", op, args }, options);
12715
+ return (relPath, isDir) => {
12716
+ const p = relPath.replace(/\\/g, "/").replace(/^\/+/, "");
12717
+ let ignored = false;
12718
+ for (const r of rules) {
12719
+ const re = r.dirOnly && !isDir ? r.under : r.eqOrUnder;
12720
+ if (re.test(p)) ignored = !r.negated;
12721
+ }
12722
+ return ignored;
12723
+ };
12724
+ }
12725
+ async function loadGitignoreMatcher(projectRoot) {
12726
+ let lines = [];
12727
+ try {
12728
+ const raw = await fs13.readFile(path17.join(projectRoot, ".gitignore"), "utf8");
12729
+ lines = raw.split("\n");
12730
+ } catch {
12742
12731
  }
12743
- async shutdownRemote(reason) {
12744
- try {
12745
- await this.ensureConnected(false);
12746
- } catch {
12747
- return { stopped: false, reason: "not-running" };
12732
+ return compileGitignore(lines);
12733
+ }
12734
+
12735
+ // src/codebase-index/indexer.ts
12736
+ init_languages2();
12737
+
12738
+ // src/codebase-index/parser-dispatch.ts
12739
+ async function parseFileContent(file, content, lang) {
12740
+ switch (lang) {
12741
+ case "ts":
12742
+ case "tsx":
12743
+ case "js":
12744
+ case "jsx": {
12745
+ const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_ts_parser(), ts_parser_exports));
12746
+ return parseSymbols8({ file, content, lang });
12748
12747
  }
12749
- const pid = this.info?.pid;
12750
- try {
12751
- this.transition("stopping", { pid });
12752
- await this.request(
12753
- { type: "shutdown", reason },
12754
- { timeoutMs: SERVER_CONTROL_TIMEOUT_MS }
12755
- );
12756
- return { stopped: true, pid };
12757
- } catch (error) {
12758
- const forceKilled = this.forceKillKnownServer();
12759
- return {
12760
- stopped: forceKilled,
12761
- pid,
12762
- reason: forceKilled ? `force-killed after graceful shutdown failed: ${error instanceof Error ? error.message : String(error)}` : error instanceof Error ? error.message : String(error)
12763
- };
12764
- } finally {
12765
- this.close();
12748
+ case "go": {
12749
+ const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_go_parser(), go_parser_exports));
12750
+ return parseSymbols8({ file, content, lang: "go" });
12751
+ }
12752
+ case "py": {
12753
+ const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_py_parser(), py_parser_exports));
12754
+ return parseSymbols8({ file, content, lang: "py" });
12755
+ }
12756
+ case "rs": {
12757
+ const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_rs_parser(), rs_parser_exports));
12758
+ return parseSymbols8({ file, content, lang: "rs" });
12759
+ }
12760
+ case "json": {
12761
+ const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_json_parser(), json_parser_exports));
12762
+ return parseSymbols8({ file, content, lang: "json" });
12763
+ }
12764
+ case "yaml": {
12765
+ const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_yaml_parser(), yaml_parser_exports));
12766
+ return parseSymbols8({ file, content, lang: "yaml" });
12767
+ }
12768
+ default: {
12769
+ const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_generic_parser(), generic_parser_exports));
12770
+ return parseSymbols8({ file, content, lang });
12766
12771
  }
12767
12772
  }
12768
- async configure(watchExternal, debounceMs) {
12769
- await this.ensureConnected(true);
12770
- const startedAt = Date.now();
12771
- const result = await this.request(
12772
- { type: "configure", watchExternal, debounceMs },
12773
- { timeoutMs: SERVER_CONTROL_TIMEOUT_MS }
12773
+ }
12774
+
12775
+ // src/codebase-index/indexer.ts
12776
+ var YIELD_EVERY_N = 50;
12777
+ function resolveParallelBatch() {
12778
+ return indexParallelBatchSize(availableParallelism());
12779
+ }
12780
+ function yieldEventLoop() {
12781
+ return new Promise((resolve17) => setImmediate(resolve17));
12782
+ }
12783
+ function throwIfAborted(signal) {
12784
+ if (!signal?.aborted) return;
12785
+ if (signal.reason instanceof Error) throw signal.reason;
12786
+ throw new Error(typeof signal.reason === "string" ? signal.reason : "Indexing cancelled");
12787
+ }
12788
+ function isAbortError(err) {
12789
+ return err instanceof DOMException && err.name === "AbortError";
12790
+ }
12791
+ var DEFAULT_IGNORE = DEFAULT_WALK_IGNORE_DIRS;
12792
+ var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "pnpm-lock.yml"]);
12793
+ var INDEXABLE_EXTENSION_SET = new Set(INDEXABLE_EXTENSIONS);
12794
+ var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
12795
+ var MAX_GIT_FILE_LIST_BYTES = 64 * 1024 * 1024;
12796
+ function isWithinProject(projectRoot, file) {
12797
+ const rel = path23.relative(projectRoot, file);
12798
+ return rel !== "" && !rel.startsWith(`..${path23.sep}`) && rel !== ".." && !path23.isAbsolute(rel);
12799
+ }
12800
+ function isMissingPathError(err) {
12801
+ const code = err?.code;
12802
+ return code === "ENOENT" || code === "ENOTDIR";
12803
+ }
12804
+ function normalizeComparablePath(value) {
12805
+ const resolved = path23.resolve(value);
12806
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
12807
+ }
12808
+ function gitOutput(projectRoot, args) {
12809
+ return new Promise((resolve17, reject) => {
12810
+ execFile2(
12811
+ "git",
12812
+ ["-C", projectRoot, ...args],
12813
+ {
12814
+ encoding: "buffer",
12815
+ maxBuffer: MAX_GIT_FILE_LIST_BYTES,
12816
+ windowsHide: true
12817
+ },
12818
+ (error, stdout) => {
12819
+ if (error) reject(error);
12820
+ else resolve17(Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout));
12821
+ }
12774
12822
  );
12775
- if (isProjectIndexServerHealth(result.health)) {
12776
- const now2 = Date.now();
12777
- this.health = {
12778
- status: "healthy",
12779
- checkedAt: now2,
12780
- lastHealthyAt: now2,
12781
- latencyMs: Math.max(0, now2 - startedAt),
12782
- missedHeartbeats: 0,
12783
- server: result.health
12784
- };
12785
- this.transition("connected", { pid: this.info?.pid });
12823
+ });
12824
+ }
12825
+ async function findGitSourceFiles(projectRoot, ignore, signal) {
12826
+ try {
12827
+ throwIfAborted(signal);
12828
+ const topLevel = (await gitOutput(projectRoot, ["rev-parse", "--show-toplevel"])).toString("utf8").trim();
12829
+ if (normalizeComparablePath(topLevel) !== normalizeComparablePath(projectRoot)) return null;
12830
+ throwIfAborted(signal);
12831
+ const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
12832
+ const [output, statusOutput] = await Promise.all([
12833
+ gitOutput(projectRoot, ["ls-files", "--cached", "--others", "--exclude-standard", "-z"]),
12834
+ gitOutput(projectRoot, [
12835
+ "status",
12836
+ "--porcelain=v1",
12837
+ "-z",
12838
+ "--untracked-files=all",
12839
+ "--ignored=no"
12840
+ ])
12841
+ ]);
12842
+ throwIfAborted(signal);
12843
+ const dirty = /* @__PURE__ */ new Set();
12844
+ const deleted = /* @__PURE__ */ new Set();
12845
+ const statusRecords = statusOutput.toString("utf8").split("\0");
12846
+ for (let i = 0; i < statusRecords.length; i++) {
12847
+ const record = statusRecords[i];
12848
+ if (!record) continue;
12849
+ const status = record.slice(0, 2);
12850
+ const changedPath = path23.resolve(projectRoot, record.slice(3));
12851
+ dirty.add(changedPath);
12852
+ if (status.includes("D")) deleted.add(changedPath);
12853
+ if (status.includes("R") || status.includes("C")) {
12854
+ const source = statusRecords[++i];
12855
+ if (source) dirty.add(path23.resolve(projectRoot, source));
12856
+ }
12786
12857
  }
12858
+ const files = [];
12859
+ for (const relative13 of output.toString("utf8").split("\0")) {
12860
+ if (!relative13) continue;
12861
+ const portable = relative13.replace(/\\/g, "/");
12862
+ if (portable.split("/").some((segment) => ignoreSet.has(segment)) || DEFAULT_IGNORE_FILES.has(path23.posix.basename(portable))) {
12863
+ continue;
12864
+ }
12865
+ const full = path23.resolve(projectRoot, relative13);
12866
+ if (deleted.has(full)) continue;
12867
+ const ext = path23.extname(relative13).toLowerCase();
12868
+ if (INDEXABLE_EXTENSION_SET.has(ext) || detectLang(full) !== null) files.push(full);
12869
+ }
12870
+ return {
12871
+ files,
12872
+ trustedUnchanged: new Set(files.filter((file) => !dirty.has(file)))
12873
+ };
12874
+ } catch {
12875
+ return null;
12787
12876
  }
12788
- close() {
12789
- const socket = this.socket;
12790
- this.socket = null;
12791
- this.info = null;
12792
- this.activity = null;
12793
- this.health = null;
12794
- this.connectReject?.(new Error("codebase-index client disconnected"));
12795
- this.connectResolve = null;
12796
- this.connectReject = null;
12797
- if (socket && !socket.destroyed) socket.destroy();
12798
- this.rejectPending(new Error("codebase-index client disconnected"));
12799
- this.transition("offline");
12800
- maybeStopHeartbeatLoop();
12801
- }
12802
- request(message, options) {
12803
- const socket = this.socket;
12804
- if (!socket || socket.destroyed) {
12805
- return Promise.reject(new Error("codebase-index server connection is not available"));
12877
+ }
12878
+ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
12879
+ const gitFiles = await findGitSourceFiles(projectRoot, ignore, signal);
12880
+ if (gitFiles) {
12881
+ return {
12882
+ files: gitFiles.files,
12883
+ complete: true,
12884
+ errors: [],
12885
+ trustedUnchanged: gitFiles.trustedUnchanged
12886
+ };
12887
+ }
12888
+ const results = [];
12889
+ const errors = [];
12890
+ let complete = true;
12891
+ const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
12892
+ const indexableExts = new Set(INDEXABLE_EXTENSIONS);
12893
+ let dirCount = 0;
12894
+ const walk2 = async (dir) => {
12895
+ throwIfAborted(signal);
12896
+ if (dirCount > 0 && dirCount % YIELD_EVERY_N === 0) {
12897
+ await yieldEventLoop();
12898
+ throwIfAborted(signal);
12806
12899
  }
12807
- const id = this.nextId++;
12808
- return new Promise((resolve17, reject) => {
12809
- const timer = setTimeout(() => {
12810
- const entry = this.pending.get(id);
12811
- if (!entry) return;
12812
- this.pending.delete(id);
12813
- this.write({ type: "cancel", id });
12814
- const error = new IndexTimeoutError(
12815
- `Index ${message.type === "request" ? message.op : message.type} exceeded its ${options.timeoutMs}ms watchdog timeout`
12816
- );
12817
- this.cleanupPending(entry);
12818
- entry.reject(error);
12819
- }, options.timeoutMs);
12820
- timer.unref?.();
12821
- const signal = options.signal;
12822
- const onAbort = signal ? () => {
12823
- const entry = this.pending.get(id);
12824
- if (!entry) return;
12825
- this.pending.delete(id);
12826
- this.write({ type: "cancel", id });
12827
- this.cleanupPending(entry);
12828
- entry.reject(cancellationError(signal));
12829
- } : void 0;
12830
- this.pending.set(id, {
12831
- resolve: resolve17,
12832
- reject,
12833
- timer,
12834
- signal,
12835
- onAbort,
12836
- onProgress: options.onProgress
12837
- });
12838
- if (signal && onAbort) {
12839
- signal.addEventListener("abort", onAbort, { once: true });
12840
- if (signal.aborted) {
12841
- onAbort();
12842
- return;
12900
+ let entries;
12901
+ try {
12902
+ entries = await fs17.readdir(dir, { withFileTypes: true });
12903
+ } catch (err) {
12904
+ complete = false;
12905
+ errors.push(`scan error: ${dir}: ${err instanceof Error ? err.message : String(err)}`);
12906
+ return;
12907
+ }
12908
+ dirCount++;
12909
+ for (const e of entries) {
12910
+ if (ignoreSet.has(e.name)) continue;
12911
+ const full = path23.join(dir, e.name);
12912
+ const rel = path23.relative(projectRoot, full).replace(/\\/g, "/");
12913
+ if (e.isDirectory()) {
12914
+ if (isGitIgnored(rel, true)) continue;
12915
+ await walk2(full);
12916
+ } else if (e.isFile()) {
12917
+ if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
12918
+ const ext = path23.extname(e.name).toLowerCase();
12919
+ if (indexableExts.has(ext) || detectLang(full) !== null) {
12920
+ results.push(full);
12843
12921
  }
12844
12922
  }
12845
- this.write({ ...message, id });
12923
+ }
12924
+ };
12925
+ await walk2(projectRoot);
12926
+ return { files: results, complete, errors };
12927
+ }
12928
+ function assignRefsToSymbols2(refs, symbols) {
12929
+ if (refs.length === 0 || symbols.length === 0) return [];
12930
+ const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
12931
+ const seen = /* @__PURE__ */ new Set();
12932
+ const assigned = [];
12933
+ for (const ref of refs) {
12934
+ let owner2;
12935
+ for (const symbol of ordered) {
12936
+ if (symbol.line > ref.line) break;
12937
+ owner2 = symbol;
12938
+ }
12939
+ if (!owner2 && ref.callType === "import") owner2 = ordered[0];
12940
+ if (!owner2 || owner2.id <= 0) continue;
12941
+ const key = `${owner2.id}:${ref.toName}:${ref.callType}`;
12942
+ if (seen.has(key)) continue;
12943
+ seen.add(key);
12944
+ assigned.push({ ...ref, fromId: owner2.id });
12945
+ }
12946
+ return assigned;
12947
+ }
12948
+ async function runIndexerWithStore(store, opts) {
12949
+ const { projectRoot, langs, ignore = [], signal } = opts;
12950
+ const relationGraphVersion = "2";
12951
+ const refResolutionVersion = "2";
12952
+ const force = (opts.force ?? false) || store.getMetadata("relation_graph_version") !== relationGraphVersion;
12953
+ const needsFullRefResolution = force || store.getMetadata("ref_resolution_version") !== refResolutionVersion;
12954
+ const startMs = Date.now();
12955
+ const errors = [];
12956
+ const langStats = {};
12957
+ let filesIndexed = 0;
12958
+ let symbolsIndexed = 0;
12959
+ const isGitIgnored = await loadGitignoreMatcher(projectRoot);
12960
+ let files;
12961
+ let discoveredFiles = null;
12962
+ let discoveryComplete = true;
12963
+ let trustedUnchanged;
12964
+ if (opts.files && opts.files.length > 0) {
12965
+ files = opts.files.map((f) => path23.resolve(projectRoot, f)).filter((f) => {
12966
+ if (!isWithinProject(projectRoot, f)) return false;
12967
+ const rel = path23.relative(projectRoot, f).replace(/\\/g, "/");
12968
+ return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path23.basename(f)) && !isGitIgnored(rel, false);
12846
12969
  });
12970
+ } else {
12971
+ const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
12972
+ files = discovery.files;
12973
+ errors.push(...discovery.errors);
12974
+ discoveryComplete = discovery.complete;
12975
+ discoveredFiles = new Set(files);
12976
+ trustedUnchanged = discovery.trustedUnchanged;
12847
12977
  }
12848
- async ensureConnected(spawnIfMissing) {
12849
- if (this.socket && !this.socket.destroyed && this.info) return;
12850
- if (this.connecting) return this.connecting;
12851
- this.transition("connecting");
12852
- this.connecting = this.connectWithElection(spawnIfMissing).catch((error) => {
12853
- this.transition("error", { error });
12854
- throw error;
12855
- }).finally(() => {
12856
- this.connecting = null;
12978
+ if (langs && langs.length > 0) {
12979
+ const langSet = new Set(langs);
12980
+ files = files.filter((f) => {
12981
+ const lang = detectLang(f);
12982
+ return lang ? langSet.has(lang) : false;
12857
12983
  });
12858
- return this.connecting;
12859
12984
  }
12860
- async connectWithElection(spawnIfMissing) {
12861
- const deadline = Date.now() + (spawnIfMissing ? SERVER_START_TIMEOUT_MS : CONNECT_ATTEMPT_TIMEOUT_MS);
12862
- let spawned = false;
12863
- let staleAttempts = 0;
12864
- let lastError = new Error("codebase-index server unavailable");
12865
- while (Date.now() < deadline) {
12866
- try {
12867
- await this.connectOnce();
12868
- return;
12869
- } catch (error) {
12870
- lastError = error;
12871
- if (error instanceof StaleProjectIndexServerError) {
12872
- staleAttempts++;
12873
- if (!spawnIfMissing) break;
12874
- if (staleAttempts >= 3) this.forceKillServer(error.pid);
12875
- spawned = false;
12876
- await delay(100);
12877
- continue;
12878
- }
12879
- }
12880
- if (!spawnIfMissing) break;
12881
- if (!spawned) {
12882
- this.spawnDetachedServer();
12883
- spawned = true;
12884
- }
12885
- await delay(75);
12886
- }
12887
- throw lastError;
12985
+ if (force) store.clearAll();
12986
+ const existingMeta = /* @__PURE__ */ new Map();
12987
+ if (!force) {
12988
+ for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
12888
12989
  }
12889
- connectOnce() {
12890
- this.socket?.destroy();
12891
- this.socket = null;
12892
- this.info = null;
12893
- this.activity = null;
12894
- this.health = null;
12895
- this.buffer = "";
12896
- return new Promise((resolve17, reject) => {
12897
- const socket = net3.createConnection(this.endpoint);
12898
- this.socket = socket;
12899
- socket.setEncoding("utf8");
12900
- const timer = setTimeout(() => {
12901
- reject(new Error("codebase-index server handshake timed out"));
12902
- socket.destroy();
12903
- }, CONNECT_ATTEMPT_TIMEOUT_MS);
12904
- timer.unref?.();
12905
- const finishResolve = () => {
12906
- clearTimeout(timer);
12907
- this.connectResolve = null;
12908
- this.connectReject = null;
12909
- resolve17();
12910
- };
12911
- const finishReject = (error) => {
12912
- clearTimeout(timer);
12913
- this.connectResolve = null;
12914
- this.connectReject = null;
12915
- reject(error);
12916
- };
12917
- this.connectResolve = finishResolve;
12918
- this.connectReject = finishReject;
12919
- socket.on("data", (chunk) => this.onData(socket, chunk));
12920
- socket.on("error", (error) => {
12921
- if (!this.info) finishReject(error);
12922
- });
12923
- socket.on("close", () => this.onClose(socket));
12990
+ const totalFilesForProgress = files.length;
12991
+ let filesPreSkipped = 0;
12992
+ if (!force && trustedUnchanged) {
12993
+ files = files.filter((file) => {
12994
+ const meta = existingMeta.get(file);
12995
+ if (!meta || !trustedUnchanged.has(file)) return true;
12996
+ langStats[meta.lang] = (langStats[meta.lang] ?? 0) + meta.symbolCount;
12997
+ symbolsIndexed += meta.symbolCount;
12998
+ filesIndexed++;
12999
+ filesPreSkipped++;
13000
+ return false;
12924
13001
  });
13002
+ if (filesPreSkipped > 0) opts.onProgress?.(filesPreSkipped, totalFilesForProgress);
12925
13003
  }
12926
- onData(socket, chunk) {
12927
- if (socket !== this.socket) return;
12928
- this.buffer += chunk;
12929
- while (true) {
12930
- const newline = this.buffer.indexOf("\n");
12931
- if (newline < 0) {
12932
- if (this.buffer.length > PROJECT_INDEX_SERVER_MAX_FRAME_CHARS) {
12933
- socket.destroy(new Error("codebase-index server response exceeds the IPC limit"));
13004
+ const parallelBatch = resolveParallelBatch();
13005
+ let filesSinceLastYield = 0;
13006
+ for (let batchStart = 0; batchStart < files.length; batchStart += parallelBatch) {
13007
+ const batchEnd = Math.min(batchStart + parallelBatch, files.length);
13008
+ const batchFiles = files.slice(batchStart, batchEnd);
13009
+ opts.onProgress?.(filesPreSkipped + batchEnd, totalFilesForProgress);
13010
+ filesSinceLastYield += batchFiles.length;
13011
+ if (filesSinceLastYield >= YIELD_EVERY_N) {
13012
+ filesSinceLastYield = 0;
13013
+ await yieldEventLoop();
13014
+ if (isFrugalPerf()) {
13015
+ await new Promise((r) => setTimeout(r, 8));
13016
+ }
13017
+ throwIfAborted(signal);
13018
+ }
13019
+ const statOpts = signal ? { signal } : {};
13020
+ const statReadParse = await Promise.allSettled(
13021
+ batchFiles.map(
13022
+ async (file) => {
13023
+ let stat18;
13024
+ try {
13025
+ stat18 = await fs17.stat(file, statOpts);
13026
+ } catch (e) {
13027
+ if (isAbortError(e)) throw e;
13028
+ return {
13029
+ file,
13030
+ stat: null,
13031
+ lang: "",
13032
+ parsed: null,
13033
+ error: `stat error: ${e instanceof Error ? e.message : String(e)}`,
13034
+ missing: isMissingPathError(e)
13035
+ };
13036
+ }
13037
+ if (!stat18.isFile()) return { file, stat: stat18, lang: "", parsed: null };
13038
+ const lang = detectLang(file);
13039
+ if (!lang) return { file, stat: stat18, lang: "", parsed: null };
13040
+ if (stat18.size > MAX_INDEX_FILE_BYTES) {
13041
+ return {
13042
+ file,
13043
+ stat: stat18,
13044
+ lang,
13045
+ parsed: null,
13046
+ error: `file too large (${stat18.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
13047
+ };
13048
+ }
13049
+ const meta = existingMeta.get(file);
13050
+ if (!force && meta && meta.mtimeMs === Math.floor(stat18.mtimeMs)) {
13051
+ return { file, stat: stat18, lang, parsed: null, skippedMeta: meta };
13052
+ }
13053
+ let content;
13054
+ try {
13055
+ content = await fs17.readFile(file, { encoding: "utf8", signal });
13056
+ } catch (e) {
13057
+ if (isAbortError(e)) throw e;
13058
+ return {
13059
+ file,
13060
+ stat: stat18,
13061
+ lang,
13062
+ parsed: null,
13063
+ error: `read error: ${e instanceof Error ? e.message : String(e)}`
13064
+ };
13065
+ }
13066
+ let parsed;
13067
+ try {
13068
+ parsed = await parseFileContent(file, content, lang);
13069
+ } catch (e) {
13070
+ return {
13071
+ file,
13072
+ stat: stat18,
13073
+ lang,
13074
+ parsed: null,
13075
+ error: `parse error: ${e instanceof Error ? e.message : String(e)}`
13076
+ };
13077
+ }
13078
+ return { file, stat: stat18, lang, parsed, content };
12934
13079
  }
12935
- return;
13080
+ )
13081
+ );
13082
+ const batchEntries = [];
13083
+ const deleteForFiles = [];
13084
+ for (let fi = 0; fi < statReadParse.length; fi++) {
13085
+ const settled = statReadParse[fi];
13086
+ const file = expectDefined6(batchFiles[fi]);
13087
+ if (settled.status === "rejected") {
13088
+ const err = settled.reason;
13089
+ if (err instanceof Error && isAbortError(err)) throw err;
13090
+ errors.push(`batch error: ${file}: ${err instanceof Error ? err.message : String(err)}`);
13091
+ continue;
12936
13092
  }
12937
- if (newline > PROJECT_INDEX_SERVER_MAX_FRAME_CHARS) {
12938
- socket.destroy(new Error("codebase-index server response exceeds the IPC limit"));
12939
- return;
13093
+ const result = settled.value;
13094
+ if (result.error) {
13095
+ if (result.missing) store.deleteFile(file);
13096
+ errors.push(`${file}: ${result.error}`);
13097
+ continue;
12940
13098
  }
12941
- const line = this.buffer.slice(0, newline);
12942
- this.buffer = this.buffer.slice(newline + 1);
12943
- if (!line) continue;
12944
- let message;
12945
- try {
12946
- message = JSON.parse(line);
12947
- } catch {
12948
- socket.destroy(new Error("invalid codebase-index server response"));
12949
- return;
13099
+ const { stat: stat18, lang, parsed } = result;
13100
+ if (result.skippedMeta) {
13101
+ langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
13102
+ symbolsIndexed += result.skippedMeta.symbolCount;
13103
+ filesIndexed++;
13104
+ continue;
12950
13105
  }
12951
- this.onMessage(message);
12952
- }
12953
- }
12954
- onMessage(message) {
12955
- if (message.type === "hello") {
12956
- if (message.protocolVersion !== PROJECT_INDEX_SERVER_PROTOCOL_VERSION) {
12957
- this.rejectStaleServer(
12958
- message,
12959
- `codebase-index protocol mismatch: client=${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}, server=${message.protocolVersion}`
12960
- );
12961
- return;
13106
+ if (!lang || !parsed) {
13107
+ if (lang) {
13108
+ store.upsertFile({
13109
+ file,
13110
+ lang,
13111
+ mtimeMs: Math.floor(stat18.mtimeMs),
13112
+ symbolCount: 0,
13113
+ lastIndexed: Date.now()
13114
+ });
13115
+ filesIndexed++;
13116
+ }
13117
+ continue;
12962
13118
  }
12963
- const expectedBuildId = projectIndexServerExpectedBuildId();
12964
- if (expectedBuildId && message.buildId !== expectedBuildId) {
12965
- this.rejectStaleServer(
12966
- message,
12967
- `codebase-index build mismatch: client=${expectedBuildId}, server=${message.buildId ?? "legacy"}`
12968
- );
12969
- return;
13119
+ if (parsed.symbols.length === 0) {
13120
+ store.replaceEmptyFile({
13121
+ file,
13122
+ lang,
13123
+ mtimeMs: Math.floor(stat18.mtimeMs),
13124
+ symbolCount: 0,
13125
+ lastIndexed: Date.now()
13126
+ });
13127
+ filesIndexed++;
13128
+ continue;
12970
13129
  }
12971
- this.info = message;
12972
- this.markResponsive();
12973
- this.transition("connected", { pid: message.pid });
12974
- ensureHeartbeatLoop();
12975
- this.connectResolve?.();
12976
- return;
12977
- }
12978
- if (message.type === "index-state") {
12979
- this.activity = message.state;
12980
- this.markResponsive();
12981
- this.transition("connected", { pid: this.info?.pid });
12982
- return;
12983
- }
12984
- const entry = this.pending.get(message.id);
12985
- if (!entry) return;
12986
- this.markResponsive();
12987
- const status = connectionStates.get(this.endpoint)?.status;
12988
- if (status === "degraded" || status === "unresponsive") {
12989
- this.transition("connected", { pid: this.info?.pid });
12990
- }
12991
- if (message.type === "progress") {
12992
- entry.onProgress?.(message.current, message.total);
12993
- return;
12994
- }
12995
- this.pending.delete(message.id);
12996
- this.cleanupPending(entry);
12997
- if (message.ok) entry.resolve(message.result);
12998
- else entry.reject(remoteError(message.error, message.errorName));
12999
- }
13000
- onClose(socket) {
13001
- if (socket !== this.socket) return;
13002
- const wasConnected = this.info !== null;
13003
- this.socket = null;
13004
- this.info = null;
13005
- this.activity = null;
13006
- this.health = null;
13007
- const error = new Error("codebase-index server connection closed");
13008
- this.connectReject?.(error);
13009
- this.connectResolve = null;
13010
- this.connectReject = null;
13011
- this.rejectPending(error);
13012
- if (wasConnected) this.transition("error", { error });
13013
- maybeStopHeartbeatLoop();
13014
- }
13015
- cleanupPending(entry) {
13016
- clearTimeout(entry.timer);
13017
- if (entry.signal && entry.onAbort) {
13018
- entry.signal.removeEventListener("abort", entry.onAbort);
13019
- }
13020
- }
13021
- rejectPending(error) {
13022
- const entries = [...this.pending.values()];
13023
- this.pending.clear();
13024
- for (const entry of entries) {
13025
- this.cleanupPending(entry);
13026
- entry.reject(error);
13027
- }
13028
- }
13029
- write(message) {
13030
- const socket = this.socket;
13031
- if (socket && !socket.destroyed) socket.write(encodeProjectServerMessage(message));
13032
- }
13033
- rejectStaleServer(message, reason) {
13034
- const socket = this.socket;
13035
- if (socket && !socket.destroyed) {
13036
- socket.write(
13037
- encodeProjectServerMessage({
13038
- type: "shutdown",
13039
- id: 0,
13040
- reason: "stale-build-replacement"
13041
- })
13042
- );
13043
- const timer = setTimeout(() => socket.destroy(), 25);
13044
- timer.unref?.();
13130
+ batchEntries.push({
13131
+ file,
13132
+ lang,
13133
+ symbols: parsed.symbols,
13134
+ refs: parsed.refs ?? [],
13135
+ mtimeMs: Math.floor(stat18.mtimeMs),
13136
+ symbolCount: parsed.symbols.length
13137
+ });
13138
+ deleteForFiles.push(file);
13045
13139
  }
13046
- this.connectReject?.(new StaleProjectIndexServerError(reason, message.pid));
13047
- }
13048
- spawnDetachedServer() {
13049
- const url = resolveProjectServerUrl();
13050
- if (!url) throw new Error("built codebase-index project server is unavailable");
13051
- if (process.platform !== "win32") {
13140
+ if (batchEntries.length > 0) {
13052
13141
  try {
13053
- fs17.rmSync(this.endpoint, { force: true });
13054
- } catch {
13142
+ store.commitBatch(batchEntries, { deleteForFiles });
13143
+ for (const entry of batchEntries) {
13144
+ const count = entry.symbols.length;
13145
+ symbolsIndexed += count;
13146
+ langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
13147
+ filesIndexed++;
13148
+ }
13149
+ } catch (err) {
13150
+ const message = err instanceof Error ? err.message : String(err);
13151
+ errors.push(`commitBatch failed: ${message} \u2014 falling back to per-file writes`);
13152
+ for (const entry of batchEntries) {
13153
+ try {
13154
+ store.deleteRefsForFile(entry.file);
13155
+ store.deleteSymbolsForFile(entry.file);
13156
+ const symbolsWithIds = store.insertSymbols(entry.symbols);
13157
+ symbolsIndexed += symbolsWithIds.length;
13158
+ langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
13159
+ filesIndexed++;
13160
+ if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
13161
+ const fallbackBatch = assignRefsToSymbols2(entry.refs, symbolsWithIds);
13162
+ if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
13163
+ }
13164
+ store.resolveRefsForNames([
13165
+ ...entry.symbols.map((symbol) => symbol.name),
13166
+ ...entry.refs.map((ref) => ref.toName)
13167
+ ]);
13168
+ store.upsertFile({
13169
+ file: entry.file,
13170
+ lang: entry.lang,
13171
+ mtimeMs: entry.mtimeMs,
13172
+ symbolCount: entry.symbolCount,
13173
+ lastIndexed: Date.now()
13174
+ });
13175
+ } catch (innerErr) {
13176
+ errors.push(
13177
+ `fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
13178
+ );
13179
+ }
13180
+ }
13055
13181
  }
13056
13182
  }
13057
- const args = [fileURLToPath2(url), "--project-root", this.projectRoot];
13058
- if (this.indexDir) args.push("--index-dir", this.indexDir);
13059
- const child = spawn7(process.execPath, args, {
13060
- detached: true,
13061
- stdio: "ignore",
13062
- windowsHide: true,
13063
- env: process.env
13064
- });
13065
- child.unref();
13066
- }
13067
- forceKillKnownServer() {
13068
- const pid = this.info?.pid;
13069
- return pid ? this.forceKillServer(pid) : false;
13070
13183
  }
13071
- forceKillServer(pid) {
13072
- if (pid === process.pid) return false;
13073
- try {
13074
- process.kill(pid);
13075
- const metadataPath = projectIndexServerMetadataPath(this.projectRoot, this.indexDir);
13076
- try {
13077
- const metadata = JSON.parse(fs17.readFileSync(metadataPath, "utf8"));
13078
- if (metadata.pid === pid) fs17.rmSync(metadataPath, { force: true });
13079
- } catch {
13184
+ if (discoveredFiles && discoveryComplete) {
13185
+ for (const [file_] of existingMeta) {
13186
+ if (!discoveredFiles.has(file_)) {
13187
+ store.deleteFile(file_);
13080
13188
  }
13081
- return true;
13082
- } catch {
13083
- return false;
13084
13189
  }
13085
13190
  }
13086
- };
13087
- var connections = /* @__PURE__ */ new Map();
13088
- var heartbeatTimer;
13089
- function ensureHeartbeatLoop() {
13090
- if (heartbeatTimer) return;
13091
- heartbeatTimer = setInterval(() => {
13092
- for (const connection of connections.values()) {
13093
- if (connection.isConnected()) void connection.checkHealth(false).catch(() => {
13094
- });
13095
- }
13096
- }, SERVER_HEARTBEAT_INTERVAL_MS);
13097
- heartbeatTimer.unref?.();
13098
- }
13099
- function maybeStopHeartbeatLoop() {
13100
- if (!heartbeatTimer) return;
13101
- if ([...connections.values()].some((connection) => connection.isConnected())) return;
13102
- clearInterval(heartbeatTimer);
13103
- heartbeatTimer = void 0;
13191
+ if (needsFullRefResolution) store.resolveRefs();
13192
+ store.setMetadata("ref_resolution_version", refResolutionVersion);
13193
+ store.setMetadata("relation_graph_version", relationGraphVersion);
13194
+ if (!opts.files || filesIndexed >= 50) store.optimize();
13195
+ store.setLastIndexed(Date.now());
13196
+ if (!opts.files) store.compactIfNeeded();
13197
+ const durationMs = Date.now() - startMs;
13198
+ return {
13199
+ filesIndexed,
13200
+ symbolsIndexed,
13201
+ langStats,
13202
+ durationMs,
13203
+ errors
13204
+ };
13104
13205
  }
13105
- function connectionFor(projectRoot, indexDir) {
13106
- const endpoint = projectIndexServerEndpoint(projectRoot, indexDir);
13107
- let connection = connections.get(endpoint);
13108
- if (!connection) {
13109
- connection = new ProjectServerConnection(projectRoot, indexDir, endpoint);
13110
- connections.set(endpoint, connection);
13206
+
13207
+ // src/codebase-index/index-service.ts
13208
+ async function indexService(args, hooks = {}) {
13209
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
13210
+ try {
13211
+ return await runIndexerWithStore(store, {
13212
+ projectRoot: args.projectRoot,
13213
+ indexDir: args.indexDir,
13214
+ files: args.files,
13215
+ force: args.force,
13216
+ langs: args.langs,
13217
+ ignore: args.ignore,
13218
+ signal: hooks.signal,
13219
+ onProgress: hooks.onProgress
13220
+ });
13221
+ } finally {
13222
+ indexStorePool.release(store);
13111
13223
  }
13112
- return connection;
13113
13224
  }
13114
- function callProjectIndexServer(op, args, options) {
13115
- return connectionFor(args.projectRoot, args.indexDir).call(op, args, options);
13225
+ function searchService(args) {
13226
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
13227
+ try {
13228
+ return store.searchRanked(
13229
+ args.query,
13230
+ {
13231
+ kind: args.kind,
13232
+ lang: args.lang,
13233
+ file: args.file,
13234
+ lspKind: args.lspKind
13235
+ },
13236
+ args.limit
13237
+ );
13238
+ } finally {
13239
+ indexStorePool.release(store);
13240
+ }
13116
13241
  }
13117
- function ensureProjectIndexServer(options) {
13118
- return connectionFor(options.projectRoot, options.indexDir).configure(
13119
- options.watchExternal,
13120
- options.debounceMs
13121
- );
13242
+ function statsService(args) {
13243
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
13244
+ try {
13245
+ return store.getStats();
13246
+ } finally {
13247
+ indexStorePool.release(store);
13248
+ }
13122
13249
  }
13123
- function checkProjectIndexServerHealth(projectRoot, indexDir, options = {}) {
13124
- return connectionFor(projectRoot, indexDir).checkHealth(
13125
- false,
13126
- options.timeoutMs ?? SERVER_HEALTH_TIMEOUT_MS
13127
- );
13250
+ function packageGraphService(args) {
13251
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
13252
+ try {
13253
+ return store.getPackageGraph();
13254
+ } finally {
13255
+ indexStorePool.release(store);
13256
+ }
13128
13257
  }
13129
- async function shutdownProjectIndexServer(projectRoot, indexDir, reason) {
13130
- const endpoint = projectIndexServerEndpoint(projectRoot, indexDir);
13131
- const connection = connectionFor(projectRoot, indexDir);
13258
+ function fileGraphService(args) {
13259
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
13132
13260
  try {
13133
- return await connection.shutdownRemote(reason);
13261
+ return store.getFileGraph(args.packageFilter);
13134
13262
  } finally {
13135
- connection.close();
13136
- connections.delete(endpoint);
13137
- connectionStates.delete(endpoint);
13263
+ indexStorePool.release(store);
13138
13264
  }
13139
13265
  }
13140
- function closeProjectIndexServerClients() {
13141
- for (const connection of connections.values()) connection.close();
13142
- connections.clear();
13143
- connectionStates.clear();
13144
- latestConnectionState = {
13145
- status: isProjectIndexServerAvailable() ? "offline" : "unavailable",
13146
- connected: false
13147
- };
13148
- if (heartbeatTimer) clearInterval(heartbeatTimer);
13149
- heartbeatTimer = void 0;
13266
+ function symbolGraphService(args) {
13267
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
13268
+ try {
13269
+ return store.getSymbolGraph(args.fileFilter);
13270
+ } finally {
13271
+ indexStorePool.release(store);
13272
+ }
13150
13273
  }
13151
13274
 
13152
13275
  // src/codebase-index/background-indexer.ts
13276
+ init_languages2();
13153
13277
  var DEFAULT_FULL_INDEX_TIMEOUT_MS = 24e4;
13154
13278
  var DEFAULT_INCREMENTAL_TIMEOUT_MS = 6e4;
13155
13279
  var DEFAULT_QUERY_TIMEOUT_MS = 3e4;
@@ -13282,8 +13406,17 @@ async function shutdownCodebaseIndexHost() {
13282
13406
  }
13283
13407
  }
13284
13408
  }
13409
+ var warnedInvalidEndpoints = /* @__PURE__ */ new Set();
13410
+ function warnEndpointInvalidOnce(availability) {
13411
+ if (warnedInvalidEndpoints.has(availability.endpoint)) return;
13412
+ warnedInvalidEndpoints.add(availability.endpoint);
13413
+ process.stderr.write(
13414
+ `codebase-index: socket path is ${availability.byteLength} bytes, over this platform's ${availability.maxBytes}-byte sun_path limit (${availability.endpoint}). Subsequent calls will reject until TMPDIR is shortened to restore the shared daemon.
13415
+ `
13416
+ );
13417
+ }
13285
13418
  function callIndexOp(op, args, opts) {
13286
- const availability = resolveProjectIndexDaemonAvailability();
13419
+ const availability = resolveProjectIndexDaemonAvailability(args.projectRoot, args.indexDir);
13287
13420
  if (availability.kind === "available") {
13288
13421
  return callProjectIndexServer(op, args, opts);
13289
13422
  }
@@ -13294,6 +13427,14 @@ function callIndexOp(op, args, opts) {
13294
13427
  )
13295
13428
  );
13296
13429
  }
13430
+ if (availability.kind === "endpoint-invalid") {
13431
+ warnEndpointInvalidOnce(availability);
13432
+ return Promise.reject(
13433
+ new Error(
13434
+ `codebase-index: socket path is ${availability.byteLength} bytes, over this platform's ${availability.maxBytes}-byte sun_path limit (${availability.endpoint}). Set a shorter TMPDIR to relocate the shared daemon, or set WRONGSTACK_INDEX_INLINE=1 to explicitly opt into a process-local index.`
13435
+ )
13436
+ );
13437
+ }
13297
13438
  const w = ensureWorker();
13298
13439
  if (!w) return callInline(op, args, opts);
13299
13440
  if (opts.signal?.aborted) {
@@ -13567,7 +13708,11 @@ function checkCodebaseIndexServerHealth(projectRoot, indexDir, options = {}) {
13567
13708
  return checkProjectIndexServerHealth(projectRoot, indexDir, options);
13568
13709
  }
13569
13710
  function ensureCodebaseIndexServer(options) {
13570
- if (!isProjectIndexServerAvailable()) return Promise.resolve();
13711
+ const availability = resolveProjectIndexDaemonAvailability(options.projectRoot, options.indexDir);
13712
+ if (availability.kind !== "available") {
13713
+ if (availability.kind === "endpoint-invalid") warnEndpointInvalidOnce(availability);
13714
+ return Promise.resolve();
13715
+ }
13571
13716
  return ensureProjectIndexServer({
13572
13717
  projectRoot: options.projectRoot,
13573
13718
  indexDir: options.indexDir,
@@ -13852,6 +13997,7 @@ var codebaseStatsTool = {
13852
13997
  };
13853
13998
 
13854
13999
  // src/codebase-index/dead-code-scan.ts
14000
+ init_languages2();
13855
14001
  import * as fs19 from "node:fs";
13856
14002
  import * as path24 from "node:path";
13857
14003
  var deadCodeScanTool = {
@@ -13920,7 +14066,15 @@ function discoverEntryPoints(projectRoot, userEntryPoints) {
13920
14066
  if (rootPkg) {
13921
14067
  addPkgJsonEntryPoints(projectRoot, rootPkg, entries);
13922
14068
  }
13923
- const workspaces = rootPkg ? extractWorkspaceGlobs(rootPkg, projectRoot) : [];
14069
+ let workspaces;
14070
+ if (rootPkg) {
14071
+ workspaces = extractWorkspaceGlobs(rootPkg, projectRoot);
14072
+ if (workspaces.length === 0) {
14073
+ workspaces = extractPnpmWorkspaceDirs(projectRoot);
14074
+ }
14075
+ } else {
14076
+ workspaces = [];
14077
+ }
13924
14078
  for (const wsDir of workspaces) {
13925
14079
  const pkgJsonPath = path24.join(wsDir, "package.json");
13926
14080
  const pkg = tryReadJson(pkgJsonPath);
@@ -13942,77 +14096,189 @@ function discoverEntryPoints(projectRoot, userEntryPoints) {
13942
14096
  }
13943
14097
  return [...entries];
13944
14098
  }
14099
+ var BUILD_OUTPUT_DIRS = ["dist", "out", "build", "release"];
14100
+ var BUILD_OUTPUT_DIR_NAMES = BUILD_OUTPUT_DIRS.map((d) => `${path24.sep}${d}${path24.sep}`);
14101
+ function trySourceEquivalent(resolved) {
14102
+ resolved = resolved.replace(/[/\\]/g, path24.sep);
14103
+ for (const marker of BUILD_OUTPUT_DIR_NAMES) {
14104
+ const idx = resolved.indexOf(marker);
14105
+ if (idx === -1) continue;
14106
+ const base = resolved.replace(marker, `${path24.sep}src${path24.sep}`);
14107
+ const candidate = base.replace(/\.(js|mjs|cjs)$/, ".ts");
14108
+ if (candidate !== base && fs19.existsSync(candidate)) {
14109
+ return candidate;
14110
+ }
14111
+ const dtsStripped = base.replace(/\.d\.ts$/, "");
14112
+ const candidateDts = dtsStripped + ".ts";
14113
+ if (candidateDts !== base && candidateDts !== candidate && fs19.existsSync(candidateDts)) {
14114
+ return candidateDts;
14115
+ }
14116
+ const candidateNoExt = base + ".ts";
14117
+ if (candidate !== candidateNoExt && candidateNoExt !== candidateDts && fs19.existsSync(candidateNoExt)) {
14118
+ return candidateNoExt;
14119
+ }
14120
+ }
14121
+ return null;
14122
+ }
14123
+ function tryAddEntryPath(pkgDir, rawPath, entries) {
14124
+ const resolved = resolveAgainst(pkgDir, rawPath);
14125
+ if (fs19.existsSync(resolved)) entries.add(resolved);
14126
+ const tsResolved = resolved.replace(/\.(js|mjs|cjs)$/, ".ts");
14127
+ if (tsResolved !== resolved && fs19.existsSync(tsResolved)) {
14128
+ entries.add(tsResolved);
14129
+ }
14130
+ const srcAlt = trySourceEquivalent(resolved);
14131
+ if (srcAlt) entries.add(srcAlt);
14132
+ }
13945
14133
  function addPkgJsonEntryPoints(pkgDir, pkg, entries) {
13946
14134
  if (typeof pkg.main === "string") {
13947
- const resolved = resolveAgainst(pkgDir, pkg.main);
13948
- if (fs19.existsSync(resolved)) entries.add(resolved);
13949
- const tsResolved = resolved.replace(/\.(js|mjs|cjs)$/, ".ts");
13950
- if (tsResolved !== resolved && fs19.existsSync(tsResolved)) {
13951
- entries.add(tsResolved);
13952
- }
14135
+ tryAddEntryPath(pkgDir, pkg.main, entries);
13953
14136
  }
13954
14137
  const bin = pkg.bin;
13955
14138
  if (typeof bin === "string") {
13956
- const resolved = resolveAgainst(pkgDir, bin);
13957
- if (fs19.existsSync(resolved)) entries.add(resolved);
14139
+ tryAddEntryPath(pkgDir, bin, entries);
13958
14140
  } else if (bin && typeof bin === "object") {
13959
14141
  for (const value of Object.values(bin)) {
13960
14142
  if (typeof value === "string") {
13961
- const resolved = resolveAgainst(pkgDir, value);
13962
- if (fs19.existsSync(resolved)) entries.add(resolved);
14143
+ tryAddEntryPath(pkgDir, value, entries);
13963
14144
  }
13964
14145
  }
13965
14146
  }
13966
14147
  for (const key of ["types", "typings"]) {
13967
14148
  if (typeof pkg[key] === "string") {
13968
- const resolved = resolveAgainst(pkgDir, pkg[key]);
13969
- if (fs19.existsSync(resolved)) entries.add(resolved);
14149
+ tryAddEntryPath(pkgDir, pkg[key], entries);
13970
14150
  }
13971
14151
  }
13972
14152
  const exports_ = pkg.exports;
13973
14153
  if (exports_ && typeof exports_ === "object") {
13974
14154
  for (const value of Object.values(exports_)) {
13975
14155
  if (typeof value === "string") {
13976
- const resolved = resolveAgainst(pkgDir, value);
13977
- if (fs19.existsSync(resolved)) entries.add(resolved);
14156
+ tryAddEntryPath(pkgDir, value, entries);
13978
14157
  } else if (value && typeof value === "object") {
13979
- for (const nested of Object.values(
13980
- value
13981
- )) {
14158
+ for (const nested of Object.values(value)) {
13982
14159
  if (typeof nested === "string") {
13983
- const resolved = resolveAgainst(pkgDir, nested);
13984
- if (fs19.existsSync(resolved)) entries.add(resolved);
14160
+ tryAddEntryPath(pkgDir, nested, entries);
13985
14161
  }
13986
14162
  }
13987
14163
  }
13988
14164
  }
13989
14165
  }
13990
14166
  }
14167
+ function expandGlobPattern(entry, projectRoot) {
14168
+ const dirs = [];
14169
+ if (entry.includes("*")) {
14170
+ const base = entry.replace(/\/\*+$/, "");
14171
+ const baseDir = path24.resolve(projectRoot, base);
14172
+ try {
14173
+ const children = fs19.readdirSync(baseDir, { withFileTypes: true });
14174
+ for (const child of children) {
14175
+ if (child.isDirectory()) {
14176
+ dirs.push(path24.join(baseDir, child.name));
14177
+ }
14178
+ }
14179
+ } catch {
14180
+ }
14181
+ } else {
14182
+ dirs.push(path24.resolve(projectRoot, entry));
14183
+ }
14184
+ return dirs;
14185
+ }
13991
14186
  function extractWorkspaceGlobs(pkg, projectRoot) {
13992
14187
  const dirs = [];
13993
14188
  const workspaces = pkg.workspaces;
13994
14189
  if (Array.isArray(workspaces)) {
13995
14190
  for (const entry of workspaces) {
13996
14191
  if (typeof entry === "string") {
13997
- if (entry.includes("*")) {
13998
- const base = entry.replace(/\/\*+$/, "");
13999
- const baseDir = path24.resolve(projectRoot, base);
14000
- try {
14001
- const children = fs19.readdirSync(baseDir, { withFileTypes: true });
14002
- for (const child of children) {
14003
- if (child.isDirectory()) {
14004
- dirs.push(path24.join(baseDir, child.name));
14005
- }
14006
- }
14007
- } catch {
14192
+ dirs.push(...expandGlobPattern(entry, projectRoot));
14193
+ }
14194
+ }
14195
+ }
14196
+ return dirs;
14197
+ }
14198
+ function extractPnpmWorkspaceDirs(projectRoot) {
14199
+ const yamlPath = path24.join(projectRoot, "pnpm-workspace.yaml");
14200
+ if (!fs19.existsSync(yamlPath)) return [];
14201
+ try {
14202
+ const content = fs19.readFileSync(yamlPath, "utf8");
14203
+ const dirs = [];
14204
+ let inPackages = false;
14205
+ const lines = content.split("\n");
14206
+ const itemRe = /^\s+-\s+"([^"]+)"|^\s+-\s+'([^']+)'|^\s+-\s+(\S+)/;
14207
+ for (const line of lines) {
14208
+ const trimmed = line.trim();
14209
+ if (/^packages\s*:\s*$/.test(trimmed)) {
14210
+ inPackages = true;
14211
+ continue;
14212
+ }
14213
+ if (inPackages && trimmed.length > 0 && !line.startsWith(" ") && !line.startsWith(" ")) {
14214
+ if (!trimmed.startsWith("-")) {
14215
+ inPackages = false;
14216
+ continue;
14217
+ }
14218
+ }
14219
+ if (inPackages) {
14220
+ const m = itemRe.exec(line);
14221
+ if (m) {
14222
+ const entry = m[1] ?? m[2] ?? m[3];
14223
+ if (entry) {
14224
+ dirs.push(...expandGlobPattern(entry, projectRoot));
14008
14225
  }
14009
- } else {
14010
- dirs.push(path24.resolve(projectRoot, entry));
14011
14226
  }
14012
14227
  }
14013
14228
  }
14229
+ return dirs;
14230
+ } catch {
14231
+ return [];
14014
14232
  }
14015
- return dirs;
14233
+ }
14234
+ function resolveModulePath(importerPath, moduleSpecifier, indexedFiles) {
14235
+ if (!moduleSpecifier.startsWith(".")) return [];
14236
+ const dir = path24.dirname(importerPath);
14237
+ const base = path24.resolve(dir, moduleSpecifier);
14238
+ const results = [];
14239
+ const stripped = base.replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/, "");
14240
+ const skipBase = stripped !== base && /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(base);
14241
+ const candidates = skipBase ? [stripped] : [base];
14242
+ for (const candidate of candidates) {
14243
+ if (indexedFiles.has(candidate + ".ts")) results.push(candidate + ".ts");
14244
+ if (indexedFiles.has(candidate + ".tsx")) results.push(candidate + ".tsx");
14245
+ if (indexedFiles.has(candidate + ".js")) results.push(candidate + ".js");
14246
+ if (indexedFiles.has(candidate + ".jsx")) results.push(candidate + ".jsx");
14247
+ if (indexedFiles.has(candidate + ".mjs")) results.push(candidate + ".mjs");
14248
+ if (indexedFiles.has(candidate + ".cjs")) results.push(candidate + ".cjs");
14249
+ if (indexedFiles.has(path24.join(candidate, "index.ts")))
14250
+ results.push(path24.join(candidate, "index.ts"));
14251
+ if (indexedFiles.has(path24.join(candidate, "index.tsx")))
14252
+ results.push(path24.join(candidate, "index.tsx"));
14253
+ if (indexedFiles.has(path24.join(candidate, "index.js")))
14254
+ results.push(path24.join(candidate, "index.js"));
14255
+ if (indexedFiles.has(path24.join(candidate, "index.jsx")))
14256
+ results.push(path24.join(candidate, "index.jsx"));
14257
+ if (indexedFiles.has(path24.join(candidate, "index.mjs")))
14258
+ results.push(path24.join(candidate, "index.mjs"));
14259
+ if (indexedFiles.has(path24.join(candidate, "index.cjs")))
14260
+ results.push(path24.join(candidate, "index.cjs"));
14261
+ }
14262
+ return [...new Set(results)];
14263
+ }
14264
+ function parseNamedExportSymbols(matchText) {
14265
+ const braceStart = matchText.indexOf("{");
14266
+ if (braceStart === -1) return null;
14267
+ const braceEnd = matchText.indexOf("}", braceStart);
14268
+ if (braceEnd === -1) return null;
14269
+ const inner = matchText.slice(braceStart + 1, braceEnd);
14270
+ const symbols = [];
14271
+ for (const part of inner.split(",")) {
14272
+ let s = part.trim();
14273
+ if (!s) continue;
14274
+ s = s.replace(/^type\s+/, "");
14275
+ const asIdx = s.search(/\s+as\s+/);
14276
+ if (asIdx !== -1) {
14277
+ s = s.slice(0, asIdx).trim();
14278
+ }
14279
+ if (s) symbols.push(s);
14280
+ }
14281
+ return symbols;
14016
14282
  }
14017
14283
  function runDeadCodeScan(projectRoot, opts = {}) {
14018
14284
  const store = opts.store ?? indexStorePool.acquire(projectRoot, { indexDir: opts.indexDir });
@@ -14034,12 +14300,71 @@ function runDeadCodeScan(projectRoot, opts = {}) {
14034
14300
  }
14035
14301
  const discoveredFiles = discoverEntryPoints(projectRoot, opts.userEntryPoints);
14036
14302
  const entryFileSet = new Set(discoveredFiles.map((f) => path24.resolve(f)));
14303
+ const indexedFiles = /* @__PURE__ */ new Set();
14304
+ for (const s of allSymbols) indexedFiles.add(s.file);
14305
+ for (const fm of store.getAllFileMetas()) indexedFiles.add(fm.file);
14037
14306
  const seedIds = /* @__PURE__ */ new Set();
14038
14307
  for (const s of allSymbols) {
14039
14308
  if (entryFileSet.has(s.file)) {
14040
14309
  seedIds.add(s.id);
14041
14310
  }
14042
14311
  }
14312
+ const fileToSymbolIds = /* @__PURE__ */ new Map();
14313
+ for (const s of allSymbols) {
14314
+ let byFile = fileToSymbolIds.get(s.file);
14315
+ if (!byFile) {
14316
+ byFile = [];
14317
+ fileToSymbolIds.set(s.file, byFile);
14318
+ }
14319
+ byFile.push(s.id);
14320
+ }
14321
+ const scannedBarrels = /* @__PURE__ */ new Set();
14322
+ const barrelWorkList = [...entryFileSet];
14323
+ while (barrelWorkList.length > 0) {
14324
+ const epFile = barrelWorkList.pop();
14325
+ if (scannedBarrels.has(epFile)) continue;
14326
+ scannedBarrels.add(epFile);
14327
+ try {
14328
+ const content = fs19.readFileSync(epFile, "utf8");
14329
+ const strippedContent = content.replace(/\/\*[\s\S]*?\*\//g, (m) => " ".repeat(m.length)).replace(/\/\/[^\n]*/g, (m) => " ".repeat(m.length));
14330
+ const reExportRe = /export\s+(?:(?:type\s+)?\{[\s\S]*?\}\s+from|\*\s+as\s+\w+\s+from|\*\s+from)\s+['"]([^'"]+)['"]/g;
14331
+ let match;
14332
+ while ((match = reExportRe.exec(strippedContent)) !== null) {
14333
+ const moduleSpec = match[1];
14334
+ const resolvedFiles = resolveModulePath(epFile, moduleSpec, indexedFiles);
14335
+ for (const rf of resolvedFiles) {
14336
+ const fileSyms = fileToSymbolIds.get(rf);
14337
+ if (fileSyms) {
14338
+ const namedSymbols = parseNamedExportSymbols(match[0]);
14339
+ if (namedSymbols) {
14340
+ const nameSet = new Set(namedSymbols);
14341
+ for (const sid of fileSyms) {
14342
+ const sym = symbolById.get(sid);
14343
+ if (sym && nameSet.has(sym.name)) seedIds.add(sid);
14344
+ }
14345
+ } else {
14346
+ for (const sid of fileSyms) seedIds.add(sid);
14347
+ }
14348
+ }
14349
+ if (!scannedBarrels.has(rf)) {
14350
+ barrelWorkList.push(rf);
14351
+ }
14352
+ }
14353
+ }
14354
+ } catch (err) {
14355
+ if (err instanceof Error && err.code !== "ENOENT") {
14356
+ console.warn(
14357
+ JSON.stringify({
14358
+ level: "warn",
14359
+ event: "dead_code_scan_barrel_read_failed",
14360
+ message: err.message,
14361
+ file: epFile,
14362
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
14363
+ })
14364
+ );
14365
+ }
14366
+ }
14367
+ }
14043
14368
  const alive = new Set(seedIds);
14044
14369
  const frontier = [...seedIds];
14045
14370
  const visitedEdges = /* @__PURE__ */ new Set();
@@ -14073,8 +14398,7 @@ function runDeadCodeScan(projectRoot, opts = {}) {
14073
14398
  dead.push({
14074
14399
  name: s.name,
14075
14400
  kind: s.kind,
14076
- lang: "ts",
14077
- // populated from symbol file metadata
14401
+ lang: detectLang(s.file) ?? "ts",
14078
14402
  file: s.file,
14079
14403
  line: s.line,
14080
14404
  reason
@@ -14099,16 +14423,14 @@ function runDeadCodeScan(projectRoot, opts = {}) {
14099
14423
  deadFiles.push({
14100
14424
  file,
14101
14425
  symbolCount: syms.length,
14102
- lang: syms[0]?.kind ?? "ts"
14426
+ lang: detectLang(file) ?? "ts"
14103
14427
  });
14104
14428
  }
14105
14429
  }
14106
14430
  const deadPackages = [];
14107
14431
  const pkgEntries = findPackageEntries(projectRoot);
14108
14432
  for (const [pkgName, pkgDir] of pkgEntries) {
14109
- const pkgFiles = allSymbols.filter(
14110
- (s) => s.file.startsWith(pkgDir + path24.sep)
14111
- );
14433
+ const pkgFiles = allSymbols.filter((s) => s.file.startsWith(pkgDir + path24.sep));
14112
14434
  if (pkgFiles.length === 0) continue;
14113
14435
  const pkgUsed = pkgFiles.filter((s) => alive.has(s.id));
14114
14436
  if (pkgUsed.length === 0) {
@@ -14149,7 +14471,10 @@ function findPackageEntries(projectRoot) {
14149
14471
  pkgMap.set(rootPkg.name, projectRoot);
14150
14472
  }
14151
14473
  if (rootPkg) {
14152
- const wsDirs = extractWorkspaceGlobs(rootPkg, projectRoot);
14474
+ let wsDirs = extractWorkspaceGlobs(rootPkg, projectRoot);
14475
+ if (wsDirs.length === 0) {
14476
+ wsDirs = extractPnpmWorkspaceDirs(projectRoot);
14477
+ }
14153
14478
  for (const wsDir of wsDirs) {
14154
14479
  const wsPkg = tryReadJson(path24.join(wsDir, "package.json"));
14155
14480
  if (wsPkg && typeof wsPkg.name === "string") {
@@ -14766,7 +15091,7 @@ function processFile(content, absPath, _style, _overwrite, target) {
14766
15091
 
14767
15092
  // src/e2e.ts
14768
15093
  init_util();
14769
- import { open, readdir as readdir5 } from "node:fs/promises";
15094
+ import { open, readdir as readdir6 } from "node:fs/promises";
14770
15095
  import * as path27 from "node:path";
14771
15096
  async function readBoundedText(filePath, maxBytes) {
14772
15097
  let handle;
@@ -14884,7 +15209,7 @@ async function scanWorkspace(root, maxDepth, signal) {
14884
15209
  }
14885
15210
  let entries;
14886
15211
  try {
14887
- entries = await readdir5(current.directory, { withFileTypes: true });
15212
+ entries = await readdir6(current.directory, { withFileTypes: true });
14888
15213
  } catch {
14889
15214
  continue;
14890
15215
  }
@@ -14943,7 +15268,7 @@ async function detectPackageManager3(projectRoot, scanRoot, declared) {
14943
15268
  while (true) {
14944
15269
  const names = /* @__PURE__ */ new Set();
14945
15270
  try {
14946
- for (const entry of await readdir5(directory)) names.add(entry);
15271
+ for (const entry of await readdir6(directory)) names.add(entry);
14947
15272
  } catch {
14948
15273
  }
14949
15274
  if (names.has("pnpm-lock.yaml")) return "pnpm";
@@ -15011,7 +15336,7 @@ async function collectSpecs(root, framework, testDirectory, signal) {
15011
15336
  if (scanned > MAX_SCAN_DIRECTORIES) return { count, samples, truncated: true };
15012
15337
  let entries;
15013
15338
  try {
15014
- entries = await readdir5(directory, { withFileTypes: true });
15339
+ entries = await readdir6(directory, { withFileTypes: true });
15015
15340
  } catch {
15016
15341
  continue;
15017
15342
  }
@@ -17468,7 +17793,7 @@ async function detectFixer(cwd) {
17468
17793
  init_util();
17469
17794
  import { spawn as spawn10 } from "node:child_process";
17470
17795
  import { statSync as statSync4 } from "node:fs";
17471
- import { dirname as dirname13, resolve as resolve13, sep as sep6 } from "node:path";
17796
+ import { dirname as dirname14, resolve as resolve13, sep as sep6 } from "node:path";
17472
17797
  import { assessCommitSafety } from "@wrongstack/core/coordination";
17473
17798
  import { buildChildEnv as buildChildEnv4 } from "@wrongstack/core/utils";
17474
17799
  var TIMEOUT_MS2 = 3e4;
@@ -17630,7 +17955,7 @@ function findGitDir2(cwd, projectRoot) {
17630
17955
  } catch {
17631
17956
  }
17632
17957
  if (dir === root) break;
17633
- const parent = dirname13(dir);
17958
+ const parent = dirname14(dir);
17634
17959
  if (parent === dir) break;
17635
17960
  dir = parent;
17636
17961
  }
@@ -19774,7 +20099,7 @@ function createKanbanPresenceWrapper(projectRoot, input, ctx) {
19774
20099
 
19775
20100
  // src/session-kanban.ts
19776
20101
  import { watch } from "node:fs";
19777
- import { basename as basename12, dirname as dirname14 } from "node:path";
20102
+ import { basename as basename12, dirname as dirname15 } from "node:path";
19778
20103
  import { getSharedProjectMailbox } from "@wrongstack/core/coordination";
19779
20104
  import {
19780
20105
  loadPlan,
@@ -20246,7 +20571,7 @@ function attachSessionKanbanMirror(context) {
20246
20571
  const configureWatcher = () => {
20247
20572
  const planPath = context.meta["plan.path"];
20248
20573
  const taskPath = context.meta["task.path"];
20249
- const candidate = typeof planPath === "string" && planPath ? dirname14(planPath) : typeof taskPath === "string" && taskPath ? dirname14(taskPath) : "";
20574
+ const candidate = typeof planPath === "string" && planPath ? dirname15(planPath) : typeof taskPath === "string" && taskPath ? dirname15(taskPath) : "";
20250
20575
  if (!candidate || candidate === watchedDir) return;
20251
20576
  watcher?.close();
20252
20577
  watcher = null;
@@ -22128,12 +22453,13 @@ init_util();
22128
22453
  import * as fs28 from "node:fs/promises";
22129
22454
  import { FsError, ToolValidationError as ToolValidationError5 } from "@wrongstack/core/types";
22130
22455
  import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
22456
+ var ADVANCED_MODE_META_KEY = "tools.read.advancedMode";
22131
22457
  var MAX_BYTES2 = 5 * 1024 * 1024;
22132
22458
  var readTool = {
22133
22459
  name: "read",
22134
22460
  category: "Filesystem",
22135
- description: "Read the contents of a file with line numbers. This is the primary way to inspect source code, configuration, or any text file before making changes. Lines are returned 1-indexed with a ` N| ` prefix for easy reference in edits.",
22136
- usageHint: "FOUNDATIONAL TOOL \u2014 call this before almost any edit operation.\n\nBest practices:\n- Always read a file before using `edit`, `replace`, or `write` on it (the system often requires it for safety).\n- Use `offset` + `limit` for very large files instead of reading everything at once.\n- Default limit is generous (2000 lines) but can be increased.\n- The output format is designed to be directly usable as context for `edit` operations.",
22461
+ description: "Read the contents of a file with line numbers. This is the primary way to inspect source code, configuration, or any text file before making changes. Lines are returned 1-indexed with a ` N| ` prefix for easy reference in edits. When advanced mode is on or `includeSymbols` is set, the result also includes a `symbols` field listing codebase-index symbol names, kinds, and line numbers for the file (not file content).",
22462
+ usageHint: "FOUNDATIONAL TOOL \u2014 call this before almost any edit operation.\n\nBest practices:\n- Always read a file before using `edit`, `replace`, or `write` on it (the system often requires it for safety).\n- Use `offset` + `limit` for very large files instead of reading everything at once.\n- Default limit is generous (2000 lines) but can be increased.\n- The output format is designed to be directly usable as context for `edit` operations.\n- Set `includeSymbols: true` to also receive the codebase-index symbol listing for the file.\n- Enable advanced mode (`ctx.meta['tools.read.advancedMode'] = true`) to auto-inject symbols on every read.",
22137
22463
  selection: {
22138
22464
  doNotUseWhen: "you need to search many files for matching content.",
22139
22465
  useInstead: ["grep"]
@@ -22163,11 +22489,15 @@ var readTool = {
22163
22489
  type: "string",
22164
22490
  enum: ["content", "summary"],
22165
22491
  description: "Return full line-numbered content (default) or a compact file summary with imports/exports/symbols."
22492
+ },
22493
+ includeSymbols: {
22494
+ type: "boolean",
22495
+ description: "When true, include the codebase-index symbol list for this file as a structured `symbols` field in the result. Overrides the advanced-mode meta flag per-call."
22166
22496
  }
22167
22497
  },
22168
22498
  required: ["path"]
22169
22499
  },
22170
- async execute(input, ctx) {
22500
+ async execute(input, ctx, execOpts) {
22171
22501
  if (!input?.path) {
22172
22502
  throw new ToolValidationError5({
22173
22503
  message: "read: path is required",
@@ -22175,6 +22505,7 @@ var readTool = {
22175
22505
  });
22176
22506
  }
22177
22507
  const absPath = await safeResolveReal(input.path, ctx);
22508
+ const shouldIncludeSymbols = input.includeSymbols === true || input.includeSymbols !== false && ctx.meta[ADVANCED_MODE_META_KEY] === true;
22178
22509
  let stat18;
22179
22510
  try {
22180
22511
  stat18 = await fs28.stat(absPath);
@@ -22218,13 +22549,15 @@ var readTool = {
22218
22549
  const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
22219
22550
  if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat18.mtimeMs, offset, requestedEnd)) {
22220
22551
  ctx.recordRead(absPath, stat18.mtimeMs);
22552
+ const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
22221
22553
  return {
22222
22554
  text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat18.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
22223
22555
  total_lines: prior.totalLines,
22224
22556
  encoding: "utf8",
22225
22557
  truncated: requestedEnd < prior.totalLines,
22226
22558
  cached: true,
22227
- note: "Repeated read suppressed to save tokens."
22559
+ note: mergeSymbolNote("Repeated read suppressed to save tokens.", symResult2?.note),
22560
+ ...symResult2?.symbols ? { symbols: symResult2.symbols } : {}
22228
22561
  };
22229
22562
  }
22230
22563
  const buf = await fs28.readFile(absPath);
@@ -22238,27 +22571,43 @@ var readTool = {
22238
22571
  if (input.mode === "summary") {
22239
22572
  ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
22240
22573
  rememberReadRange(ctx, absPath, stat18.mtimeMs, total, 1, Math.min(total, 200));
22574
+ const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
22241
22575
  return {
22242
22576
  text: summarizeFile(input.path, stat18.size, allLines),
22243
22577
  total_lines: total,
22244
22578
  encoding: "utf8",
22245
22579
  truncated: total > 200,
22246
- note: "Summary mode returned compact structure instead of full file content."
22580
+ note: mergeSymbolNote(
22581
+ "Summary mode returned compact structure instead of full file content.",
22582
+ symResult2?.note
22583
+ ),
22584
+ ...symResult2?.symbols ? { symbols: symResult2.symbols } : {}
22247
22585
  };
22248
22586
  }
22249
22587
  if (limit === 0) {
22250
22588
  ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
22251
22589
  rememberReadRange(ctx, absPath, stat18.mtimeMs, total, 1, 0);
22252
- return { text: "", total_lines: total, encoding: "utf8", truncated: total > 0 };
22590
+ const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
22591
+ return {
22592
+ text: "",
22593
+ total_lines: total,
22594
+ encoding: "utf8",
22595
+ truncated: total > 0,
22596
+ ...symResult2?.symbols ? { symbols: symResult2.symbols } : {},
22597
+ ...symResult2?.note ? { note: symResult2.note } : {}
22598
+ };
22253
22599
  }
22254
22600
  if (offset > total) {
22255
22601
  ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
22256
22602
  rememberReadRange(ctx, absPath, stat18.mtimeMs, total, total + 1, total + 1);
22603
+ const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
22257
22604
  return {
22258
22605
  text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
22259
22606
  total_lines: total,
22260
22607
  encoding: "utf8",
22261
- truncated: false
22608
+ truncated: false,
22609
+ ...symResult2?.symbols ? { symbols: symResult2.symbols } : {},
22610
+ ...symResult2?.note ? { note: symResult2.note } : {}
22262
22611
  };
22263
22612
  }
22264
22613
  const slice = allLines.slice(offset - 1, offset - 1 + limit);
@@ -22267,14 +22616,53 @@ var readTool = {
22267
22616
  const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
22268
22617
  ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
22269
22618
  rememberReadRange(ctx, absPath, stat18.mtimeMs, total, offset, offset + slice.length - 1);
22619
+ const symResult = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
22270
22620
  return {
22271
22621
  text: numbered,
22272
22622
  total_lines: total,
22273
22623
  encoding: "utf8",
22274
- truncated
22624
+ truncated,
22625
+ ...symResult?.symbols ? { symbols: symResult.symbols } : {},
22626
+ ...symResult?.note ? { note: symResult.note } : {}
22275
22627
  };
22276
22628
  }
22277
22629
  };
22630
+ async function fetchSymbolsForFile(absPath, ctx, signal) {
22631
+ try {
22632
+ const state = getIndexState();
22633
+ if (!state.ready) return {};
22634
+ const { results, total } = await searchCodebaseIndex(
22635
+ {
22636
+ projectRoot: ctx.projectRoot,
22637
+ indexDir: codebaseIndexDirOverride(ctx),
22638
+ query: "",
22639
+ file: absPath,
22640
+ limit: 500
22641
+ },
22642
+ { signal }
22643
+ );
22644
+ if (results.length === 0) return {};
22645
+ const sorted = results.map((r) => ({
22646
+ name: r.name,
22647
+ kind: r.kind,
22648
+ line: r.line,
22649
+ col: r.col,
22650
+ signature: r.signature
22651
+ })).sort((a, b) => a.line - b.line || a.col - b.col);
22652
+ const result = { symbols: sorted };
22653
+ if (total > results.length) {
22654
+ result.note = `Symbol listing truncated to ${results.length} of ${total} entries.`;
22655
+ }
22656
+ return result;
22657
+ } catch {
22658
+ return {};
22659
+ }
22660
+ }
22661
+ function mergeSymbolNote(note, symNote) {
22662
+ if (!symNote) return note;
22663
+ if (!note) return symNote;
22664
+ return `${note} ${symNote}`;
22665
+ }
22278
22666
  var READ_RANGES_META_KEY = "tools.read.ranges.v1";
22279
22667
  function getReadRanges(ctx) {
22280
22668
  const existing = ctx.meta[READ_RANGES_META_KEY];
@@ -26097,6 +26485,7 @@ export {
26097
26485
  resetIndexCircuitBreaker,
26098
26486
  resetPersistentProcessRegistry,
26099
26487
  resolvePinnedBrowserTarget,
26488
+ resolveProjectIndexDaemonAvailability,
26100
26489
  resolveSessionShell,
26101
26490
  runDeadCodeScan,
26102
26491
  runStartupIndex,