opencode-codebase-index 0.13.2 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -491,7 +491,7 @@ var require_ignore = __commonJS({
491
491
  // path matching.
492
492
  // - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
493
493
  // @returns {TestResult} true if a file is ignored
494
- test(path25, checkUnignored, mode) {
494
+ test(path27, checkUnignored, mode) {
495
495
  let ignored = false;
496
496
  let unignored = false;
497
497
  let matchedRule;
@@ -500,7 +500,7 @@ var require_ignore = __commonJS({
500
500
  if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) {
501
501
  return;
502
502
  }
503
- const matched = rule[mode].test(path25);
503
+ const matched = rule[mode].test(path27);
504
504
  if (!matched) {
505
505
  return;
506
506
  }
@@ -521,17 +521,17 @@ var require_ignore = __commonJS({
521
521
  var throwError = (message, Ctor) => {
522
522
  throw new Ctor(message);
523
523
  };
524
- var checkPath = (path25, originalPath, doThrow) => {
525
- if (!isString(path25)) {
524
+ var checkPath = (path27, originalPath, doThrow) => {
525
+ if (!isString(path27)) {
526
526
  return doThrow(
527
527
  `path must be a string, but got \`${originalPath}\``,
528
528
  TypeError
529
529
  );
530
530
  }
531
- if (!path25) {
531
+ if (!path27) {
532
532
  return doThrow(`path must not be empty`, TypeError);
533
533
  }
534
- if (checkPath.isNotRelative(path25)) {
534
+ if (checkPath.isNotRelative(path27)) {
535
535
  const r = "`path.relative()`d";
536
536
  return doThrow(
537
537
  `path should be a ${r} string, but got "${originalPath}"`,
@@ -540,7 +540,7 @@ var require_ignore = __commonJS({
540
540
  }
541
541
  return true;
542
542
  };
543
- var isNotRelative = (path25) => REGEX_TEST_INVALID_PATH.test(path25);
543
+ var isNotRelative = (path27) => REGEX_TEST_INVALID_PATH.test(path27);
544
544
  checkPath.isNotRelative = isNotRelative;
545
545
  checkPath.convert = (p) => p;
546
546
  var Ignore2 = class {
@@ -570,19 +570,19 @@ var require_ignore = __commonJS({
570
570
  }
571
571
  // @returns {TestResult}
572
572
  _test(originalPath, cache, checkUnignored, slices) {
573
- const path25 = originalPath && checkPath.convert(originalPath);
573
+ const path27 = originalPath && checkPath.convert(originalPath);
574
574
  checkPath(
575
- path25,
575
+ path27,
576
576
  originalPath,
577
577
  this._strictPathCheck ? throwError : RETURN_FALSE
578
578
  );
579
- return this._t(path25, cache, checkUnignored, slices);
579
+ return this._t(path27, cache, checkUnignored, slices);
580
580
  }
581
- checkIgnore(path25) {
582
- if (!REGEX_TEST_TRAILING_SLASH.test(path25)) {
583
- return this.test(path25);
581
+ checkIgnore(path27) {
582
+ if (!REGEX_TEST_TRAILING_SLASH.test(path27)) {
583
+ return this.test(path27);
584
584
  }
585
- const slices = path25.split(SLASH2).filter(Boolean);
585
+ const slices = path27.split(SLASH2).filter(Boolean);
586
586
  slices.pop();
587
587
  if (slices.length) {
588
588
  const parent = this._t(
@@ -595,18 +595,18 @@ var require_ignore = __commonJS({
595
595
  return parent;
596
596
  }
597
597
  }
598
- return this._rules.test(path25, false, MODE_CHECK_IGNORE);
598
+ return this._rules.test(path27, false, MODE_CHECK_IGNORE);
599
599
  }
600
- _t(path25, cache, checkUnignored, slices) {
601
- if (path25 in cache) {
602
- return cache[path25];
600
+ _t(path27, cache, checkUnignored, slices) {
601
+ if (path27 in cache) {
602
+ return cache[path27];
603
603
  }
604
604
  if (!slices) {
605
- slices = path25.split(SLASH2).filter(Boolean);
605
+ slices = path27.split(SLASH2).filter(Boolean);
606
606
  }
607
607
  slices.pop();
608
608
  if (!slices.length) {
609
- return cache[path25] = this._rules.test(path25, checkUnignored, MODE_IGNORE);
609
+ return cache[path27] = this._rules.test(path27, checkUnignored, MODE_IGNORE);
610
610
  }
611
611
  const parent = this._t(
612
612
  slices.join(SLASH2) + SLASH2,
@@ -614,29 +614,29 @@ var require_ignore = __commonJS({
614
614
  checkUnignored,
615
615
  slices
616
616
  );
617
- return cache[path25] = parent.ignored ? parent : this._rules.test(path25, checkUnignored, MODE_IGNORE);
617
+ return cache[path27] = parent.ignored ? parent : this._rules.test(path27, checkUnignored, MODE_IGNORE);
618
618
  }
619
- ignores(path25) {
620
- return this._test(path25, this._ignoreCache, false).ignored;
619
+ ignores(path27) {
620
+ return this._test(path27, this._ignoreCache, false).ignored;
621
621
  }
622
622
  createFilter() {
623
- return (path25) => !this.ignores(path25);
623
+ return (path27) => !this.ignores(path27);
624
624
  }
625
625
  filter(paths) {
626
626
  return makeArray(paths).filter(this.createFilter());
627
627
  }
628
628
  // @returns {TestResult}
629
- test(path25) {
630
- return this._test(path25, this._testCache, true);
629
+ test(path27) {
630
+ return this._test(path27, this._testCache, true);
631
631
  }
632
632
  };
633
633
  var factory = (options) => new Ignore2(options);
634
- var isPathValid = (path25) => checkPath(path25 && checkPath.convert(path25), path25, RETURN_FALSE);
634
+ var isPathValid = (path27) => checkPath(path27 && checkPath.convert(path27), path27, RETURN_FALSE);
635
635
  var setupWindows = () => {
636
636
  const makePosix = (str) => /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str) ? str : str.replace(/\\/g, "/");
637
637
  checkPath.convert = makePosix;
638
638
  const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
639
- checkPath.isNotRelative = (path25) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path25) || isNotRelative(path25);
639
+ checkPath.isNotRelative = (path27) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path27) || isNotRelative(path27);
640
640
  };
641
641
  if (
642
642
  // Detect `process` so that it can run in browsers.
@@ -653,17 +653,17 @@ var require_ignore = __commonJS({
653
653
 
654
654
  // src/cli.ts
655
655
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
656
- import { realpathSync as realpathSync2, writeFileSync as writeFileSync6 } from "fs";
657
- import * as os5 from "os";
658
- import * as path24 from "path";
656
+ import { realpathSync as realpathSync3, writeFileSync as writeFileSync7 } from "fs";
657
+ import * as os6 from "os";
658
+ import * as path26 from "path";
659
659
  import { fileURLToPath as fileURLToPath2 } from "url";
660
660
 
661
661
  // src/config/constants.ts
662
662
  var DEFAULT_INCLUDE = [
663
- "**/*.{ts,tsx,js,jsx,mjs,cjs}",
663
+ "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}",
664
664
  "**/*.{py,pyi}",
665
- "**/*.{go,rs,java,kt,scala}",
666
- "**/*.{c,cpp,cc,h,hpp}",
665
+ "**/*.{go,rs,java,cs,kt,scala}",
666
+ "**/*.{c,cpp,cc,cxx,h,hpp,hxx}",
667
667
  "**/*.{rb,php,inc,swift}",
668
668
  "**/*.{cls,trigger}",
669
669
  "**/*.{vue,svelte,astro}",
@@ -788,7 +788,8 @@ function getDefaultIndexingConfig() {
788
788
  requireProjectMarker: true,
789
789
  maxDepth: 5,
790
790
  maxFilesPerDirectory: 100,
791
- fallbackToTextOnMaxChunks: true
791
+ fallbackToTextOnMaxChunks: true,
792
+ gitBlame: { enabled: false }
792
793
  };
793
794
  }
794
795
  function getDefaultSearchConfig() {
@@ -912,7 +913,10 @@ function parseConfig(raw) {
912
913
  requireProjectMarker: typeof rawIndexing.requireProjectMarker === "boolean" ? rawIndexing.requireProjectMarker : defaultIndexing.requireProjectMarker,
913
914
  maxDepth: typeof rawIndexing.maxDepth === "number" ? rawIndexing.maxDepth < -1 ? -1 : rawIndexing.maxDepth : defaultIndexing.maxDepth,
914
915
  maxFilesPerDirectory: typeof rawIndexing.maxFilesPerDirectory === "number" ? Math.max(1, rawIndexing.maxFilesPerDirectory) : defaultIndexing.maxFilesPerDirectory,
915
- fallbackToTextOnMaxChunks: typeof rawIndexing.fallbackToTextOnMaxChunks === "boolean" ? rawIndexing.fallbackToTextOnMaxChunks : defaultIndexing.fallbackToTextOnMaxChunks
916
+ fallbackToTextOnMaxChunks: typeof rawIndexing.fallbackToTextOnMaxChunks === "boolean" ? rawIndexing.fallbackToTextOnMaxChunks : defaultIndexing.fallbackToTextOnMaxChunks,
917
+ gitBlame: {
918
+ enabled: rawIndexing.gitBlame && typeof rawIndexing.gitBlame === "object" && typeof rawIndexing.gitBlame.enabled === "boolean" ? rawIndexing.gitBlame.enabled : defaultIndexing.gitBlame.enabled
919
+ }
916
920
  };
917
921
  const rawSearch = input.search && typeof input.search === "object" ? input.search : {};
918
922
  const search = {
@@ -1042,7 +1046,7 @@ var autoDetectProviders = AUTO_DETECT_PROVIDER_ORDER.filter(
1042
1046
  );
1043
1047
 
1044
1048
  // src/config/host.ts
1045
- var HOST_MODES = ["opencode", "codex", "claude", "pi"];
1049
+ var HOST_MODES = ["opencode", "codex", "claude", "pi", "jcode"];
1046
1050
  function isSupportedHostMode(value) {
1047
1051
  return HOST_MODES.includes(value);
1048
1052
  }
@@ -1097,9 +1101,9 @@ import { mkdirSync, readFileSync, writeFileSync } from "fs";
1097
1101
  import * as path from "path";
1098
1102
 
1099
1103
  // src/eval/report-formatters.ts
1100
- function assertFiniteNumber(value, path25) {
1104
+ function assertFiniteNumber(value, path27) {
1101
1105
  if (typeof value !== "number" || Number.isNaN(value) || !Number.isFinite(value)) {
1102
- throw new Error(`${path25} must be a finite number`);
1106
+ throw new Error(`${path27} must be a finite number`);
1103
1107
  }
1104
1108
  return value;
1105
1109
  }
@@ -1287,16 +1291,16 @@ function buildPerQueryArtifact(perQuery) {
1287
1291
  }
1288
1292
 
1289
1293
  // src/eval/runner.ts
1290
- import { existsSync as existsSync8 } from "fs";
1291
- import * as path13 from "path";
1294
+ import { existsSync as existsSync9 } from "fs";
1295
+ import * as path15 from "path";
1292
1296
  import { performance as performance3 } from "perf_hooks";
1293
1297
 
1294
1298
  // src/indexer/index.ts
1295
- import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync2, renameSync, unlinkSync, mkdirSync as mkdirSync2, promises as fsPromises2 } from "fs";
1296
- import * as path10 from "path";
1299
+ import { existsSync as existsSync7, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync3, renameSync as renameSync2, unlinkSync, mkdirSync as mkdirSync3, promises as fsPromises2 } from "fs";
1300
+ import * as path12 from "path";
1297
1301
  import { performance as performance2 } from "perf_hooks";
1298
- import { execFile as execFile2 } from "child_process";
1299
- import { promisify as promisify2 } from "util";
1302
+ import { execFile as execFile3 } from "child_process";
1303
+ import { promisify as promisify3 } from "util";
1300
1304
 
1301
1305
  // node_modules/eventemitter3/index.mjs
1302
1306
  var import_index = __toESM(require_eventemitter3(), 1);
@@ -2575,17 +2579,17 @@ function validateExternalUrl(urlString) {
2575
2579
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
2576
2580
  return { valid: false, reason: `Blocked protocol: ${parsed.protocol}` };
2577
2581
  }
2578
- const hostname = parsed.hostname.toLowerCase();
2579
- if (BLOCKED_HOSTNAMES.has(hostname)) {
2580
- return { valid: false, reason: `Blocked: cloud metadata service (${hostname})` };
2582
+ const hostname2 = parsed.hostname.toLowerCase();
2583
+ if (BLOCKED_HOSTNAMES.has(hostname2)) {
2584
+ return { valid: false, reason: `Blocked: cloud metadata service (${hostname2})` };
2581
2585
  }
2582
2586
  for (const pattern of BLOCKED_METADATA_IPS) {
2583
- if (pattern.test(hostname)) {
2584
- return { valid: false, reason: `Blocked: cloud metadata IP (${hostname})` };
2587
+ if (pattern.test(hostname2)) {
2588
+ return { valid: false, reason: `Blocked: cloud metadata IP (${hostname2})` };
2585
2589
  }
2586
2590
  }
2587
- if (/^169\.254\./.test(hostname)) {
2588
- return { valid: false, reason: `Blocked: link-local address (${hostname})` };
2591
+ if (/^169\.254\./.test(hostname2)) {
2592
+ return { valid: false, reason: `Blocked: link-local address (${hostname2})` };
2589
2593
  }
2590
2594
  return { valid: true };
2591
2595
  }
@@ -3775,6 +3779,12 @@ function createMockNativeBinding() {
3775
3779
  constructor() {
3776
3780
  throw error;
3777
3781
  }
3782
+ static openReadOnly() {
3783
+ throw error;
3784
+ }
3785
+ static createEmptyReadOnly() {
3786
+ throw error;
3787
+ }
3778
3788
  close() {
3779
3789
  throw error;
3780
3790
  }
@@ -3878,6 +3888,12 @@ var VectorStore = class {
3878
3888
  load() {
3879
3889
  this.inner.load();
3880
3890
  }
3891
+ loadStrict() {
3892
+ this.inner.loadStrict();
3893
+ }
3894
+ hasFingerprint() {
3895
+ return this.inner.hasFingerprint();
3896
+ }
3881
3897
  count() {
3882
3898
  return this.inner.count();
3883
3899
  }
@@ -4160,12 +4176,24 @@ var InvertedIndex = class {
4160
4176
  return this.inner.documentCount();
4161
4177
  }
4162
4178
  };
4163
- var Database = class {
4179
+ var Database = class _Database {
4164
4180
  inner;
4165
4181
  closed = false;
4166
4182
  constructor(dbPath) {
4167
4183
  this.inner = new native.Database(dbPath);
4168
4184
  }
4185
+ static fromNative(inner) {
4186
+ const database = Object.create(_Database.prototype);
4187
+ database.inner = inner;
4188
+ database.closed = false;
4189
+ return database;
4190
+ }
4191
+ static openReadOnly(dbPath) {
4192
+ return _Database.fromNative(native.Database.openReadOnly(dbPath));
4193
+ }
4194
+ static createEmptyReadOnly() {
4195
+ return _Database.fromNative(native.Database.createEmptyReadOnly());
4196
+ }
4169
4197
  throwIfClosed() {
4170
4198
  if (this.closed) {
4171
4199
  throw new Error("Database is closed");
@@ -4634,6 +4662,9 @@ function getHostProjectIndexRelativePath(host) {
4634
4662
  function hasHostProjectConfig(projectRoot, host) {
4635
4663
  return existsSync5(path8.join(projectRoot, getProjectConfigRelativePath(host)));
4636
4664
  }
4665
+ function hasHostGlobalConfig(host) {
4666
+ return existsSync5(getGlobalConfigPath(host));
4667
+ }
4637
4668
  function getGlobalIndexPath(host = "opencode") {
4638
4669
  switch (host) {
4639
4670
  case "opencode":
@@ -4673,6 +4704,9 @@ function resolveGlobalIndexPath(host = "opencode") {
4673
4704
  return hostIndexPath;
4674
4705
  }
4675
4706
  if (host !== "opencode") {
4707
+ if (hasHostGlobalConfig(host)) {
4708
+ return hostIndexPath;
4709
+ }
4676
4710
  const legacyIndexPath = getGlobalIndexPath("opencode");
4677
4711
  if (existsSync5(legacyIndexPath)) {
4678
4712
  return legacyIndexPath;
@@ -4826,8 +4860,8 @@ function normalizeFiles(rawFiles, projectRoot) {
4826
4860
  const trimmed = raw.trim();
4827
4861
  if (!trimmed) continue;
4828
4862
  const absolute = path9.resolve(projectRoot, trimmed);
4829
- const relative10 = path9.relative(projectRoot, absolute);
4830
- const cleaned = relative10.startsWith("./") ? relative10.slice(2) : relative10;
4863
+ const relative11 = path9.relative(projectRoot, absolute);
4864
+ const cleaned = relative11.startsWith("./") ? relative11.slice(2) : relative11;
4831
4865
  if (!seen.has(cleaned)) {
4832
4866
  seen.add(cleaned);
4833
4867
  result.push(cleaned);
@@ -4836,8 +4870,487 @@ function normalizeFiles(rawFiles, projectRoot) {
4836
4870
  return result;
4837
4871
  }
4838
4872
 
4873
+ // src/indexer/git-blame.ts
4874
+ import { execFile as execFile2 } from "child_process";
4875
+ import * as path10 from "path";
4876
+ import { promisify as promisify2 } from "util";
4877
+ var execFileAsync2 = promisify2(execFile2);
4878
+ function parseGitBlamePorcelain(output) {
4879
+ const commits = /* @__PURE__ */ new Map();
4880
+ let current;
4881
+ for (const line of output.split("\n")) {
4882
+ if (/^[0-9a-f]{40} /.test(line)) {
4883
+ const sha = line.slice(0, 40);
4884
+ current = commits.get(sha) ?? {
4885
+ sha,
4886
+ author: "",
4887
+ authorEmail: "",
4888
+ committedAt: 0,
4889
+ summary: "",
4890
+ lines: 0
4891
+ };
4892
+ commits.set(sha, current);
4893
+ continue;
4894
+ }
4895
+ if (!current) {
4896
+ continue;
4897
+ }
4898
+ if (line.startsWith("author ")) {
4899
+ current.author = line.slice("author ".length);
4900
+ } else if (line.startsWith("author-mail ")) {
4901
+ current.authorEmail = line.slice("author-mail ".length).replace(/^<|>$/g, "");
4902
+ } else if (line.startsWith("author-time ")) {
4903
+ current.committedAt = Number.parseInt(line.slice("author-time ".length), 10);
4904
+ } else if (line.startsWith("summary ")) {
4905
+ current.summary = line.slice("summary ".length);
4906
+ } else if (line.startsWith(" ")) {
4907
+ current.lines += 1;
4908
+ }
4909
+ }
4910
+ return Array.from(commits.values()).filter((commit) => commit.lines > 0).sort((a, b) => b.lines - a.lines || b.committedAt - a.committedAt)[0];
4911
+ }
4912
+ async function getChunkGitBlame(projectRoot, filePath, startLine, endLine) {
4913
+ const relativePath = path10.relative(projectRoot, filePath);
4914
+ try {
4915
+ const { stdout } = await execFileAsync2(
4916
+ "git",
4917
+ ["blame", "--line-porcelain", "-L", `${startLine},${endLine}`, "--", relativePath],
4918
+ { cwd: projectRoot, timeout: 3e4 }
4919
+ );
4920
+ return parseGitBlamePorcelain(stdout);
4921
+ } catch {
4922
+ return void 0;
4923
+ }
4924
+ }
4925
+
4926
+ // src/indexer/index-lock.ts
4927
+ import { randomUUID } from "crypto";
4928
+ import {
4929
+ existsSync as existsSync6,
4930
+ lstatSync,
4931
+ mkdirSync as mkdirSync2,
4932
+ readFileSync as readFileSync6,
4933
+ readdirSync as readdirSync2,
4934
+ realpathSync,
4935
+ renameSync,
4936
+ rmSync,
4937
+ writeFileSync as writeFileSync2
4938
+ } from "fs";
4939
+ import * as os4 from "os";
4940
+ import * as path11 from "path";
4941
+ var OWNER_FILE_NAME = "owner.json";
4942
+ var RECLAIM_DIRECTORY_NAME = "reclaiming";
4943
+ var RECOVERY_MARKER_PREFIX = "indexing.lock.recovery.";
4944
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
4945
+ var VALID_OPERATIONS = /* @__PURE__ */ new Set([
4946
+ "initialize",
4947
+ "index",
4948
+ "force-index",
4949
+ "clear",
4950
+ "health-check",
4951
+ "retry-failed-batches",
4952
+ "recovery"
4953
+ ]);
4954
+ var temporaryCounter = 0;
4955
+ function getErrorCode(error) {
4956
+ return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
4957
+ }
4958
+ function retryTransientFilesystemOperation(operation) {
4959
+ let lastError;
4960
+ for (let attempt = 0; attempt < 3; attempt += 1) {
4961
+ try {
4962
+ operation();
4963
+ return;
4964
+ } catch (error) {
4965
+ lastError = error;
4966
+ const code = getErrorCode(error);
4967
+ if (code !== "EBUSY" && code !== "EPERM") throw error;
4968
+ if (attempt < 2) {
4969
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, (attempt + 1) * 10);
4970
+ }
4971
+ }
4972
+ }
4973
+ throw lastError;
4974
+ }
4975
+ function parseOwner(value) {
4976
+ if (typeof value !== "object" || value === null) return null;
4977
+ const candidate = value;
4978
+ if (!Number.isInteger(candidate.pid) || (candidate.pid ?? 0) <= 0) return null;
4979
+ if (typeof candidate.hostname !== "string" || candidate.hostname.length === 0) return null;
4980
+ if (typeof candidate.startedAt !== "string" || Number.isNaN(Date.parse(candidate.startedAt))) return null;
4981
+ if (typeof candidate.operation !== "string" || !VALID_OPERATIONS.has(candidate.operation)) return null;
4982
+ if (typeof candidate.token !== "string" || !UUID_PATTERN.test(candidate.token)) return null;
4983
+ return candidate;
4984
+ }
4985
+ function parseReclaimOwner(value) {
4986
+ if (typeof value !== "object" || value === null) return null;
4987
+ const candidate = value;
4988
+ if (!Number.isInteger(candidate.pid) || (candidate.pid ?? 0) <= 0) return null;
4989
+ if (typeof candidate.hostname !== "string" || candidate.hostname.length === 0) return null;
4990
+ if (typeof candidate.startedAt !== "string" || Number.isNaN(Date.parse(candidate.startedAt))) return null;
4991
+ if (typeof candidate.token !== "string" || !UUID_PATTERN.test(candidate.token)) return null;
4992
+ if (typeof candidate.expectedOwnerToken !== "string" || !UUID_PATTERN.test(candidate.expectedOwnerToken)) return null;
4993
+ return candidate;
4994
+ }
4995
+ function readJsonDirectory(directoryPath, parser) {
4996
+ try {
4997
+ return parser(JSON.parse(readFileSync6(path11.join(directoryPath, OWNER_FILE_NAME), "utf-8")));
4998
+ } catch {
4999
+ return null;
5000
+ }
5001
+ }
5002
+ function readDirectoryOwner(lockPath) {
5003
+ return readJsonDirectory(lockPath, parseOwner);
5004
+ }
5005
+ function readReclaimOwner(markerPath) {
5006
+ return readJsonDirectory(markerPath, parseReclaimOwner);
5007
+ }
5008
+ function readRecoveryOwner(markerPath) {
5009
+ return readDirectoryOwner(markerPath);
5010
+ }
5011
+ function readLegacyOwner(lockPath) {
5012
+ try {
5013
+ const parsed = JSON.parse(readFileSync6(lockPath, "utf-8"));
5014
+ if (!Number.isInteger(parsed.pid) || Number(parsed.pid) <= 0) return null;
5015
+ if (typeof parsed.startedAt !== "string" || Number.isNaN(Date.parse(parsed.startedAt))) return null;
5016
+ return {
5017
+ pid: Number(parsed.pid),
5018
+ hostname: typeof parsed.hostname === "string" ? parsed.hostname : os4.hostname(),
5019
+ startedAt: parsed.startedAt,
5020
+ operation: typeof parsed.operation === "string" && VALID_OPERATIONS.has(parsed.operation) ? parsed.operation : "index",
5021
+ token: typeof parsed.token === "string" ? parsed.token : "legacy-v0.14.0"
5022
+ };
5023
+ } catch {
5024
+ return null;
5025
+ }
5026
+ }
5027
+ function getOwnerLiveness(owner) {
5028
+ if (owner.hostname !== os4.hostname()) return "unknown";
5029
+ try {
5030
+ process.kill(owner.pid, 0);
5031
+ return "alive";
5032
+ } catch (error) {
5033
+ const code = getErrorCode(error);
5034
+ if (code === "ESRCH") return "dead";
5035
+ if (code === "EPERM") return "alive";
5036
+ return "unknown";
5037
+ }
5038
+ }
5039
+ function sameOwner(left, right) {
5040
+ return left.pid === right.pid && left.hostname === right.hostname && left.token === right.token;
5041
+ }
5042
+ function sameReclaimOwner(left, right) {
5043
+ return left.pid === right.pid && left.hostname === right.hostname && left.token === right.token && left.expectedOwnerToken === right.expectedOwnerToken;
5044
+ }
5045
+ function publishJsonDirectory(finalPath, value) {
5046
+ const candidatePath = `${finalPath}.candidate.${process.pid}.${randomUUID()}`;
5047
+ mkdirSync2(candidatePath, { mode: 448 });
5048
+ try {
5049
+ writeFileSync2(path11.join(candidatePath, OWNER_FILE_NAME), JSON.stringify(value), {
5050
+ encoding: "utf-8",
5051
+ flag: "wx",
5052
+ mode: 384
5053
+ });
5054
+ if (existsSync6(finalPath)) return false;
5055
+ try {
5056
+ renameSync(candidatePath, finalPath);
5057
+ return true;
5058
+ } catch (error) {
5059
+ if (existsSync6(finalPath)) return false;
5060
+ throw error;
5061
+ }
5062
+ } finally {
5063
+ if (existsSync6(candidatePath)) rmSync(candidatePath, { recursive: true, force: true });
5064
+ }
5065
+ }
5066
+ function createOwner(operation) {
5067
+ return {
5068
+ pid: process.pid,
5069
+ hostname: os4.hostname(),
5070
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5071
+ operation,
5072
+ token: randomUUID()
5073
+ };
5074
+ }
5075
+ function recoveryMarkerPath(indexPath, owner) {
5076
+ return path11.join(indexPath, `${RECOVERY_MARKER_PREFIX}${owner.token}`);
5077
+ }
5078
+ function publishRecoveryMarker(indexPath, owner) {
5079
+ const markerPath = recoveryMarkerPath(indexPath, owner);
5080
+ if (!publishJsonDirectory(markerPath, owner)) {
5081
+ const markerOwner = readRecoveryOwner(markerPath);
5082
+ if (!markerOwner || !sameOwner(markerOwner, owner)) {
5083
+ throw new IndexLockContentionError(markerPath, markerOwner, "unknown-owner");
5084
+ }
5085
+ }
5086
+ return markerPath;
5087
+ }
5088
+ function getPendingRecoveries(indexPath) {
5089
+ const recoveries = [];
5090
+ const markerNames = readdirSync2(indexPath).filter((name) => {
5091
+ if (!name.startsWith(RECOVERY_MARKER_PREFIX)) return false;
5092
+ return UUID_PATTERN.test(name.slice(RECOVERY_MARKER_PREFIX.length));
5093
+ }).sort();
5094
+ for (const markerName of markerNames) {
5095
+ const markerPath = path11.join(indexPath, markerName);
5096
+ const markerToken = markerName.slice(RECOVERY_MARKER_PREFIX.length);
5097
+ let markerStats;
5098
+ try {
5099
+ markerStats = lstatSync(markerPath);
5100
+ } catch (error) {
5101
+ if (getErrorCode(error) === "ENOENT") continue;
5102
+ throw error;
5103
+ }
5104
+ if (!markerStats.isDirectory()) {
5105
+ throw new IndexLockContentionError(markerPath, null, "unknown-owner");
5106
+ }
5107
+ const owner = readRecoveryOwner(markerPath);
5108
+ if (!owner || owner.token !== markerToken || getOwnerLiveness(owner) !== "dead") {
5109
+ throw new IndexLockContentionError(markerPath, owner, "unknown-owner");
5110
+ }
5111
+ recoveries.push({ owner, markerPath });
5112
+ }
5113
+ recoveries.sort((left, right) => {
5114
+ const byTimestamp = left.owner.startedAt.localeCompare(right.owner.startedAt);
5115
+ return byTimestamp !== 0 ? byTimestamp : left.markerPath.localeCompare(right.markerPath);
5116
+ });
5117
+ return recoveries;
5118
+ }
5119
+ function cleanupDeadPublicationCandidates(indexPath) {
5120
+ const candidatePattern = /^indexing\.lock(?:\.recovery\.[0-9a-f-]{36})?\.candidate\.(\d+)\.[0-9a-f-]{36}$/i;
5121
+ for (const entry of readdirSync2(indexPath)) {
5122
+ const match = candidatePattern.exec(entry);
5123
+ if (!match) continue;
5124
+ const pid = Number(match[1]);
5125
+ if (!Number.isInteger(pid) || pid <= 0) continue;
5126
+ if (getOwnerLiveness({ pid, hostname: os4.hostname() }) !== "dead") continue;
5127
+ rmSync(path11.join(indexPath, entry), { recursive: true, force: true });
5128
+ }
5129
+ }
5130
+ function removeDeadReclaimMarker(lockPath, expectedOwner) {
5131
+ const markerPath = path11.join(lockPath, RECLAIM_DIRECTORY_NAME);
5132
+ const marker = readReclaimOwner(markerPath);
5133
+ if (!marker || marker.expectedOwnerToken !== expectedOwner.token || getOwnerLiveness(marker) !== "dead") {
5134
+ return false;
5135
+ }
5136
+ const currentOwner = readDirectoryOwner(lockPath);
5137
+ if (!currentOwner || !sameOwner(currentOwner, expectedOwner) || getOwnerLiveness(currentOwner) !== "dead") {
5138
+ return false;
5139
+ }
5140
+ const claimedMarkerPath = `${markerPath}.stale.${marker.pid}.${marker.token}.${randomUUID()}`;
5141
+ try {
5142
+ renameSync(markerPath, claimedMarkerPath);
5143
+ } catch (error) {
5144
+ if (getErrorCode(error) === "ENOENT") return false;
5145
+ throw error;
5146
+ }
5147
+ const claimedMarker = readReclaimOwner(claimedMarkerPath);
5148
+ const ownerAfterClaim = readDirectoryOwner(lockPath);
5149
+ if (!claimedMarker || !sameReclaimOwner(claimedMarker, marker) || !ownerAfterClaim || !sameOwner(ownerAfterClaim, expectedOwner) || getOwnerLiveness(ownerAfterClaim) !== "dead") {
5150
+ if (!existsSync6(markerPath) && existsSync6(claimedMarkerPath)) {
5151
+ renameSync(claimedMarkerPath, markerPath);
5152
+ }
5153
+ return false;
5154
+ }
5155
+ rmSync(claimedMarkerPath, { recursive: true, force: true });
5156
+ return true;
5157
+ }
5158
+ function reclaimDeadOwner(indexPath, lockPath, expectedOwner) {
5159
+ const reclaimPath = path11.join(lockPath, RECLAIM_DIRECTORY_NAME);
5160
+ const reclaimOwner = {
5161
+ pid: process.pid,
5162
+ hostname: os4.hostname(),
5163
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5164
+ token: randomUUID(),
5165
+ expectedOwnerToken: expectedOwner.token
5166
+ };
5167
+ for (let attempt = 0; attempt < 2; attempt += 1) {
5168
+ if (publishJsonDirectory(reclaimPath, reclaimOwner)) break;
5169
+ if (attempt === 0 && removeDeadReclaimMarker(lockPath, expectedOwner)) continue;
5170
+ return false;
5171
+ }
5172
+ try {
5173
+ const currentReclaimer = readReclaimOwner(reclaimPath);
5174
+ const currentOwner = readDirectoryOwner(lockPath);
5175
+ if (!currentReclaimer || !sameReclaimOwner(currentReclaimer, reclaimOwner) || !currentOwner || !sameOwner(currentOwner, expectedOwner) || getOwnerLiveness(currentOwner) !== "dead") {
5176
+ return false;
5177
+ }
5178
+ publishRecoveryMarker(indexPath, expectedOwner);
5179
+ const ownerBeforeQuarantine = readDirectoryOwner(lockPath);
5180
+ const reclaimerBeforeQuarantine = readReclaimOwner(reclaimPath);
5181
+ if (!ownerBeforeQuarantine || !sameOwner(ownerBeforeQuarantine, expectedOwner) || getOwnerLiveness(ownerBeforeQuarantine) !== "dead" || !reclaimerBeforeQuarantine || !sameReclaimOwner(reclaimerBeforeQuarantine, reclaimOwner)) {
5182
+ return false;
5183
+ }
5184
+ const quarantinePath = `${lockPath}.stale.${expectedOwner.token}.${reclaimOwner.token}`;
5185
+ renameSync(lockPath, quarantinePath);
5186
+ rmSync(quarantinePath, { recursive: true, force: true });
5187
+ return true;
5188
+ } catch (error) {
5189
+ if (getErrorCode(error) === "ENOENT") return false;
5190
+ throw error;
5191
+ }
5192
+ }
5193
+ var IndexLockContentionError = class extends Error {
5194
+ constructor(lockPath, owner, reason) {
5195
+ const ownerDescription = owner ? `PID ${owner.pid} on ${owner.hostname}, operation ${owner.operation}, since ${owner.startedAt}` : "an unreadable owner";
5196
+ super(`Index mutation already in progress: ${ownerDescription}`);
5197
+ this.lockPath = lockPath;
5198
+ this.owner = owner;
5199
+ this.reason = reason;
5200
+ this.name = "IndexLockContentionError";
5201
+ }
5202
+ lockPath;
5203
+ owner;
5204
+ reason;
5205
+ code = "INDEX_BUSY";
5206
+ };
5207
+ function isIndexLockContentionError(error) {
5208
+ return error instanceof IndexLockContentionError || typeof error === "object" && error !== null && "code" in error && error.code === "INDEX_BUSY";
5209
+ }
5210
+ function isTransientIndexLockContention(error) {
5211
+ if (!isIndexLockContentionError(error) || !("reason" in error)) return false;
5212
+ return error.reason === "active" || error.reason === "reclaiming";
5213
+ }
5214
+ function acquireIndexLock(indexPath, operation) {
5215
+ mkdirSync2(indexPath, { recursive: true });
5216
+ const canonicalIndexPath = realpathSync.native(indexPath);
5217
+ const lockPath = path11.join(canonicalIndexPath, "indexing.lock");
5218
+ cleanupDeadPublicationCandidates(canonicalIndexPath);
5219
+ for (let attempt = 0; attempt < 6; attempt += 1) {
5220
+ const owner = createOwner(operation);
5221
+ if (publishJsonDirectory(lockPath, owner)) {
5222
+ const lease = {
5223
+ canonicalIndexPath,
5224
+ lockPath,
5225
+ owner,
5226
+ recoveries: []
5227
+ };
5228
+ try {
5229
+ lease.recoveries = getPendingRecoveries(canonicalIndexPath);
5230
+ return lease;
5231
+ } catch (error) {
5232
+ releaseIndexLock(lease);
5233
+ throw error;
5234
+ }
5235
+ }
5236
+ let stats;
5237
+ try {
5238
+ stats = lstatSync(lockPath);
5239
+ } catch (error) {
5240
+ if (getErrorCode(error) === "ENOENT") continue;
5241
+ throw error;
5242
+ }
5243
+ if (!stats.isDirectory()) {
5244
+ const legacyOwner = readLegacyOwner(lockPath);
5245
+ throw new IndexLockContentionError(lockPath, legacyOwner, "legacy-lock");
5246
+ }
5247
+ const existingOwner = readDirectoryOwner(lockPath);
5248
+ if (!existingOwner) {
5249
+ throw new IndexLockContentionError(lockPath, null, "unknown-owner");
5250
+ }
5251
+ const liveness = getOwnerLiveness(existingOwner);
5252
+ if (liveness !== "dead") {
5253
+ throw new IndexLockContentionError(lockPath, existingOwner, liveness === "alive" ? "active" : "unknown-owner");
5254
+ }
5255
+ if (!reclaimDeadOwner(canonicalIndexPath, lockPath, existingOwner)) {
5256
+ throw new IndexLockContentionError(lockPath, existingOwner, "reclaiming");
5257
+ }
5258
+ }
5259
+ throw new IndexLockContentionError(lockPath, null, "reclaiming");
5260
+ }
5261
+ function releaseIndexLock(lease) {
5262
+ const currentOwner = readDirectoryOwner(lease.lockPath);
5263
+ if (!currentOwner || !sameOwner(currentOwner, lease.owner)) return false;
5264
+ const releasePath = `${lease.lockPath}.release.${lease.owner.pid}.${lease.owner.token}`;
5265
+ try {
5266
+ retryTransientFilesystemOperation(() => renameSync(lease.lockPath, releasePath));
5267
+ } catch (error) {
5268
+ if (getErrorCode(error) === "ENOENT") return false;
5269
+ throw error;
5270
+ }
5271
+ const claimedOwner = readDirectoryOwner(releasePath);
5272
+ if (!claimedOwner || !sameOwner(claimedOwner, lease.owner)) {
5273
+ if (!existsSync6(lease.lockPath) && existsSync6(releasePath)) {
5274
+ renameSync(releasePath, lease.lockPath);
5275
+ }
5276
+ return false;
5277
+ }
5278
+ try {
5279
+ retryTransientFilesystemOperation(() => rmSync(releasePath, { recursive: true, force: true }));
5280
+ } catch (error) {
5281
+ console.error(`[codebase-index] Lease released but tombstone cleanup failed: ${releasePath}`, error);
5282
+ }
5283
+ return true;
5284
+ }
5285
+ async function withIndexLock(indexPath, operation, callback, options = {}) {
5286
+ const lease = acquireIndexLock(indexPath, operation);
5287
+ let result;
5288
+ let callbackError;
5289
+ let callbackFailed = false;
5290
+ try {
5291
+ result = await callback(lease);
5292
+ } catch (error) {
5293
+ callbackFailed = true;
5294
+ callbackError = error;
5295
+ }
5296
+ if (!callbackFailed && options.completeRecoveries !== false) {
5297
+ try {
5298
+ completeLeaseRecovery(lease);
5299
+ } catch (error) {
5300
+ callbackFailed = true;
5301
+ callbackError = error;
5302
+ }
5303
+ }
5304
+ let releaseError;
5305
+ try {
5306
+ if (!releaseIndexLock(lease)) {
5307
+ releaseError = new Error(`Lost ownership of index mutation lease ${lease.owner.token}`);
5308
+ }
5309
+ } catch (error) {
5310
+ releaseError = error;
5311
+ }
5312
+ if (releaseError !== void 0) {
5313
+ if (callbackFailed) throw new AggregateError([callbackError, releaseError], "Index mutation and lease release both failed");
5314
+ throw releaseError;
5315
+ }
5316
+ if (callbackFailed) throw callbackError;
5317
+ return result;
5318
+ }
5319
+ function createLeaseTemporaryPath(targetPath, owner, kind = "tmp") {
5320
+ if (kind === "bak") return `${targetPath}.bak.${owner.pid}.${owner.token}`;
5321
+ temporaryCounter += 1;
5322
+ return `${targetPath}.tmp.${owner.pid}.${owner.token}.${temporaryCounter}`;
5323
+ }
5324
+ function removeLeaseTemporaryPath(temporaryPath) {
5325
+ if (existsSync6(temporaryPath)) rmSync(temporaryPath, { recursive: true, force: true });
5326
+ }
5327
+ function recoverLeaseArtifacts(indexPath, owner, backupTargets) {
5328
+ for (const targetPath of backupTargets) {
5329
+ const backupPath = createLeaseTemporaryPath(targetPath, owner, "bak");
5330
+ if (!existsSync6(backupPath)) continue;
5331
+ if (existsSync6(targetPath)) rmSync(targetPath, { recursive: true, force: true });
5332
+ renameSync(backupPath, targetPath);
5333
+ }
5334
+ const temporaryOwnerMarker = `.tmp.${owner.pid}.${owner.token}.`;
5335
+ for (const entry of readdirSync2(indexPath)) {
5336
+ if (!entry.includes(temporaryOwnerMarker)) continue;
5337
+ rmSync(path11.join(indexPath, entry), { recursive: true, force: true });
5338
+ }
5339
+ }
5340
+ function completeLeaseRecovery(lease) {
5341
+ for (const recovery of lease.recoveries) {
5342
+ const markerOwner = readRecoveryOwner(recovery.markerPath);
5343
+ if (!markerOwner || !sameOwner(markerOwner, recovery.owner)) {
5344
+ throw new Error(`Recovery marker ownership changed: ${recovery.markerPath}`);
5345
+ }
5346
+ }
5347
+ for (const recovery of lease.recoveries) {
5348
+ rmSync(recovery.markerPath, { recursive: true, force: true });
5349
+ }
5350
+ }
5351
+
4839
5352
  // src/indexer/index.ts
4840
- var CALL_GRAPH_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "tsx", "javascript", "jsx", "python", "go", "rust", "php", "apex", "zig", "gdscript", "matlab"]);
5353
+ var CALL_GRAPH_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "tsx", "javascript", "jsx", "python", "go", "rust", "php", "apex", "zig", "gdscript", "matlab", "bash"]);
4841
5354
  var CASE_INSENSITIVE_LANGUAGES = /* @__PURE__ */ new Set(["apex"]);
4842
5355
  var CALL_GRAPH_SYMBOL_CHUNK_TYPES = /* @__PURE__ */ new Set([
4843
5356
  "function_declaration",
@@ -4917,6 +5430,46 @@ function isSqliteCorruptionError(error) {
4917
5430
  return message.includes("database disk image is malformed") || message.includes("file is not a database") || message.includes("database schema is corrupt") || message.includes("sqlite_corrupt");
4918
5431
  }
4919
5432
  var STARTUP_WARNING_METADATA_KEY = "index.startupWarning";
5433
+ var READER_ARTIFACT_RETRY_INTERVAL_MS = 1e3;
5434
+ function metadataFromBlame(blame) {
5435
+ if (!blame) {
5436
+ return {};
5437
+ }
5438
+ return {
5439
+ blameSha: blame.sha,
5440
+ blameAuthor: blame.author,
5441
+ blameAuthorEmail: blame.authorEmail,
5442
+ blameCommittedAt: blame.committedAt,
5443
+ blameSummary: blame.summary
5444
+ };
5445
+ }
5446
+ function blameFromChunkData(chunk) {
5447
+ if (!chunk?.blameSha || !chunk.blameAuthor || !chunk.blameAuthorEmail || chunk.blameCommittedAt === void 0 || !chunk.blameSummary) {
5448
+ return void 0;
5449
+ }
5450
+ return {
5451
+ sha: chunk.blameSha,
5452
+ author: chunk.blameAuthor,
5453
+ authorEmail: chunk.blameAuthorEmail,
5454
+ committedAt: chunk.blameCommittedAt,
5455
+ summary: chunk.blameSummary
5456
+ };
5457
+ }
5458
+ function blameFromMetadata(metadata) {
5459
+ if (!metadata.blameSha || !metadata.blameAuthor || !metadata.blameAuthorEmail || metadata.blameCommittedAt === void 0 || !metadata.blameSummary) {
5460
+ return void 0;
5461
+ }
5462
+ return {
5463
+ sha: metadata.blameSha,
5464
+ author: metadata.blameAuthor,
5465
+ authorEmail: metadata.blameAuthorEmail,
5466
+ committedAt: metadata.blameCommittedAt,
5467
+ summary: metadata.blameSummary
5468
+ };
5469
+ }
5470
+ function hasBlameMetadata(metadata) {
5471
+ return blameFromMetadata(metadata) !== void 0;
5472
+ }
4920
5473
  var INDEX_METADATA_VERSION = "1";
4921
5474
  var EMBEDDING_STRATEGY_VERSION = "2";
4922
5475
  var RANKING_TOKEN_CACHE_LIMIT = 4096;
@@ -5075,9 +5628,9 @@ function hasAllEmbeddingParts(parts, expectedPartCount) {
5075
5628
  return true;
5076
5629
  }
5077
5630
  function isPathWithinRoot(filePath, rootPath) {
5078
- const normalizedFilePath = path10.resolve(filePath);
5079
- const normalizedRoot = path10.resolve(rootPath);
5080
- return normalizedFilePath === normalizedRoot || normalizedFilePath.startsWith(`${normalizedRoot}${path10.sep}`);
5631
+ const normalizedFilePath = path12.resolve(filePath);
5632
+ const normalizedRoot = path12.resolve(rootPath);
5633
+ return normalizedFilePath === normalizedRoot || normalizedFilePath.startsWith(`${normalizedRoot}${path12.sep}`);
5081
5634
  }
5082
5635
  var rankingQueryTokenCache = /* @__PURE__ */ new Map();
5083
5636
  var rankingNameTokenCache = /* @__PURE__ */ new Map();
@@ -5836,7 +6389,8 @@ function promoteIdentifierMatches(query, combined, semanticCandidates, keywordCa
5836
6389
  chunkType,
5837
6390
  name: chunk.name ?? void 0,
5838
6391
  language: chunk.language,
5839
- hash: chunk.contentHash
6392
+ hash: chunk.contentHash,
6393
+ ...metadataFromBlame(blameFromChunkData(chunk))
5840
6394
  };
5841
6395
  const baselineScore = existing?.score ?? 0.5;
5842
6396
  candidateUnion.set(chunk.chunkId, {
@@ -5920,7 +6474,8 @@ function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallb
5920
6474
  chunkType,
5921
6475
  name: chunk.name ?? void 0,
5922
6476
  language: chunk.language,
5923
- hash: chunk.contentHash
6477
+ hash: chunk.contentHash,
6478
+ ...metadataFromBlame(blameFromChunkData(chunk))
5924
6479
  }
5925
6480
  });
5926
6481
  }
@@ -6089,6 +6644,21 @@ function matchesSearchFilters(candidate, options, minScore) {
6089
6644
  if (options?.chunkType && candidate.metadata.chunkType !== options.chunkType) {
6090
6645
  return false;
6091
6646
  }
6647
+ if (options?.blameAuthor) {
6648
+ const author = options.blameAuthor.toLowerCase();
6649
+ const candidateAuthor = candidate.metadata.blameAuthor?.toLowerCase();
6650
+ const candidateEmail = candidate.metadata.blameAuthorEmail?.toLowerCase();
6651
+ if (candidateAuthor !== author && candidateEmail !== author) return false;
6652
+ }
6653
+ if (options?.blameSha && !candidate.metadata.blameSha?.toLowerCase().startsWith(options.blameSha.toLowerCase())) {
6654
+ return false;
6655
+ }
6656
+ if (options?.blameSince) {
6657
+ const sinceMs = Date.parse(options.blameSince);
6658
+ if (Number.isNaN(sinceMs)) return false;
6659
+ const committedAt = candidate.metadata.blameCommittedAt;
6660
+ if (committedAt === void 0 || committedAt < Math.floor(sinceMs / 1e3)) return false;
6661
+ }
6092
6662
  return true;
6093
6663
  }
6094
6664
  function unionCandidates(semanticCandidates, keywordCandidates) {
@@ -6126,26 +6696,133 @@ var Indexer = class {
6126
6696
  queryCacheTtlMs = 5 * 60 * 1e3;
6127
6697
  querySimilarityThreshold = 0.85;
6128
6698
  indexCompatibility = null;
6129
- indexingLockPath = "";
6699
+ activeIndexLease = null;
6700
+ initializationPromise = null;
6701
+ initializationMode = "none";
6702
+ readIssues = [];
6703
+ retiredDatabases = [];
6704
+ readerArtifactFingerprint = null;
6705
+ writerArtifactFingerprint = null;
6706
+ readerArtifactRetryAfter = /* @__PURE__ */ new Map();
6130
6707
  constructor(projectRoot, config, host = "opencode") {
6131
6708
  this.projectRoot = projectRoot;
6132
6709
  this.config = config;
6133
6710
  this.host = host;
6134
6711
  this.indexPath = this.getIndexPath();
6135
- this.fileHashCachePath = path10.join(this.indexPath, "file-hashes.json");
6136
- this.failedBatchesPath = path10.join(this.indexPath, "failed-batches.json");
6137
- this.indexingLockPath = path10.join(this.indexPath, "indexing.lock");
6712
+ this.fileHashCachePath = path12.join(this.indexPath, "file-hashes.json");
6713
+ this.failedBatchesPath = path12.join(this.indexPath, "failed-batches.json");
6138
6714
  this.logger = initializeLogger(config.debug);
6139
6715
  }
6140
6716
  getIndexPath() {
6141
6717
  return resolveProjectIndexPath(this.projectRoot, this.config.scope, this.host);
6142
6718
  }
6719
+ isLocalProjectIndexPath() {
6720
+ const localProjectIndexPaths = [path12.join(this.projectRoot, getHostProjectIndexRelativePath(this.host))];
6721
+ if (this.host !== "opencode") {
6722
+ localProjectIndexPaths.push(path12.join(this.projectRoot, getHostProjectIndexRelativePath("opencode")));
6723
+ }
6724
+ return localProjectIndexPaths.some((localPath) => {
6725
+ if (!existsSync7(localPath) || !existsSync7(this.indexPath)) {
6726
+ return path12.resolve(this.indexPath) === path12.resolve(localPath);
6727
+ }
6728
+ const indexStats = statSync3(this.indexPath);
6729
+ const localStats = statSync3(localPath);
6730
+ return indexStats.dev === localStats.dev && indexStats.ino === localStats.ino;
6731
+ });
6732
+ }
6733
+ resetLoadedIndexState(retireDatabase = false) {
6734
+ if (this.database) {
6735
+ if (retireDatabase) {
6736
+ this.retiredDatabases.push(this.database);
6737
+ } else {
6738
+ this.database.close();
6739
+ }
6740
+ }
6741
+ this.store = null;
6742
+ this.invertedIndex = null;
6743
+ this.database = null;
6744
+ this.provider = null;
6745
+ this.configuredProviderInfo = null;
6746
+ this.reranker = null;
6747
+ this.indexCompatibility = null;
6748
+ this.initializationMode = "none";
6749
+ this.readIssues = [];
6750
+ this.readerArtifactFingerprint = null;
6751
+ this.writerArtifactFingerprint = null;
6752
+ this.readerArtifactRetryAfter.clear();
6753
+ this.fileHashCache.clear();
6754
+ }
6755
+ refreshLoadedIndexState() {
6756
+ if (!this.store || !this.invertedIndex || !this.configuredProviderInfo) return;
6757
+ this.store.load();
6758
+ this.invertedIndex.load();
6759
+ this.fileHashCache.clear();
6760
+ this.loadFileHashCache();
6761
+ this.indexCompatibility = this.validateIndexCompatibility(this.configuredProviderInfo);
6762
+ this.readIssues = [];
6763
+ this.readerArtifactRetryAfter.clear();
6764
+ }
6765
+ async withIndexMutationLease(operation, callback) {
6766
+ const lease = acquireIndexLock(this.indexPath, operation);
6767
+ this.indexPath = lease.canonicalIndexPath;
6768
+ this.fileHashCachePath = path12.join(this.indexPath, "file-hashes.json");
6769
+ this.failedBatchesPath = path12.join(this.indexPath, "failed-batches.json");
6770
+ this.activeIndexLease = lease;
6771
+ let result;
6772
+ let callbackError;
6773
+ let callbackFailed = false;
6774
+ try {
6775
+ result = await callback(lease.recoveries.map(({ owner }) => owner));
6776
+ } catch (error) {
6777
+ callbackFailed = true;
6778
+ callbackError = error;
6779
+ }
6780
+ if (!callbackFailed) {
6781
+ try {
6782
+ completeLeaseRecovery(lease);
6783
+ this.writerArtifactFingerprint = this.captureReaderArtifactFingerprint();
6784
+ } catch (error) {
6785
+ callbackFailed = true;
6786
+ callbackError = error;
6787
+ }
6788
+ }
6789
+ let releaseError;
6790
+ try {
6791
+ if (!releaseIndexLock(lease)) {
6792
+ releaseError = new Error(`Lost ownership of index mutation lease ${lease.owner.token}`);
6793
+ this.writerArtifactFingerprint = null;
6794
+ if (this.activeIndexLease?.owner.token === lease.owner.token) {
6795
+ this.activeIndexLease = null;
6796
+ }
6797
+ } else if (this.activeIndexLease?.owner.token === lease.owner.token) {
6798
+ this.activeIndexLease = null;
6799
+ }
6800
+ } catch (error) {
6801
+ releaseError = error;
6802
+ this.writerArtifactFingerprint = null;
6803
+ if (!existsSync7(lease.lockPath) && this.activeIndexLease?.owner.token === lease.owner.token) {
6804
+ this.activeIndexLease = null;
6805
+ }
6806
+ }
6807
+ if (releaseError !== void 0) {
6808
+ if (callbackFailed) throw new AggregateError([callbackError, releaseError], "Index mutation and lease release both failed");
6809
+ throw releaseError;
6810
+ }
6811
+ if (callbackFailed) throw callbackError;
6812
+ return result;
6813
+ }
6814
+ requireActiveLease() {
6815
+ if (!this.activeIndexLease) {
6816
+ throw new Error("Index mutation attempted without an active interprocess lease");
6817
+ }
6818
+ return this.activeIndexLease;
6819
+ }
6143
6820
  loadFileHashCache() {
6144
- if (!existsSync6(this.fileHashCachePath)) {
6821
+ if (!existsSync7(this.fileHashCachePath)) {
6145
6822
  return;
6146
6823
  }
6147
6824
  try {
6148
- const data = readFileSync6(this.fileHashCachePath, "utf-8");
6825
+ const data = readFileSync7(this.fileHashCachePath, "utf-8");
6149
6826
  const parsed = JSON.parse(data);
6150
6827
  this.fileHashCache = new Map(Object.entries(parsed));
6151
6828
  } catch (error) {
@@ -6165,15 +6842,26 @@ var Indexer = class {
6165
6842
  this.atomicWriteSync(this.fileHashCachePath, JSON.stringify(obj));
6166
6843
  }
6167
6844
  atomicWriteSync(targetPath, data) {
6168
- const tempPath = `${targetPath}.tmp`;
6169
- mkdirSync2(path10.dirname(targetPath), { recursive: true });
6170
- writeFileSync2(tempPath, data);
6171
- renameSync(tempPath, targetPath);
6845
+ const lease = this.requireActiveLease();
6846
+ const tempPath = createLeaseTemporaryPath(targetPath, lease.owner, "tmp");
6847
+ mkdirSync3(path12.dirname(targetPath), { recursive: true });
6848
+ try {
6849
+ writeFileSync3(tempPath, data);
6850
+ renameSync2(tempPath, targetPath);
6851
+ } finally {
6852
+ removeLeaseTemporaryPath(tempPath);
6853
+ }
6854
+ }
6855
+ saveInvertedIndex(invertedIndex) {
6856
+ this.atomicWriteSync(
6857
+ path12.join(this.indexPath, "inverted-index.json"),
6858
+ invertedIndex.serialize()
6859
+ );
6172
6860
  }
6173
6861
  getScopedRoots() {
6174
- const roots = /* @__PURE__ */ new Set([path10.resolve(this.projectRoot)]);
6862
+ const roots = /* @__PURE__ */ new Set([path12.resolve(this.projectRoot)]);
6175
6863
  for (const kbRoot of this.config.knowledgeBases) {
6176
- roots.add(path10.resolve(this.projectRoot, kbRoot));
6864
+ roots.add(path12.resolve(this.projectRoot, kbRoot));
6177
6865
  }
6178
6866
  return Array.from(roots);
6179
6867
  }
@@ -6182,29 +6870,29 @@ var Indexer = class {
6182
6870
  if (this.config.scope !== "global") {
6183
6871
  return branchName;
6184
6872
  }
6185
- const projectHash = hashContent(path10.resolve(this.projectRoot)).slice(0, 16);
6873
+ const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6186
6874
  return `${projectHash}:${branchName}`;
6187
6875
  }
6188
6876
  getBranchCatalogKeyFor(branchName) {
6189
6877
  if (this.config.scope !== "global") {
6190
6878
  return branchName;
6191
6879
  }
6192
- const projectHash = hashContent(path10.resolve(this.projectRoot)).slice(0, 16);
6880
+ const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6193
6881
  return `${projectHash}:${branchName}`;
6194
6882
  }
6195
6883
  getLegacyBranchCatalogKey() {
6196
6884
  return this.currentBranch || "default";
6197
6885
  }
6198
6886
  getLegacyMigrationMetadataKey() {
6199
- const projectHash = hashContent(path10.resolve(this.projectRoot)).slice(0, 16);
6887
+ const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6200
6888
  return `index.globalBranchMigration.${projectHash}`;
6201
6889
  }
6202
6890
  getProjectEmbeddingStrategyMetadataKey() {
6203
- const projectHash = hashContent(path10.resolve(this.projectRoot)).slice(0, 16);
6891
+ const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6204
6892
  return `index.embeddingStrategyVersion.${projectHash}`;
6205
6893
  }
6206
6894
  getProjectForceReembedMetadataKey() {
6207
- const projectHash = hashContent(path10.resolve(this.projectRoot)).slice(0, 16);
6895
+ const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6208
6896
  return `index.forceReembed.${projectHash}`;
6209
6897
  }
6210
6898
  hasProjectForceReembedPending() {
@@ -6298,7 +6986,7 @@ var Indexer = class {
6298
6986
  if (!this.database) {
6299
6987
  return { chunkIds, symbolIds };
6300
6988
  }
6301
- const projectRootPath = path10.resolve(this.projectRoot);
6989
+ const projectRootPath = path12.resolve(this.projectRoot);
6302
6990
  const projectLocalFilePaths = /* @__PURE__ */ new Set([
6303
6991
  ...Array.from(this.fileHashCache.keys()).filter(
6304
6992
  (filePath) => this.isFileInCurrentScope(filePath, roots) && isPathWithinRoot(filePath, projectRootPath)
@@ -6321,7 +7009,7 @@ var Indexer = class {
6321
7009
  if (this.config.scope !== "global") {
6322
7010
  return this.getBranchCatalogCleanupKeys();
6323
7011
  }
6324
- const projectHash = hashContent(path10.resolve(this.projectRoot)).slice(0, 16);
7012
+ const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6325
7013
  const keys = /* @__PURE__ */ new Set();
6326
7014
  const projectChunkIdSet = new Set(projectChunkIds);
6327
7015
  const projectSymbolIdSet = new Set(projectSymbolIds);
@@ -6405,7 +7093,7 @@ var Indexer = class {
6405
7093
  if (!this.database || this.config.scope !== "global") {
6406
7094
  return false;
6407
7095
  }
6408
- const projectHash = hashContent(path10.resolve(this.projectRoot)).slice(0, 16);
7096
+ const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6409
7097
  const roots = this.getScopedRoots();
6410
7098
  const { chunkIds: projectLocalChunkIds, symbolIds: projectLocalSymbolIds } = this.getProjectLocalScopedOwnershipIds(roots);
6411
7099
  return this.database.getAllBranches().some(
@@ -6439,7 +7127,7 @@ var Indexer = class {
6439
7127
  ...Array.from(this.fileHashCache.keys()).filter((filePath) => this.isFileInCurrentScope(filePath, roots)),
6440
7128
  ...scopedEntries.map(({ metadata }) => metadata.filePath)
6441
7129
  ]);
6442
- const projectRootPath = path10.resolve(this.projectRoot);
7130
+ const projectRootPath = path12.resolve(this.projectRoot);
6443
7131
  const projectLocalFilePaths = new Set(
6444
7132
  Array.from(filePaths).filter((filePath) => isPathWithinRoot(filePath, projectRootPath))
6445
7133
  );
@@ -6501,34 +7189,27 @@ var Indexer = class {
6501
7189
  database.gcOrphanEmbeddings();
6502
7190
  database.gcOrphanChunks();
6503
7191
  store.save();
6504
- invertedIndex.save();
7192
+ this.saveInvertedIndex(invertedIndex);
6505
7193
  return {
6506
7194
  removedChunkIds: removedChunkIdList,
6507
7195
  hasForeignData: allMetadata.some(({ metadata }) => !this.isFileInCurrentScope(metadata.filePath, roots))
6508
7196
  };
6509
7197
  }
6510
- checkForInterruptedIndexing() {
6511
- return existsSync6(this.indexingLockPath);
6512
- }
6513
- acquireIndexingLock() {
6514
- const lockData = {
6515
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
6516
- pid: process.pid
6517
- };
6518
- writeFileSync2(this.indexingLockPath, JSON.stringify(lockData));
6519
- }
6520
- releaseIndexingLock() {
6521
- if (existsSync6(this.indexingLockPath)) {
6522
- unlinkSync(this.indexingLockPath);
7198
+ async recoverFromInterruptedIndexingUnlocked(owners) {
7199
+ for (const owner of owners) {
7200
+ this.logger.warn("Detected interrupted indexing session, recovering...", {
7201
+ pid: owner.pid,
7202
+ hostname: owner.hostname,
7203
+ operation: owner.operation,
7204
+ startedAt: owner.startedAt
7205
+ });
6523
7206
  }
6524
- }
6525
- async recoverFromInterruptedIndexing() {
6526
- this.logger.warn("Detected interrupted indexing session, recovering...");
6527
- if (existsSync6(this.fileHashCachePath)) {
6528
- unlinkSync(this.fileHashCachePath);
7207
+ if (this.config.scope === "global") {
7208
+ if (existsSync7(this.fileHashCachePath)) {
7209
+ unlinkSync(this.fileHashCachePath);
7210
+ }
7211
+ await this.healthCheckUnlocked();
6529
7212
  }
6530
- await this.healthCheck();
6531
- this.releaseIndexingLock();
6532
7213
  this.logger.info("Recovery complete, next index will re-process all files");
6533
7214
  }
6534
7215
  loadFailedBatches(maxChunkTokens) {
@@ -6544,10 +7225,10 @@ var Indexer = class {
6544
7225
  }
6545
7226
  }
6546
7227
  loadSerializedFailedBatches() {
6547
- if (!existsSync6(this.failedBatchesPath)) {
7228
+ if (!existsSync7(this.failedBatchesPath)) {
6548
7229
  return [];
6549
7230
  }
6550
- const data = readFileSync6(this.failedBatchesPath, "utf-8");
7231
+ const data = readFileSync7(this.failedBatchesPath, "utf-8");
6551
7232
  const parsed = JSON.parse(data);
6552
7233
  return parsed.map((batch) => {
6553
7234
  const chunks = Array.isArray(batch.chunks) ? batch.chunks : [];
@@ -6564,7 +7245,7 @@ var Indexer = class {
6564
7245
  }
6565
7246
  saveFailedBatches(batches) {
6566
7247
  if (batches.length === 0) {
6567
- if (existsSync6(this.failedBatchesPath)) {
7248
+ if (existsSync7(this.failedBatchesPath)) {
6568
7249
  try {
6569
7250
  unlinkSync(this.failedBatchesPath);
6570
7251
  } catch {
@@ -6572,7 +7253,7 @@ var Indexer = class {
6572
7253
  }
6573
7254
  return;
6574
7255
  }
6575
- writeFileSync2(this.failedBatchesPath, JSON.stringify(batches, null, 2));
7256
+ this.atomicWriteSync(this.failedBatchesPath, JSON.stringify(batches, null, 2));
6576
7257
  }
6577
7258
  collectRetryableFailedChunks(currentFileHashes, unchangedFilePaths, maxChunkTokens) {
6578
7259
  const retryableById = /* @__PURE__ */ new Map();
@@ -6763,9 +7444,225 @@ var Indexer = class {
6763
7444
  parts.push("snippet:");
6764
7445
  parts.push("[unavailable]");
6765
7446
  }
6766
- return parts.join("\n");
7447
+ return parts.join("\n");
7448
+ }
7449
+ async initialize() {
7450
+ if (this.initializationPromise) {
7451
+ await this.initializationPromise;
7452
+ }
7453
+ if (this.isInitializedFor("reader")) {
7454
+ return;
7455
+ }
7456
+ await this.initializeOnce("reader", [], { skipAutoGc: true });
7457
+ }
7458
+ async initializeOnce(mode, recoveredOwners, options) {
7459
+ if (this.initializationPromise) {
7460
+ await this.initializationPromise;
7461
+ if (this.isInitializedFor(mode)) {
7462
+ return;
7463
+ }
7464
+ return this.initializeOnce(mode, recoveredOwners, options);
7465
+ }
7466
+ if (this.isInitializedFor(mode)) {
7467
+ return;
7468
+ }
7469
+ const initialization = this.initializeUnlocked(mode, recoveredOwners, options).catch((error) => {
7470
+ this.resetLoadedIndexState();
7471
+ throw error;
7472
+ }).finally(() => {
7473
+ if (this.initializationPromise === initialization) {
7474
+ this.initializationPromise = null;
7475
+ }
7476
+ });
7477
+ this.initializationPromise = initialization;
7478
+ await initialization;
7479
+ }
7480
+ isInitializedFor(mode) {
7481
+ const hasState = Boolean(
7482
+ this.store && this.provider && this.invertedIndex && this.configuredProviderInfo && this.database
7483
+ );
7484
+ if (!hasState) {
7485
+ return false;
7486
+ }
7487
+ return mode === "reader" ? this.initializationMode !== "none" : this.initializationMode === "writer";
7488
+ }
7489
+ recordReadIssue(component, message, error) {
7490
+ this.readIssues.push(this.createReadIssue(component, message));
7491
+ this.readerArtifactRetryAfter.set(component, Date.now() + READER_ARTIFACT_RETRY_INTERVAL_MS);
7492
+ this.logger.warn(message, error === void 0 ? void 0 : { error: getErrorMessage2(error) });
7493
+ }
7494
+ createReadIssue(component, message) {
7495
+ return {
7496
+ component,
7497
+ message,
7498
+ blocking: component !== "keyword"
7499
+ };
7500
+ }
7501
+ getVectorReadIssueMessage() {
7502
+ if (this.config.scope === "global") {
7503
+ return "Shared vector index could not be read. Restore or repair the complete fingerprinted shared vector artifacts; automatic reset is disabled for global scope.";
7504
+ }
7505
+ if (!this.isLocalProjectIndexPath()) {
7506
+ return "Vector index could not be read from an inherited project index. Restore or fingerprint it from the checkout that owns the index; do not remove or rebuild it from this worktree.";
7507
+ }
7508
+ return "Vector index could not be read. Run index_codebase after the active writer finishes to fingerprint a structurally valid legacy pair, or remove this checkout's local index directory and run index_codebase to rebuild it.";
7509
+ }
7510
+ getKeywordReadIssueMessage() {
7511
+ if (this.config.scope === "global") {
7512
+ return "Shared keyword index could not be read; semantic search remains available. Restore or repair the shared keyword artifact; automatic reset is disabled for global scope.";
7513
+ }
7514
+ if (!this.isLocalProjectIndexPath()) {
7515
+ return "Keyword index could not be read from an inherited project index; semantic search remains available. Restore or repair it from the checkout that owns the index; do not rebuild it from this worktree.";
7516
+ }
7517
+ return "Keyword index could not be read; semantic search remains available. Restore a readable published keyword index, or run index_codebase with force=true after the active writer finishes.";
7518
+ }
7519
+ getDatabaseReadIssueMessage() {
7520
+ if (this.config.scope === "global") {
7521
+ return "Shared index database could not be read. Restore or repair the shared SQLite database; automatic reset is disabled for global scope.";
7522
+ }
7523
+ if (!this.isLocalProjectIndexPath()) {
7524
+ return "Index database could not be read from an inherited project index. Restore or repair it from the checkout that owns the index; do not migrate or rebuild it from this worktree.";
7525
+ }
7526
+ return "Index database could not be read. Run index_codebase after the active writer finishes to repair or migrate it under the writer lease.";
7527
+ }
7528
+ getReaderFileFingerprint(filePath, identityOnly = false) {
7529
+ try {
7530
+ const stats = statSync3(filePath);
7531
+ if (identityOnly) {
7532
+ return `${stats.dev}:${stats.ino}`;
7533
+ }
7534
+ return `${stats.dev}:${stats.ino}:${stats.size}:${stats.mtimeMs}:${stats.ctimeMs}`;
7535
+ } catch (error) {
7536
+ return `unavailable:${getErrorMessage2(error)}`;
7537
+ }
7538
+ }
7539
+ captureReaderArtifactFingerprint() {
7540
+ const storePath = path12.join(this.indexPath, "vectors");
7541
+ return {
7542
+ vectors: `${this.getReaderFileFingerprint(storePath)}|${this.getReaderFileFingerprint(`${storePath}.meta.json`)}`,
7543
+ keyword: this.getReaderFileFingerprint(path12.join(this.indexPath, "inverted-index.json")),
7544
+ database: this.getReaderFileFingerprint(path12.join(this.indexPath, "codebase.db")),
7545
+ databaseIdentity: this.getReaderFileFingerprint(path12.join(this.indexPath, "codebase.db"), true)
7546
+ };
7547
+ }
7548
+ refreshReaderArtifacts() {
7549
+ if (this.initializationMode !== "reader" || !this.configuredProviderInfo) {
7550
+ return;
7551
+ }
7552
+ const previousFingerprint = this.readerArtifactFingerprint;
7553
+ const currentFingerprint = this.captureReaderArtifactFingerprint();
7554
+ const issues = new Map(this.readIssues.map((issue) => [issue.component, issue]));
7555
+ const retryDue = (component) => issues.has(component) && Date.now() >= (this.readerArtifactRetryAfter.get(component) ?? 0);
7556
+ const vectorsChanged = !previousFingerprint || currentFingerprint.vectors !== previousFingerprint.vectors;
7557
+ const keywordChanged = !previousFingerprint || currentFingerprint.keyword !== previousFingerprint.keyword;
7558
+ const databaseChanged = !previousFingerprint || currentFingerprint.database !== previousFingerprint.database;
7559
+ const databaseReplaced = !previousFingerprint || currentFingerprint.databaseIdentity !== previousFingerprint.databaseIdentity;
7560
+ if (previousFingerprint && !vectorsChanged && !keywordChanged && !databaseChanged && !Array.from(issues.keys()).some(retryDue)) {
7561
+ return;
7562
+ }
7563
+ const setIssue = (component, message, error) => {
7564
+ if (!issues.has(component)) {
7565
+ this.logger.warn(message, error === void 0 ? void 0 : { error: getErrorMessage2(error) });
7566
+ }
7567
+ issues.set(component, this.createReadIssue(component, message));
7568
+ this.readerArtifactRetryAfter.set(component, Date.now() + READER_ARTIFACT_RETRY_INTERVAL_MS);
7569
+ };
7570
+ const storePath = path12.join(this.indexPath, "vectors");
7571
+ const vectorMetadataPath = `${storePath}.meta.json`;
7572
+ const invertedIndexPath = path12.join(this.indexPath, "inverted-index.json");
7573
+ const dbPath = path12.join(this.indexPath, "codebase.db");
7574
+ if (vectorsChanged || retryDue("vectors")) {
7575
+ const vectorStoreExists = existsSync7(storePath);
7576
+ const vectorMetadataExists = existsSync7(vectorMetadataPath);
7577
+ if (vectorStoreExists && vectorMetadataExists) {
7578
+ try {
7579
+ const store = new VectorStore(storePath, this.configuredProviderInfo.modelInfo.dimensions);
7580
+ store.loadStrict();
7581
+ this.store = store;
7582
+ issues.delete("vectors");
7583
+ this.readerArtifactRetryAfter.delete("vectors");
7584
+ } catch (error) {
7585
+ setIssue("vectors", this.getVectorReadIssueMessage(), error);
7586
+ }
7587
+ } else if (vectorStoreExists !== vectorMetadataExists || issues.has("vectors")) {
7588
+ setIssue("vectors", this.getVectorReadIssueMessage());
7589
+ }
7590
+ }
7591
+ if (keywordChanged || retryDue("keyword") || !existsSync7(invertedIndexPath) && (this.store?.count() ?? 0) > 0) {
7592
+ if (existsSync7(invertedIndexPath)) {
7593
+ try {
7594
+ const invertedIndex = new InvertedIndex(invertedIndexPath);
7595
+ invertedIndex.load();
7596
+ this.invertedIndex = invertedIndex;
7597
+ issues.delete("keyword");
7598
+ this.readerArtifactRetryAfter.delete("keyword");
7599
+ } catch (error) {
7600
+ setIssue("keyword", this.getKeywordReadIssueMessage(), error);
7601
+ }
7602
+ } else if ((this.store?.count() ?? 0) > 0 || issues.has("keyword")) {
7603
+ setIssue("keyword", this.getKeywordReadIssueMessage());
7604
+ }
7605
+ }
7606
+ if (databaseReplaced || databaseChanged && issues.has("database") || retryDue("database")) {
7607
+ if (existsSync7(dbPath)) {
7608
+ try {
7609
+ const database = Database.openReadOnly(dbPath);
7610
+ if (this.database) {
7611
+ this.retiredDatabases.push(this.database);
7612
+ }
7613
+ this.database = database;
7614
+ issues.delete("database");
7615
+ this.readerArtifactRetryAfter.delete("database");
7616
+ } catch (error) {
7617
+ setIssue("database", this.getDatabaseReadIssueMessage(), error);
7618
+ }
7619
+ } else if ((this.store?.count() ?? 0) > 0 || issues.has("database")) {
7620
+ setIssue("database", this.getDatabaseReadIssueMessage());
7621
+ }
7622
+ }
7623
+ if (!issues.has("database")) {
7624
+ try {
7625
+ this.indexCompatibility = this.validateIndexCompatibility(this.configuredProviderInfo);
7626
+ } catch (error) {
7627
+ setIssue("database", this.getDatabaseReadIssueMessage(), error);
7628
+ }
7629
+ }
7630
+ this.readIssues = Array.from(issues.values());
7631
+ this.readerArtifactFingerprint = currentFingerprint;
7632
+ }
7633
+ refreshInactiveWriterArtifacts() {
7634
+ if (this.initializationMode !== "writer" || this.activeIndexLease) {
7635
+ return true;
7636
+ }
7637
+ const previousFingerprint = this.writerArtifactFingerprint;
7638
+ const currentFingerprint = this.captureReaderArtifactFingerprint();
7639
+ const retryDue = this.readIssues.some(
7640
+ (issue) => Date.now() >= (this.readerArtifactRetryAfter.get(issue.component) ?? 0)
7641
+ );
7642
+ const artifactsChanged = !previousFingerprint || currentFingerprint.vectors !== previousFingerprint.vectors || currentFingerprint.keyword !== previousFingerprint.keyword || currentFingerprint.database !== previousFingerprint.database || currentFingerprint.databaseIdentity !== previousFingerprint.databaseIdentity;
7643
+ if (!artifactsChanged && !retryDue) {
7644
+ return true;
7645
+ }
7646
+ if (!previousFingerprint || currentFingerprint.databaseIdentity !== previousFingerprint.databaseIdentity) {
7647
+ return false;
7648
+ }
7649
+ this.initializationMode = "reader";
7650
+ this.readerArtifactFingerprint = previousFingerprint;
7651
+ try {
7652
+ this.refreshReaderArtifacts();
7653
+ this.writerArtifactFingerprint = this.readerArtifactFingerprint ?? currentFingerprint;
7654
+ } finally {
7655
+ this.readerArtifactFingerprint = null;
7656
+ this.initializationMode = "writer";
7657
+ }
7658
+ return true;
6767
7659
  }
6768
- async initialize() {
7660
+ async initializeUnlocked(mode, recoveredOwners = [], options = {}) {
7661
+ if (mode === "writer") {
7662
+ this.requireActiveLease();
7663
+ }
7664
+ this.readIssues = [];
7665
+ this.readerArtifactRetryAfter.clear();
6769
7666
  if (this.config.embeddingProvider === "custom") {
6770
7667
  if (!this.config.customProvider) {
6771
7668
  throw new Error("embeddingProvider is 'custom' but customProvider config is missing.");
@@ -6797,36 +7694,103 @@ var Indexer = class {
6797
7694
  });
6798
7695
  }
6799
7696
  }
6800
- await fsPromises2.mkdir(this.indexPath, { recursive: true });
6801
7697
  const dimensions = this.configuredProviderInfo.modelInfo.dimensions;
6802
- const storePath = path10.join(this.indexPath, "vectors");
6803
- this.store = new VectorStore(storePath, dimensions);
6804
- const indexFilePath = path10.join(this.indexPath, "vectors.usearch");
6805
- if (existsSync6(indexFilePath)) {
6806
- this.store.load();
6807
- }
6808
- const invertedIndexPath = path10.join(this.indexPath, "inverted-index.json");
6809
- this.invertedIndex = new InvertedIndex(invertedIndexPath);
6810
- try {
6811
- this.invertedIndex.load();
6812
- } catch {
6813
- if (existsSync6(invertedIndexPath)) {
6814
- await fsPromises2.unlink(invertedIndexPath);
7698
+ const storePath = path12.join(this.indexPath, "vectors");
7699
+ const vectorMetadataPath = `${storePath}.meta.json`;
7700
+ const invertedIndexPath = path12.join(this.indexPath, "inverted-index.json");
7701
+ const dbPath = path12.join(this.indexPath, "codebase.db");
7702
+ let dbIsNew = !existsSync7(dbPath);
7703
+ const readerArtifactFingerprint = mode === "reader" ? this.captureReaderArtifactFingerprint() : null;
7704
+ if (mode === "writer") {
7705
+ await fsPromises2.mkdir(this.indexPath, { recursive: true });
7706
+ if (recoveredOwners.length > 0 && this.config.scope === "project" && !this.isLocalProjectIndexPath()) {
7707
+ throw new Error(
7708
+ "Interrupted indexing recovery is unsafe while using an inherited worktree index. Run index_codebase with force=true to create a local project index boundary."
7709
+ );
7710
+ }
7711
+ for (const recoveredOwner of recoveredOwners) {
7712
+ recoverLeaseArtifacts(this.indexPath, recoveredOwner, [
7713
+ storePath,
7714
+ `${storePath}.meta.json`
7715
+ ]);
7716
+ }
7717
+ if (recoveredOwners.length > 0 && this.config.scope === "project") {
7718
+ await this.resetLocalIndexArtifacts();
7719
+ }
7720
+ this.store = new VectorStore(storePath, dimensions);
7721
+ if (existsSync7(storePath) || existsSync7(vectorMetadataPath)) {
7722
+ this.store.load();
6815
7723
  }
6816
7724
  this.invertedIndex = new InvertedIndex(invertedIndexPath);
6817
- }
6818
- const dbPath = path10.join(this.indexPath, "codebase.db");
6819
- let dbIsNew = !existsSync6(dbPath);
6820
- try {
6821
- this.database = new Database(dbPath);
6822
- } catch (error) {
6823
- if (!await this.tryResetCorruptedIndex("initializing index database", error)) {
6824
- throw error;
7725
+ try {
7726
+ this.invertedIndex.load();
7727
+ } catch {
7728
+ if (existsSync7(invertedIndexPath)) {
7729
+ await fsPromises2.unlink(invertedIndexPath);
7730
+ }
7731
+ this.invertedIndex = new InvertedIndex(invertedIndexPath);
7732
+ }
7733
+ try {
7734
+ this.database = new Database(dbPath);
7735
+ } catch (error) {
7736
+ if (!await this.tryResetCorruptedIndex("initializing index database", error)) {
7737
+ throw error;
7738
+ }
7739
+ this.store = new VectorStore(storePath, dimensions);
7740
+ this.invertedIndex = new InvertedIndex(invertedIndexPath);
7741
+ this.database = new Database(dbPath);
7742
+ dbIsNew = true;
6825
7743
  }
7744
+ } else {
6826
7745
  this.store = new VectorStore(storePath, dimensions);
7746
+ const vectorStoreExists = existsSync7(storePath);
7747
+ const vectorMetadataExists = existsSync7(vectorMetadataPath);
7748
+ const vectorReadFailureMessage = this.getVectorReadIssueMessage();
7749
+ if (vectorStoreExists !== vectorMetadataExists) {
7750
+ this.recordReadIssue("vectors", vectorReadFailureMessage);
7751
+ } else if (vectorStoreExists) {
7752
+ try {
7753
+ this.store.loadStrict();
7754
+ } catch (error) {
7755
+ this.recordReadIssue("vectors", vectorReadFailureMessage, error);
7756
+ this.store = new VectorStore(storePath, dimensions);
7757
+ }
7758
+ }
6827
7759
  this.invertedIndex = new InvertedIndex(invertedIndexPath);
6828
- this.database = new Database(dbPath);
6829
- dbIsNew = true;
7760
+ if (existsSync7(invertedIndexPath)) {
7761
+ try {
7762
+ this.invertedIndex.load();
7763
+ } catch (error) {
7764
+ this.recordReadIssue(
7765
+ "keyword",
7766
+ this.getKeywordReadIssueMessage(),
7767
+ error
7768
+ );
7769
+ this.invertedIndex = new InvertedIndex(invertedIndexPath);
7770
+ }
7771
+ } else if (this.store.count() > 0) {
7772
+ this.recordReadIssue("keyword", this.getKeywordReadIssueMessage());
7773
+ }
7774
+ if (existsSync7(dbPath)) {
7775
+ try {
7776
+ this.database = Database.openReadOnly(dbPath);
7777
+ } catch (error) {
7778
+ this.recordReadIssue(
7779
+ "database",
7780
+ this.getDatabaseReadIssueMessage(),
7781
+ error
7782
+ );
7783
+ this.database = Database.createEmptyReadOnly();
7784
+ }
7785
+ } else {
7786
+ this.database = Database.createEmptyReadOnly();
7787
+ if (this.store.count() > 0) {
7788
+ this.recordReadIssue(
7789
+ "database",
7790
+ `Index database is missing for the published vectors. ${this.getDatabaseReadIssueMessage()}`
7791
+ );
7792
+ }
7793
+ }
6830
7794
  }
6831
7795
  if (isGitRepo(this.projectRoot)) {
6832
7796
  this.currentBranch = getBranchOrDefault(this.projectRoot);
@@ -6840,10 +7804,10 @@ var Indexer = class {
6840
7804
  this.baseBranch = "default";
6841
7805
  this.logger.branch("debug", "Not a git repository, using default branch");
6842
7806
  }
6843
- if (this.checkForInterruptedIndexing()) {
6844
- await this.recoverFromInterruptedIndexing();
7807
+ if (mode === "writer" && recoveredOwners.length > 0) {
7808
+ await this.recoverFromInterruptedIndexingUnlocked(recoveredOwners);
6845
7809
  }
6846
- if (dbIsNew && this.store.count() > 0) {
7810
+ if (mode === "writer" && dbIsNew && this.store.count() > 0) {
6847
7811
  this.migrateFromLegacyIndex();
6848
7812
  }
6849
7813
  this.loadFileHashCache();
@@ -6855,9 +7819,11 @@ var Indexer = class {
6855
7819
  configuredProviderInfo: this.configuredProviderInfo
6856
7820
  });
6857
7821
  }
6858
- if (this.config.indexing.autoGc) {
7822
+ if (mode === "writer" && this.config.indexing.autoGc && !options.skipAutoGc) {
6859
7823
  await this.maybeRunAutoGc();
6860
7824
  }
7825
+ this.initializationMode = mode;
7826
+ this.readerArtifactFingerprint = readerArtifactFingerprint;
6861
7827
  }
6862
7828
  async maybeRunAutoGc() {
6863
7829
  if (!this.database) return;
@@ -6874,7 +7840,7 @@ var Indexer = class {
6874
7840
  }
6875
7841
  }
6876
7842
  if (shouldRunGc) {
6877
- const result = await this.healthCheck();
7843
+ const result = await this.healthCheckUnlocked();
6878
7844
  if (result.warning) {
6879
7845
  this.database.setMetadata(STARTUP_WARNING_METADATA_KEY, result.warning);
6880
7846
  } else {
@@ -6896,7 +7862,7 @@ var Indexer = class {
6896
7862
  if (await this.tryResetCorruptedIndex("running automatic orphan garbage collection", error)) {
6897
7863
  return {
6898
7864
  resetCorruptedIndex: true,
6899
- warning: this.getCorruptedIndexWarning(path10.join(this.indexPath, "codebase.db"))
7865
+ warning: this.getCorruptedIndexWarning(path12.join(this.indexPath, "codebase.db"))
6900
7866
  };
6901
7867
  }
6902
7868
  throw error;
@@ -6911,28 +7877,29 @@ var Indexer = class {
6911
7877
  return;
6912
7878
  }
6913
7879
  const retainedEntries = store.getAllMetadata().filter(({ key }) => !excludedSet.has(key));
6914
- const storeBasePath = path10.join(this.indexPath, "vectors");
6915
- const storeIndexPath = `${storeBasePath}.usearch`;
7880
+ const storeBasePath = path12.join(this.indexPath, "vectors");
7881
+ const storeIndexPath = storeBasePath;
6916
7882
  const storeMetadataPath = `${storeBasePath}.meta.json`;
6917
- const backupIndexPath = `${storeIndexPath}.bak`;
6918
- const backupMetadataPath = `${storeMetadataPath}.bak`;
7883
+ const lease = this.requireActiveLease();
7884
+ const backupIndexPath = createLeaseTemporaryPath(storeIndexPath, lease.owner, "bak");
7885
+ const backupMetadataPath = createLeaseTemporaryPath(storeMetadataPath, lease.owner, "bak");
6919
7886
  let backedUpIndex = false;
6920
7887
  let backedUpMetadata = false;
6921
7888
  let rebuiltCount = 0;
6922
7889
  let skippedCount = 0;
6923
- if (existsSync6(backupIndexPath)) {
7890
+ if (existsSync7(backupIndexPath)) {
6924
7891
  unlinkSync(backupIndexPath);
6925
7892
  }
6926
- if (existsSync6(backupMetadataPath)) {
7893
+ if (existsSync7(backupMetadataPath)) {
6927
7894
  unlinkSync(backupMetadataPath);
6928
7895
  }
6929
7896
  try {
6930
- if (existsSync6(storeIndexPath)) {
6931
- renameSync(storeIndexPath, backupIndexPath);
7897
+ if (existsSync7(storeIndexPath)) {
7898
+ renameSync2(storeIndexPath, backupIndexPath);
6932
7899
  backedUpIndex = true;
6933
7900
  }
6934
- if (existsSync6(storeMetadataPath)) {
6935
- renameSync(storeMetadataPath, backupMetadataPath);
7901
+ if (existsSync7(storeMetadataPath)) {
7902
+ renameSync2(storeMetadataPath, backupMetadataPath);
6936
7903
  backedUpMetadata = true;
6937
7904
  }
6938
7905
  store.clear();
@@ -6952,10 +7919,10 @@ var Indexer = class {
6952
7919
  rebuiltCount += 1;
6953
7920
  }
6954
7921
  store.save();
6955
- if (backedUpIndex && existsSync6(backupIndexPath)) {
7922
+ if (backedUpIndex && existsSync7(backupIndexPath)) {
6956
7923
  unlinkSync(backupIndexPath);
6957
7924
  }
6958
- if (backedUpMetadata && existsSync6(backupMetadataPath)) {
7925
+ if (backedUpMetadata && existsSync7(backupMetadataPath)) {
6959
7926
  unlinkSync(backupMetadataPath);
6960
7927
  }
6961
7928
  this.logger.gc("info", "Rebuilt vector store to avoid native remove", {
@@ -6968,17 +7935,17 @@ var Indexer = class {
6968
7935
  store.clear();
6969
7936
  } catch {
6970
7937
  }
6971
- if (existsSync6(storeIndexPath)) {
7938
+ if (existsSync7(storeIndexPath)) {
6972
7939
  unlinkSync(storeIndexPath);
6973
7940
  }
6974
- if (existsSync6(storeMetadataPath)) {
7941
+ if (existsSync7(storeMetadataPath)) {
6975
7942
  unlinkSync(storeMetadataPath);
6976
7943
  }
6977
- if (backedUpIndex && existsSync6(backupIndexPath)) {
6978
- renameSync(backupIndexPath, storeIndexPath);
7944
+ if (backedUpIndex && existsSync7(backupIndexPath)) {
7945
+ renameSync2(backupIndexPath, storeIndexPath);
6979
7946
  }
6980
- if (backedUpMetadata && existsSync6(backupMetadataPath)) {
6981
- renameSync(backupMetadataPath, storeMetadataPath);
7947
+ if (backedUpMetadata && existsSync7(backupMetadataPath)) {
7948
+ renameSync2(backupMetadataPath, storeMetadataPath);
6982
7949
  }
6983
7950
  if (backedUpIndex || backedUpMetadata) {
6984
7951
  store.load();
@@ -6992,11 +7959,37 @@ var Indexer = class {
6992
7959
  }
6993
7960
  return `Detected a corrupted local SQLite index at ${dbPath} and reset the local index. Run index_codebase to rebuild search data.`;
6994
7961
  }
7962
+ async resetLocalIndexArtifacts() {
7963
+ this.store = null;
7964
+ this.invertedIndex = null;
7965
+ this.database?.close();
7966
+ this.database = null;
7967
+ this.indexCompatibility = null;
7968
+ this.initializationMode = "none";
7969
+ this.readIssues = [];
7970
+ this.readerArtifactFingerprint = null;
7971
+ this.writerArtifactFingerprint = null;
7972
+ this.readerArtifactRetryAfter.clear();
7973
+ this.fileHashCache.clear();
7974
+ const resetPaths = [
7975
+ path12.join(this.indexPath, "codebase.db"),
7976
+ path12.join(this.indexPath, "codebase.db-shm"),
7977
+ path12.join(this.indexPath, "codebase.db-wal"),
7978
+ path12.join(this.indexPath, "vectors"),
7979
+ path12.join(this.indexPath, "vectors.usearch"),
7980
+ path12.join(this.indexPath, "vectors.meta.json"),
7981
+ path12.join(this.indexPath, "inverted-index.json"),
7982
+ path12.join(this.indexPath, "file-hashes.json"),
7983
+ path12.join(this.indexPath, "failed-batches.json")
7984
+ ];
7985
+ await Promise.all(resetPaths.map((targetPath) => fsPromises2.rm(targetPath, { recursive: true, force: true })));
7986
+ await fsPromises2.mkdir(this.indexPath, { recursive: true });
7987
+ }
6995
7988
  async tryResetCorruptedIndex(stage, error) {
6996
7989
  if (!isSqliteCorruptionError(error)) {
6997
7990
  return false;
6998
7991
  }
6999
- const dbPath = path10.join(this.indexPath, "codebase.db");
7992
+ const dbPath = path12.join(this.indexPath, "codebase.db");
7000
7993
  const warning = this.getCorruptedIndexWarning(dbPath);
7001
7994
  const errorMessage = getErrorMessage2(error);
7002
7995
  if (this.config.scope === "global") {
@@ -7012,30 +8005,7 @@ var Indexer = class {
7012
8005
  dbPath,
7013
8006
  error: errorMessage
7014
8007
  });
7015
- this.store = null;
7016
- this.invertedIndex = null;
7017
- this.database?.close();
7018
- this.database = null;
7019
- this.indexCompatibility = null;
7020
- this.fileHashCache.clear();
7021
- const resetPaths = [
7022
- path10.join(this.indexPath, "codebase.db"),
7023
- path10.join(this.indexPath, "codebase.db-shm"),
7024
- path10.join(this.indexPath, "codebase.db-wal"),
7025
- path10.join(this.indexPath, "vectors.usearch"),
7026
- path10.join(this.indexPath, "inverted-index.json"),
7027
- path10.join(this.indexPath, "file-hashes.json"),
7028
- path10.join(this.indexPath, "failed-batches.json"),
7029
- path10.join(this.indexPath, "indexing.lock"),
7030
- path10.join(this.indexPath, "vectors")
7031
- ];
7032
- await Promise.all(resetPaths.map(async (targetPath) => {
7033
- try {
7034
- await fsPromises2.rm(targetPath, { recursive: true, force: true });
7035
- } catch {
7036
- }
7037
- }));
7038
- await fsPromises2.mkdir(this.indexPath, { recursive: true });
8008
+ await this.resetLocalIndexArtifacts();
7039
8009
  return true;
7040
8010
  }
7041
8011
  migrateFromLegacyIndex() {
@@ -7154,8 +8124,62 @@ var Indexer = class {
7154
8124
  return this.indexCompatibility;
7155
8125
  }
7156
8126
  async ensureInitialized() {
8127
+ let initializedReader = false;
8128
+ while (true) {
8129
+ if (this.initializationPromise) {
8130
+ await this.initializationPromise;
8131
+ }
8132
+ if (!this.isInitializedFor("reader")) {
8133
+ await this.initialize();
8134
+ initializedReader = true;
8135
+ continue;
8136
+ }
8137
+ if (this.initializationMode === "writer" && !this.activeIndexLease && !initializedReader) {
8138
+ if (!this.refreshInactiveWriterArtifacts()) {
8139
+ this.resetLoadedIndexState(true);
8140
+ await this.initialize();
8141
+ initializedReader = true;
8142
+ continue;
8143
+ }
8144
+ }
8145
+ if (this.initializationMode === "reader" && !initializedReader) {
8146
+ this.refreshReaderArtifacts();
8147
+ }
8148
+ const state = this.requireLoadedIndexState();
8149
+ return {
8150
+ ...state,
8151
+ readIssues: [...this.readIssues],
8152
+ compatibility: this.indexCompatibility ?? this.validateIndexCompatibility(state.configuredProviderInfo)
8153
+ };
8154
+ }
8155
+ }
8156
+ async ensureInitializedUnlocked(recoveredOwners = []) {
8157
+ this.requireActiveLease();
8158
+ if (this.initializationPromise) {
8159
+ await this.initializationPromise;
8160
+ }
8161
+ if (recoveredOwners.length > 0 || !this.isInitializedFor("writer")) {
8162
+ const retireReaderDatabase = this.initializationMode === "reader";
8163
+ this.resetLoadedIndexState(retireReaderDatabase);
8164
+ await this.initializeOnce("writer", recoveredOwners, { skipAutoGc: true });
8165
+ } else {
8166
+ this.refreshLoadedIndexState();
8167
+ }
8168
+ if (this.config.indexing.autoGc) {
8169
+ await this.maybeRunAutoGc();
8170
+ }
8171
+ return this.requireLoadedIndexState();
8172
+ }
8173
+ requireReadableComponents(readIssues, ...components) {
8174
+ const componentSet = new Set(components);
8175
+ const issues = readIssues.filter((issue) => issue.blocking && componentSet.has(issue.component));
8176
+ if (issues.length > 0) {
8177
+ throw new Error(issues.map((issue) => issue.message).join(" "));
8178
+ }
8179
+ }
8180
+ requireLoadedIndexState() {
7157
8181
  if (!this.store || !this.provider || !this.invertedIndex || !this.configuredProviderInfo || !this.database) {
7158
- await this.initialize();
8182
+ throw new Error("Index state is not initialized");
7159
8183
  }
7160
8184
  return {
7161
8185
  store: this.store,
@@ -7179,7 +8203,12 @@ var Indexer = class {
7179
8203
  return createCostEstimate(files, configuredProviderInfo);
7180
8204
  }
7181
8205
  async index(onProgress) {
7182
- const { store, provider, invertedIndex, database, configuredProviderInfo } = await this.ensureInitialized();
8206
+ return this.withIndexMutationLease("index", async (recoveredOwners) => {
8207
+ return this.indexUnlocked(onProgress, recoveredOwners);
8208
+ });
8209
+ }
8210
+ async indexUnlocked(onProgress, recoveredOwners = [], stateReady = false) {
8211
+ const { store, provider, invertedIndex, database, configuredProviderInfo } = stateReady ? this.requireLoadedIndexState() : await this.ensureInitializedUnlocked(recoveredOwners);
7183
8212
  const scopedRoots = this.config.scope === "global" ? this.getScopedRoots() : null;
7184
8213
  const branchCatalogKey = this.getBranchCatalogKey();
7185
8214
  const forceScopedReembed = scopedRoots !== null && database.getMetadata(this.getProjectForceReembedMetadataKey()) === "true";
@@ -7189,7 +8218,6 @@ var Indexer = class {
7189
8218
  `${this.indexCompatibility?.reason} Run index_codebase with force=true to rebuild the index.`
7190
8219
  );
7191
8220
  }
7192
- this.acquireIndexingLock();
7193
8221
  this.logger.recordIndexingStart();
7194
8222
  this.logger.info("Starting indexing", { projectRoot: this.projectRoot });
7195
8223
  const startTime = Date.now();
@@ -7264,6 +8292,7 @@ var Indexer = class {
7264
8292
  this.logger.debug("Parsed changed files", { parsedCount: parsedFiles.length, parseMs: parseMs.toFixed(2) });
7265
8293
  const existingChunks = /* @__PURE__ */ new Map();
7266
8294
  const existingChunksByFile = /* @__PURE__ */ new Map();
8295
+ const existingMetadataById = /* @__PURE__ */ new Map();
7267
8296
  for (const { key, metadata } of store.getAllMetadata()) {
7268
8297
  if (scopedRoots && !this.isFileInCurrentScope(metadata.filePath, scopedRoots)) {
7269
8298
  continue;
@@ -7272,6 +8301,7 @@ var Indexer = class {
7272
8301
  continue;
7273
8302
  }
7274
8303
  existingChunks.set(key, metadata.hash);
8304
+ existingMetadataById.set(key, metadata);
7275
8305
  const fileChunks = existingChunksByFile.get(metadata.filePath) || /* @__PURE__ */ new Set();
7276
8306
  fileChunks.add(key);
7277
8307
  existingChunksByFile.set(metadata.filePath, fileChunks);
@@ -7279,6 +8309,8 @@ var Indexer = class {
7279
8309
  const currentChunkIds = /* @__PURE__ */ new Set();
7280
8310
  const currentFilePaths = /* @__PURE__ */ new Set();
7281
8311
  const pendingChunks = [];
8312
+ const gitBlameEnabled = this.config.indexing.gitBlame.enabled && isGitRepo(this.projectRoot);
8313
+ let backfilledBlameMetadata = false;
7282
8314
  for (const filePath of unchangedFilePaths) {
7283
8315
  currentFilePaths.add(filePath);
7284
8316
  const fileChunks = existingChunksByFile.get(filePath);
@@ -7289,10 +8321,52 @@ var Indexer = class {
7289
8321
  }
7290
8322
  }
7291
8323
  const chunkDataBatch = [];
8324
+ if (gitBlameEnabled) {
8325
+ const backfillItems = [];
8326
+ for (const chunkId of currentChunkIds) {
8327
+ const metadata = existingMetadataById.get(chunkId);
8328
+ if (!metadata || hasBlameMetadata(metadata)) {
8329
+ continue;
8330
+ }
8331
+ const chunk = database.getChunk(chunkId);
8332
+ if (!chunk) {
8333
+ continue;
8334
+ }
8335
+ const blame = await getChunkGitBlame(this.projectRoot, chunk.filePath, chunk.startLine, chunk.endLine);
8336
+ const blameMetadata = metadataFromBlame(blame);
8337
+ if (!blameMetadata.blameSha) {
8338
+ continue;
8339
+ }
8340
+ chunkDataBatch.push({
8341
+ ...chunk,
8342
+ blameSha: blameMetadata.blameSha,
8343
+ blameAuthor: blameMetadata.blameAuthor,
8344
+ blameAuthorEmail: blameMetadata.blameAuthorEmail,
8345
+ blameCommittedAt: blameMetadata.blameCommittedAt,
8346
+ blameSummary: blameMetadata.blameSummary
8347
+ });
8348
+ const embeddingBuffer = database.getEmbedding(chunk.contentHash);
8349
+ if (!embeddingBuffer) {
8350
+ continue;
8351
+ }
8352
+ backfillItems.push({
8353
+ id: chunkId,
8354
+ vector: Array.from(bufferToFloat32Array(embeddingBuffer)),
8355
+ metadata: {
8356
+ ...metadata,
8357
+ ...blameMetadata
8358
+ }
8359
+ });
8360
+ }
8361
+ if (backfillItems.length > 0) {
8362
+ store.addBatch(backfillItems);
8363
+ backfilledBlameMetadata = true;
8364
+ }
8365
+ }
7292
8366
  for (const parsed of parsedFiles) {
7293
8367
  currentFilePaths.add(parsed.path);
7294
8368
  if (parsed.chunks.length === 0) {
7295
- const relativePath = path10.relative(this.projectRoot, parsed.path);
8369
+ const relativePath = path12.relative(this.projectRoot, parsed.path);
7296
8370
  stats.parseFailures.push(relativePath);
7297
8371
  }
7298
8372
  let fileChunkCount = 0;
@@ -7313,6 +8387,10 @@ var Indexer = class {
7313
8387
  }
7314
8388
  const id = generateChunkId(parsed.path, chunk);
7315
8389
  const contentHash = generateChunkHash(chunk);
8390
+ const existingContentHash = existingChunks.get(id);
8391
+ const existingChunk = gitBlameEnabled ? database.getChunk(id) : null;
8392
+ const blame = gitBlameEnabled && existingContentHash !== contentHash ? await getChunkGitBlame(this.projectRoot, parsed.path, chunk.startLine, chunk.endLine) : blameFromChunkData(existingChunk);
8393
+ const blameMetadata = metadataFromBlame(blame);
7316
8394
  currentChunkIds.add(id);
7317
8395
  chunkDataBatch.push({
7318
8396
  chunkId: id,
@@ -7322,9 +8400,14 @@ var Indexer = class {
7322
8400
  endLine: chunk.endLine,
7323
8401
  nodeType: chunk.chunkType,
7324
8402
  name: chunk.name,
7325
- language: chunk.language
8403
+ language: chunk.language,
8404
+ blameSha: blameMetadata.blameSha,
8405
+ blameAuthor: blameMetadata.blameAuthor,
8406
+ blameAuthorEmail: blameMetadata.blameAuthorEmail,
8407
+ blameCommittedAt: blameMetadata.blameCommittedAt,
8408
+ blameSummary: blameMetadata.blameSummary
7326
8409
  });
7327
- if (existingChunks.get(id) === contentHash) {
8410
+ if (existingContentHash === contentHash) {
7328
8411
  fileChunkCount++;
7329
8412
  continue;
7330
8413
  }
@@ -7339,7 +8422,8 @@ var Indexer = class {
7339
8422
  chunkType: chunk.chunkType,
7340
8423
  name: chunk.name,
7341
8424
  language: chunk.language,
7342
- hash: contentHash
8425
+ hash: contentHash,
8426
+ ...blameMetadata
7343
8427
  };
7344
8428
  pendingChunks.push({
7345
8429
  id,
@@ -7482,6 +8566,11 @@ var Indexer = class {
7482
8566
  database.addChunksToBranchBatch(branchCatalogKey, Array.from(currentChunkIds));
7483
8567
  database.clearBranchSymbols(branchCatalogKey);
7484
8568
  database.addSymbolsToBranchBatch(branchCatalogKey, Array.from(allSymbolIds));
8569
+ const vectorPath = path12.join(this.indexPath, "vectors");
8570
+ const shouldFingerprintLegacyPair = !store.hasFingerprint() && existsSync7(vectorPath) && existsSync7(`${vectorPath}.meta.json`);
8571
+ if (backfilledBlameMetadata || shouldFingerprintLegacyPair) {
8572
+ store.save();
8573
+ }
7485
8574
  if (scopedRoots) {
7486
8575
  this.replaceScopedFileHashCache(currentFileHashes, scopedRoots);
7487
8576
  this.clearScopedFailedBatches(scopedRoots);
@@ -7500,7 +8589,6 @@ var Indexer = class {
7500
8589
  chunksProcessed: 0,
7501
8590
  totalChunks: 0
7502
8591
  });
7503
- this.releaseIndexingLock();
7504
8592
  return stats;
7505
8593
  }
7506
8594
  if (pendingChunks.length === 0) {
@@ -7509,7 +8597,7 @@ var Indexer = class {
7509
8597
  database.clearBranchSymbols(branchCatalogKey);
7510
8598
  database.addSymbolsToBranchBatch(branchCatalogKey, Array.from(allSymbolIds));
7511
8599
  store.save();
7512
- invertedIndex.save();
8600
+ this.saveInvertedIndex(invertedIndex);
7513
8601
  if (scopedRoots) {
7514
8602
  this.replaceScopedFileHashCache(currentFileHashes, scopedRoots);
7515
8603
  this.clearScopedFailedBatches(scopedRoots);
@@ -7528,7 +8616,6 @@ var Indexer = class {
7528
8616
  chunksProcessed: 0,
7529
8617
  totalChunks: 0
7530
8618
  });
7531
- this.releaseIndexingLock();
7532
8619
  return stats;
7533
8620
  }
7534
8621
  onProgress?.({
@@ -7759,7 +8846,7 @@ var Indexer = class {
7759
8846
  database.clearBranchSymbols(branchCatalogKey);
7760
8847
  database.addSymbolsToBranchBatch(branchCatalogKey, Array.from(allSymbolIds));
7761
8848
  store.save();
7762
- invertedIndex.save();
8849
+ this.saveInvertedIndex(invertedIndex);
7763
8850
  if (scopedRoots) {
7764
8851
  this.replaceScopedFileHashCache(currentFileHashes, scopedRoots);
7765
8852
  } else {
@@ -7811,7 +8898,6 @@ var Indexer = class {
7811
8898
  chunksProcessed: stats.indexedChunks,
7812
8899
  totalChunks: pendingChunks.length
7813
8900
  });
7814
- this.releaseIndexingLock();
7815
8901
  return stats;
7816
8902
  }
7817
8903
  async getQueryEmbedding(query, provider) {
@@ -7877,8 +8963,8 @@ var Indexer = class {
7877
8963
  return intersection / union;
7878
8964
  }
7879
8965
  async search(query, limit, options) {
7880
- const { store, provider, database } = await this.ensureInitialized();
7881
- const compatibility = this.checkCompatibility();
8966
+ const { store, provider, invertedIndex, database, readIssues, compatibility } = await this.ensureInitialized();
8967
+ this.requireReadableComponents(readIssues, "vectors", "database");
7882
8968
  if (!compatibility.compatible) {
7883
8969
  throw new Error(
7884
8970
  `${compatibility.reason ?? "Index is incompatible with current embedding provider."} A possible solution is to run index_codebase with force=true to rebuild the index.`
@@ -7892,6 +8978,7 @@ var Indexer = class {
7892
8978
  const maxResults = limit ?? this.config.search.maxResults;
7893
8979
  const hybridWeight = options?.hybridWeight ?? this.config.search.hybridWeight;
7894
8980
  const fusionStrategy = this.config.search.fusionStrategy;
8981
+ const effectiveHybridWeight = fusionStrategy === "weighted" && readIssues.some((issue) => issue.component === "keyword") ? 0 : hybridWeight;
7895
8982
  const rrfK = this.config.search.rrfK;
7896
8983
  const rerankTopN = this.config.search.rerankTopN;
7897
8984
  const filterByBranch = options?.filterByBranch ?? true;
@@ -7900,7 +8987,7 @@ var Indexer = class {
7900
8987
  this.logger.search("debug", "Starting search", {
7901
8988
  query,
7902
8989
  maxResults,
7903
- hybridWeight,
8990
+ hybridWeight: effectiveHybridWeight,
7904
8991
  fusionStrategy,
7905
8992
  rrfK,
7906
8993
  rerankTopN,
@@ -7914,7 +9001,7 @@ var Indexer = class {
7914
9001
  const semanticResults = store.search(embedding, maxResults * 4);
7915
9002
  const vectorMs = performance2.now() - vectorStartTime;
7916
9003
  const keywordStartTime = performance2.now();
7917
- const keywordResults = await this.keywordSearch(query, maxResults * 4);
9004
+ const keywordResults = await this.keywordSearch(query, maxResults * 4, store, invertedIndex);
7918
9005
  const keywordMs = performance2.now() - keywordStartTime;
7919
9006
  let branchChunkIds = null;
7920
9007
  if (filterByBranch && (this.config.scope === "global" || this.currentBranch !== "default")) {
@@ -7951,7 +9038,7 @@ var Indexer = class {
7951
9038
  rrfK,
7952
9039
  rerankTopN,
7953
9040
  limit: maxResults,
7954
- hybridWeight,
9041
+ hybridWeight: effectiveHybridWeight,
7955
9042
  prioritizeSourcePaths: sourceIntent
7956
9043
  });
7957
9044
  const rerankedCombined = await this.rerankCandidatesWithApi(query, combined, {
@@ -8045,13 +9132,13 @@ var Indexer = class {
8045
9132
  content,
8046
9133
  score: r.score,
8047
9134
  chunkType: r.metadata.chunkType,
8048
- name: r.metadata.name
9135
+ name: r.metadata.name,
9136
+ blame: blameFromMetadata(r.metadata)
8049
9137
  };
8050
9138
  })
8051
9139
  );
8052
9140
  }
8053
- async keywordSearch(query, limit) {
8054
- const { store, invertedIndex } = await this.ensureInitialized();
9141
+ async keywordSearch(query, limit, store, invertedIndex) {
8055
9142
  const scores = invertedIndex.search(query);
8056
9143
  if (scores.size === 0) {
8057
9144
  return [];
@@ -8069,24 +9156,54 @@ var Indexer = class {
8069
9156
  return results.slice(0, limit);
8070
9157
  }
8071
9158
  async getStatus() {
8072
- const { store, configuredProviderInfo, database } = await this.ensureInitialized();
9159
+ const { store, configuredProviderInfo, database, readIssues, compatibility } = await this.ensureInitialized();
8073
9160
  const failedBatchesCount = this.getFailedBatchesCount();
9161
+ const vectorCount = store.count();
9162
+ const statusReadIssues = [...readIssues];
9163
+ let startupWarning = "";
9164
+ if (!statusReadIssues.some((issue) => issue.component === "database")) {
9165
+ try {
9166
+ startupWarning = database.getMetadata(STARTUP_WARNING_METADATA_KEY) ?? "";
9167
+ } catch (error) {
9168
+ const message = this.getDatabaseReadIssueMessage();
9169
+ statusReadIssues.push(this.createReadIssue("database", message));
9170
+ if (!this.readIssues.some((issue) => issue.component === "database")) {
9171
+ this.recordReadIssue("database", message, error);
9172
+ }
9173
+ }
9174
+ }
9175
+ const readWarning = statusReadIssues.map((issue) => issue.message).join(" ");
9176
+ const warning = [readWarning, startupWarning].filter((message) => message.length > 0).join(" ");
9177
+ const hasBlockingReadIssue = statusReadIssues.some((issue) => issue.blocking);
8074
9178
  return {
8075
- indexed: store.count() > 0,
8076
- vectorCount: store.count(),
9179
+ indexed: vectorCount > 0 && !hasBlockingReadIssue,
9180
+ vectorCount,
8077
9181
  provider: configuredProviderInfo.provider,
8078
9182
  model: configuredProviderInfo.modelInfo.model,
8079
9183
  indexPath: this.indexPath,
8080
9184
  currentBranch: this.currentBranch,
8081
9185
  baseBranch: this.baseBranch,
8082
- compatibility: this.indexCompatibility,
9186
+ compatibility,
8083
9187
  failedBatchesCount,
8084
9188
  failedBatchesPath: failedBatchesCount > 0 ? this.failedBatchesPath : void 0,
8085
- warning: database.getMetadata(STARTUP_WARNING_METADATA_KEY) ?? void 0
9189
+ warning: warning || void 0
8086
9190
  };
8087
9191
  }
9192
+ async forceIndex(onProgress) {
9193
+ return this.withIndexMutationLease("force-index", async (recoveredOwners) => {
9194
+ await this.ensureInitializedUnlocked(recoveredOwners);
9195
+ await this.clearIndexUnlocked();
9196
+ return this.indexUnlocked(onProgress, [], true);
9197
+ });
9198
+ }
8088
9199
  async clearIndex() {
8089
- const { store, invertedIndex, database } = await this.ensureInitialized();
9200
+ await this.withIndexMutationLease("clear", async (recoveredOwners) => {
9201
+ await this.ensureInitializedUnlocked(recoveredOwners);
9202
+ await this.clearIndexUnlocked();
9203
+ });
9204
+ }
9205
+ async clearIndexUnlocked() {
9206
+ const { store, invertedIndex, database } = this.requireLoadedIndexState();
8090
9207
  if (this.config.scope === "global") {
8091
9208
  store.load();
8092
9209
  invertedIndex.load();
@@ -8113,7 +9230,7 @@ var Indexer = class {
8113
9230
  store.clear();
8114
9231
  store.save();
8115
9232
  invertedIndex.clear();
8116
- invertedIndex.save();
9233
+ this.saveInvertedIndex(invertedIndex);
8117
9234
  this.fileHashCache.clear();
8118
9235
  this.saveFileHashCache();
8119
9236
  database.clearAllIndexedData();
@@ -8137,14 +9254,7 @@ var Indexer = class {
8137
9254
  this.indexCompatibility = compatibility;
8138
9255
  return;
8139
9256
  }
8140
- const localProjectIndexPaths = [path10.join(this.projectRoot, getHostProjectIndexRelativePath(this.host))];
8141
- if (this.host !== "opencode") {
8142
- localProjectIndexPaths.push(path10.join(this.projectRoot, getHostProjectIndexRelativePath("opencode")));
8143
- }
8144
- const isLocalProjectIndex = localProjectIndexPaths.some(
8145
- (localPath) => path10.resolve(this.indexPath) === path10.resolve(localPath)
8146
- );
8147
- if (!isLocalProjectIndex) {
9257
+ if (!this.isLocalProjectIndexPath()) {
8148
9258
  throw new Error(
8149
9259
  "Project-scoped force rebuild is unsafe while using an inherited worktree index. Create a local project config boundary before clearing the index."
8150
9260
  );
@@ -8152,7 +9262,7 @@ var Indexer = class {
8152
9262
  store.clear();
8153
9263
  store.save();
8154
9264
  invertedIndex.clear();
8155
- invertedIndex.save();
9265
+ this.saveInvertedIndex(invertedIndex);
8156
9266
  this.fileHashCache.clear();
8157
9267
  this.saveFileHashCache();
8158
9268
  database.clearAllIndexedData();
@@ -8170,7 +9280,13 @@ var Indexer = class {
8170
9280
  this.indexCompatibility = this.validateIndexCompatibility(this.configuredProviderInfo);
8171
9281
  }
8172
9282
  async healthCheck() {
8173
- const { store, invertedIndex, database } = await this.ensureInitialized();
9283
+ return this.withIndexMutationLease("health-check", async (recoveredOwners) => {
9284
+ await this.ensureInitializedUnlocked(recoveredOwners);
9285
+ return this.healthCheckUnlocked();
9286
+ });
9287
+ }
9288
+ async healthCheckUnlocked() {
9289
+ const { store, invertedIndex, database } = this.requireLoadedIndexState();
8174
9290
  this.logger.gc("info", "Starting health check");
8175
9291
  const allMetadata = store.getAllMetadata();
8176
9292
  const filePathsToChunkKeys = /* @__PURE__ */ new Map();
@@ -8183,7 +9299,7 @@ var Indexer = class {
8183
9299
  const removedChunkKeys = [];
8184
9300
  const chunkKeysByRemovedFile = /* @__PURE__ */ new Map();
8185
9301
  for (const [filePath, chunkKeys] of filePathsToChunkKeys) {
8186
- if (!existsSync6(filePath)) {
9302
+ if (!existsSync7(filePath)) {
8187
9303
  chunkKeysByRemovedFile.set(filePath, chunkKeys);
8188
9304
  for (const key of chunkKeys) {
8189
9305
  removedChunkKeys.push(key);
@@ -8208,7 +9324,7 @@ var Indexer = class {
8208
9324
  const removedCount = removedChunkKeys.length;
8209
9325
  if (removedCount > 0) {
8210
9326
  store.save();
8211
- invertedIndex.save();
9327
+ this.saveInvertedIndex(invertedIndex);
8212
9328
  }
8213
9329
  let gcOrphanEmbeddings;
8214
9330
  let gcOrphanChunks;
@@ -8223,7 +9339,7 @@ var Indexer = class {
8223
9339
  if (!await this.tryResetCorruptedIndex("running index health check", error)) {
8224
9340
  throw error;
8225
9341
  }
8226
- await this.ensureInitialized();
9342
+ await this.initializeUnlocked("writer", [], { skipAutoGc: true });
8227
9343
  return {
8228
9344
  removed: 0,
8229
9345
  filePaths: [],
@@ -8232,7 +9348,7 @@ var Indexer = class {
8232
9348
  gcOrphanSymbols: 0,
8233
9349
  gcOrphanCallEdges: 0,
8234
9350
  resetCorruptedIndex: true,
8235
- warning: this.getCorruptedIndexWarning(path10.join(this.indexPath, "codebase.db"))
9351
+ warning: this.getCorruptedIndexWarning(path12.join(this.indexPath, "codebase.db"))
8236
9352
  };
8237
9353
  }
8238
9354
  this.logger.recordGc(removedCount, gcOrphanChunks, gcOrphanEmbeddings);
@@ -8245,7 +9361,13 @@ var Indexer = class {
8245
9361
  return { removed: removedCount, filePaths: removedFilePaths, gcOrphanEmbeddings, gcOrphanChunks, gcOrphanSymbols, gcOrphanCallEdges };
8246
9362
  }
8247
9363
  async retryFailedBatches() {
8248
- const { store, provider, invertedIndex, database, configuredProviderInfo } = await this.ensureInitialized();
9364
+ return this.withIndexMutationLease("retry-failed-batches", async (recoveredOwners) => {
9365
+ await this.ensureInitializedUnlocked(recoveredOwners);
9366
+ return this.retryFailedBatchesUnlocked();
9367
+ });
9368
+ }
9369
+ async retryFailedBatchesUnlocked() {
9370
+ const { store, provider, invertedIndex, database, configuredProviderInfo } = this.requireLoadedIndexState();
8249
9371
  const maxChunkTokens = getSafeEmbeddingChunkTokenLimit(configuredProviderInfo);
8250
9372
  const providerRateLimits = this.getProviderRateLimits(configuredProviderInfo.provider);
8251
9373
  const roots = this.config.scope === "global" ? this.getScopedRoots() : null;
@@ -8409,7 +9531,7 @@ var Indexer = class {
8409
9531
  }
8410
9532
  if (succeeded > 0) {
8411
9533
  store.save();
8412
- invertedIndex.save();
9534
+ this.saveInvertedIndex(invertedIndex);
8413
9535
  }
8414
9536
  if (roots && succeeded > 0 && persistedStillFailing.length === 0 && this.hasProjectForceReembedPending()) {
8415
9537
  database.deleteMetadata(this.getProjectForceReembedMetadataKey());
@@ -8437,15 +9559,16 @@ var Indexer = class {
8437
9559
  }
8438
9560
  }
8439
9561
  async getDatabaseStats() {
8440
- const { database } = await this.ensureInitialized();
9562
+ const { database, readIssues } = await this.ensureInitialized();
9563
+ this.requireReadableComponents(readIssues, "database");
8441
9564
  return database.getStats();
8442
9565
  }
8443
9566
  getLogger() {
8444
9567
  return this.logger;
8445
9568
  }
8446
9569
  async findSimilar(code, limit = this.config.search.maxResults, options) {
8447
- const { store, provider, database } = await this.ensureInitialized();
8448
- const compatibility = this.checkCompatibility();
9570
+ const { store, provider, database, readIssues, compatibility } = await this.ensureInitialized();
9571
+ this.requireReadableComponents(readIssues, "vectors", "database");
8449
9572
  if (!compatibility.compatible) {
8450
9573
  throw new Error(
8451
9574
  `${compatibility.reason ?? "Index is incompatible with current embedding provider."} Run index_codebase with force=true to rebuild the index.`
@@ -8552,13 +9675,15 @@ var Indexer = class {
8552
9675
  content,
8553
9676
  score: r.score,
8554
9677
  chunkType: r.metadata.chunkType,
8555
- name: r.metadata.name
9678
+ name: r.metadata.name,
9679
+ blame: blameFromMetadata(r.metadata)
8556
9680
  };
8557
9681
  })
8558
9682
  );
8559
9683
  }
8560
9684
  async getCallers(targetName, callTypeFilter) {
8561
- const { database } = await this.ensureInitialized();
9685
+ const { database, readIssues } = await this.ensureInitialized();
9686
+ this.requireReadableComponents(readIssues, "database");
8562
9687
  const seen = /* @__PURE__ */ new Set();
8563
9688
  const results = [];
8564
9689
  for (const branchKey of this.getBranchCatalogKeys()) {
@@ -8572,7 +9697,8 @@ var Indexer = class {
8572
9697
  return results;
8573
9698
  }
8574
9699
  async getCallees(symbolId, callTypeFilter) {
8575
- const { database } = await this.ensureInitialized();
9700
+ const { database, readIssues } = await this.ensureInitialized();
9701
+ this.requireReadableComponents(readIssues, "database");
8576
9702
  const seen = /* @__PURE__ */ new Set();
8577
9703
  const results = [];
8578
9704
  for (const branchKey of this.getBranchCatalogKeys()) {
@@ -8586,44 +9712,51 @@ var Indexer = class {
8586
9712
  return results;
8587
9713
  }
8588
9714
  async findCallPath(fromName, toName, maxDepth) {
8589
- const { database } = await this.ensureInitialized();
9715
+ const { database, readIssues } = await this.ensureInitialized();
9716
+ this.requireReadableComponents(readIssues, "database");
8590
9717
  let shortest = [];
8591
9718
  for (const branchKey of this.getBranchCatalogKeys()) {
8592
- const path25 = database.findShortestPath(fromName, toName, branchKey, maxDepth);
8593
- if (path25.length > 0 && (shortest.length === 0 || path25.length < shortest.length)) {
8594
- shortest = path25;
9719
+ const path27 = database.findShortestPath(fromName, toName, branchKey, maxDepth);
9720
+ if (path27.length > 0 && (shortest.length === 0 || path27.length < shortest.length)) {
9721
+ shortest = path27;
8595
9722
  }
8596
9723
  }
8597
9724
  return shortest;
8598
9725
  }
8599
9726
  async getSymbolsForBranch(branch) {
8600
- const { database } = await this.ensureInitialized();
9727
+ const { database, readIssues } = await this.ensureInitialized();
9728
+ this.requireReadableComponents(readIssues, "database");
8601
9729
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8602
9730
  return database.getSymbolsForBranch(resolvedBranch);
8603
9731
  }
8604
9732
  async getSymbolsForFiles(filePaths, branch) {
8605
- const { database } = await this.ensureInitialized();
9733
+ const { database, readIssues } = await this.ensureInitialized();
9734
+ this.requireReadableComponents(readIssues, "database");
8606
9735
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8607
9736
  return database.getSymbolsForFiles(filePaths, resolvedBranch);
8608
9737
  }
8609
9738
  async getTransitiveReachability(rootSymbolIds, direction, maxDepth) {
8610
- const { database } = await this.ensureInitialized();
9739
+ const { database, readIssues } = await this.ensureInitialized();
9740
+ this.requireReadableComponents(readIssues, "database");
8611
9741
  const branch = this.getBranchCatalogKey();
8612
9742
  return database.getTransitiveReachability(rootSymbolIds, branch, direction, maxDepth);
8613
9743
  }
8614
9744
  async detectCommunities(branch, symbolIds) {
8615
- const { database } = await this.ensureInitialized();
9745
+ const { database, readIssues } = await this.ensureInitialized();
9746
+ this.requireReadableComponents(readIssues, "database");
8616
9747
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8617
9748
  return database.detectCommunities(resolvedBranch, symbolIds);
8618
9749
  }
8619
9750
  async computeCentrality(branch) {
8620
- const { database } = await this.ensureInitialized();
9751
+ const { database, readIssues } = await this.ensureInitialized();
9752
+ this.requireReadableComponents(readIssues, "database");
8621
9753
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8622
9754
  return database.computeCentrality(resolvedBranch);
8623
9755
  }
8624
9756
  async getPrImpact(opts) {
8625
- const { database } = await this.ensureInitialized();
8626
- const execFileAsync2 = promisify2(execFile2);
9757
+ const { database, readIssues } = await this.ensureInitialized();
9758
+ this.requireReadableComponents(readIssues, "database");
9759
+ const execFileAsync3 = promisify3(execFile3);
8627
9760
  const changedFilesResult = await getChangedFiles({
8628
9761
  pr: opts.pr,
8629
9762
  branch: opts.branch,
@@ -8645,7 +9778,7 @@ var Indexer = class {
8645
9778
  "Run index_codebase first to build the call graph and symbol index for this branch."
8646
9779
  );
8647
9780
  }
8648
- const absoluteChangedFiles = changedFiles.map((f) => path10.resolve(this.projectRoot, f));
9781
+ const absoluteChangedFiles = changedFiles.map((f) => path12.resolve(this.projectRoot, f));
8649
9782
  const directSymbols = database.getSymbolsForFiles(absoluteChangedFiles, branchKey);
8650
9783
  const directIds = directSymbols.map((s) => s.id);
8651
9784
  const direction = opts.direction ?? "both";
@@ -8707,7 +9840,7 @@ var Indexer = class {
8707
9840
  if (opts.checkConflicts) {
8708
9841
  conflictingPRs = [];
8709
9842
  try {
8710
- const { stdout } = await execFileAsync2(
9843
+ const { stdout } = await execFileAsync3(
8711
9844
  "gh",
8712
9845
  ["pr", "list", "--state", "open", "--json", "number,headRefName", "--limit", "10000"],
8713
9846
  { cwd: this.projectRoot, timeout: 3e4 }
@@ -8728,7 +9861,7 @@ var Indexer = class {
8728
9861
  projectRoot: this.projectRoot,
8729
9862
  baseBranch: this.baseBranch
8730
9863
  });
8731
- const otherAbsolute = otherChanged.files.map((f) => path10.resolve(this.projectRoot, f));
9864
+ const otherAbsolute = otherChanged.files.map((f) => path12.resolve(this.projectRoot, f));
8732
9865
  const otherBranchKey = this.getBranchCatalogKeyFor(openPr.headRefName);
8733
9866
  const otherSymbols = database.getSymbolsForFiles(otherAbsolute, otherBranchKey);
8734
9867
  const otherLabels = /* @__PURE__ */ new Set();
@@ -8778,7 +9911,8 @@ var Indexer = class {
8778
9911
  };
8779
9912
  }
8780
9913
  async getVisualizationData(options) {
8781
- const { database, store } = await this.ensureInitialized();
9914
+ const { database, store, readIssues } = await this.ensureInitialized();
9915
+ this.requireReadableComponents(readIssues, "vectors", "database");
8782
9916
  const seenSymbols = /* @__PURE__ */ new Map();
8783
9917
  const seenEdges = /* @__PURE__ */ new Map();
8784
9918
  for (const branchKey of this.getBranchCatalogKeys()) {
@@ -8791,12 +9925,12 @@ var Indexer = class {
8791
9925
  if (meta.filePath) filePaths.add(meta.filePath);
8792
9926
  }
8793
9927
  const directory = options?.directory?.replace(/\/$/, "");
8794
- const absoluteDirectoryFilter = directory ? path10.resolve(this.projectRoot, directory) : void 0;
9928
+ const absoluteDirectoryFilter = directory ? path12.resolve(this.projectRoot, directory) : void 0;
8795
9929
  for (const filePath of filePaths) {
8796
9930
  if (directory) {
8797
- const absoluteFilePath = path10.resolve(filePath);
9931
+ const absoluteFilePath = path12.resolve(filePath);
8798
9932
  const matchesRelative = filePath === directory || filePath.startsWith(directory + "/");
8799
- const matchesProjectRelative = absoluteDirectoryFilter !== void 0 && (absoluteFilePath === absoluteDirectoryFilter || absoluteFilePath.startsWith(absoluteDirectoryFilter + path10.sep));
9933
+ const matchesProjectRelative = absoluteDirectoryFilter !== void 0 && (absoluteFilePath === absoluteDirectoryFilter || absoluteFilePath.startsWith(absoluteDirectoryFilter + path12.sep));
8800
9934
  if (!matchesRelative && !matchesProjectRelative) {
8801
9935
  continue;
8802
9936
  }
@@ -8818,12 +9952,23 @@ var Indexer = class {
8818
9952
  return { symbols: [...seenSymbols.values()], edges: [...seenEdges.values()] };
8819
9953
  }
8820
9954
  async close() {
8821
- await this.database?.close();
9955
+ this.database?.close();
9956
+ for (const database of this.retiredDatabases) {
9957
+ database.close();
9958
+ }
9959
+ this.retiredDatabases = [];
8822
9960
  this.database = null;
8823
9961
  this.store = null;
8824
9962
  this.invertedIndex = null;
8825
9963
  this.provider = null;
8826
9964
  this.reranker = null;
9965
+ this.configuredProviderInfo = null;
9966
+ this.indexCompatibility = null;
9967
+ this.initializationMode = "none";
9968
+ this.readIssues = [];
9969
+ this.readerArtifactFingerprint = null;
9970
+ this.writerArtifactFingerprint = null;
9971
+ this.readerArtifactRetryAfter.clear();
8827
9972
  }
8828
9973
  };
8829
9974
 
@@ -9074,15 +10219,15 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
9074
10219
  }
9075
10220
 
9076
10221
  // src/eval/runner-config.ts
9077
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, rmSync, writeFileSync as writeFileSync3 } from "fs";
9078
- import * as os4 from "os";
9079
- import * as path12 from "path";
10222
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
10223
+ import * as os5 from "os";
10224
+ import * as path14 from "path";
9080
10225
 
9081
10226
  // src/config/rebase.ts
9082
- import * as path11 from "path";
10227
+ import * as path13 from "path";
9083
10228
  function isWithinRoot(rootDir, targetPath) {
9084
- const relativePath = path11.relative(rootDir, targetPath);
9085
- return relativePath === "" || !relativePath.startsWith("..") && !path11.isAbsolute(relativePath);
10229
+ const relativePath = path13.relative(rootDir, targetPath);
10230
+ return relativePath === "" || !relativePath.startsWith("..") && !path13.isAbsolute(relativePath);
9086
10231
  }
9087
10232
  function rebasePathEntries(values, fromDir, toDir) {
9088
10233
  if (!Array.isArray(values)) {
@@ -9090,10 +10235,10 @@ function rebasePathEntries(values, fromDir, toDir) {
9090
10235
  }
9091
10236
  return values.filter((value) => typeof value === "string").map((value) => {
9092
10237
  const trimmed = value.trim();
9093
- if (!trimmed || path11.isAbsolute(trimmed)) {
10238
+ if (!trimmed || path13.isAbsolute(trimmed)) {
9094
10239
  return trimmed;
9095
10240
  }
9096
- return normalizePathSeparators(path11.normalize(path11.relative(toDir, path11.resolve(fromDir, trimmed))));
10241
+ return normalizePathSeparators(path13.normalize(path13.relative(toDir, path13.resolve(fromDir, trimmed))));
9097
10242
  }).filter(Boolean);
9098
10243
  }
9099
10244
  function resolveInheritedKnowledgeBaseEntries(values, sourceRoot, targetRoot) {
@@ -9105,17 +10250,17 @@ function resolveInheritedKnowledgeBaseEntries(values, sourceRoot, targetRoot) {
9105
10250
  if (!trimmed) {
9106
10251
  return trimmed;
9107
10252
  }
9108
- if (path11.isAbsolute(trimmed)) {
10253
+ if (path13.isAbsolute(trimmed)) {
9109
10254
  if (isWithinRoot(sourceRoot, trimmed)) {
9110
- return normalizePathSeparators(path11.normalize(path11.relative(sourceRoot, trimmed) || "."));
10255
+ return normalizePathSeparators(path13.normalize(path13.relative(sourceRoot, trimmed) || "."));
9111
10256
  }
9112
- return path11.normalize(trimmed);
10257
+ return path13.normalize(trimmed);
9113
10258
  }
9114
- const resolvedFromSource = path11.resolve(sourceRoot, trimmed);
10259
+ const resolvedFromSource = path13.resolve(sourceRoot, trimmed);
9115
10260
  if (isWithinRoot(sourceRoot, resolvedFromSource)) {
9116
- return normalizePathSeparators(path11.normalize(trimmed));
10261
+ return normalizePathSeparators(path13.normalize(trimmed));
9117
10262
  }
9118
- return normalizePathSeparators(path11.normalize(path11.relative(targetRoot, resolvedFromSource)));
10263
+ return normalizePathSeparators(path13.normalize(path13.relative(targetRoot, resolvedFromSource)));
9119
10264
  }).filter(Boolean);
9120
10265
  }
9121
10266
 
@@ -9153,7 +10298,7 @@ function validateEvalConfigShape(rawConfig, filePath) {
9153
10298
  }
9154
10299
  function parseJsonConfigFile(filePath) {
9155
10300
  try {
9156
- return validateEvalConfigShape(JSON.parse(readFileSync7(filePath, "utf-8")), filePath);
10301
+ return validateEvalConfigShape(JSON.parse(readFileSync8(filePath, "utf-8")), filePath);
9157
10302
  } catch (error) {
9158
10303
  if (error instanceof Error && error.message.startsWith("Eval config at ")) {
9159
10304
  throw error;
@@ -9163,20 +10308,20 @@ function parseJsonConfigFile(filePath) {
9163
10308
  }
9164
10309
  }
9165
10310
  function toAbsolute(projectRoot, maybeRelative) {
9166
- return path12.isAbsolute(maybeRelative) ? maybeRelative : path12.join(projectRoot, maybeRelative);
10311
+ return path14.isAbsolute(maybeRelative) ? maybeRelative : path14.join(projectRoot, maybeRelative);
9167
10312
  }
9168
10313
  function isProjectScopedConfigPath(configPath) {
9169
- return path12.basename(configPath) === "codebase-index.json" && path12.basename(path12.dirname(configPath)) === ".opencode";
10314
+ return path14.basename(configPath) === "codebase-index.json" && path14.basename(path14.dirname(configPath)) === ".opencode";
9170
10315
  }
9171
10316
  function normalizeEvalConfigKnowledgeBases(rawConfig, projectRoot, resolvedConfigPath) {
9172
10317
  const config = rawConfig && typeof rawConfig === "object" ? { ...rawConfig } : {};
9173
10318
  const rebaseEntries = (values) => isProjectScopedConfigPath(resolvedConfigPath) ? resolveInheritedKnowledgeBaseEntries(
9174
10319
  values,
9175
- path12.dirname(path12.dirname(resolvedConfigPath)),
10320
+ path14.dirname(path14.dirname(resolvedConfigPath)),
9176
10321
  projectRoot
9177
10322
  ) : rebasePathEntries(
9178
10323
  values,
9179
- path12.dirname(resolvedConfigPath),
10324
+ path14.dirname(resolvedConfigPath),
9180
10325
  projectRoot
9181
10326
  );
9182
10327
  if (Array.isArray(config.knowledgeBases)) {
@@ -9189,7 +10334,7 @@ function normalizeEvalConfigKnowledgeBases(rawConfig, projectRoot, resolvedConfi
9189
10334
  }
9190
10335
  function loadRawConfig(projectRoot, configPath) {
9191
10336
  const fromPath = configPath ? toAbsolute(projectRoot, configPath) : null;
9192
- if (fromPath && existsSync7(fromPath)) {
10337
+ if (fromPath && existsSync8(fromPath)) {
9193
10338
  return normalizeEvalConfigKnowledgeBases(
9194
10339
  parseJsonConfigFile(fromPath),
9195
10340
  projectRoot,
@@ -9197,15 +10342,15 @@ function loadRawConfig(projectRoot, configPath) {
9197
10342
  );
9198
10343
  }
9199
10344
  const projectConfig = resolveProjectConfigPath(projectRoot);
9200
- if (existsSync7(projectConfig)) {
10345
+ if (existsSync8(projectConfig)) {
9201
10346
  return normalizeEvalConfigKnowledgeBases(
9202
10347
  parseJsonConfigFile(projectConfig),
9203
10348
  projectRoot,
9204
10349
  projectConfig
9205
10350
  );
9206
10351
  }
9207
- const globalConfig = path12.join(os4.homedir(), ".config", "opencode", "codebase-index.json");
9208
- if (existsSync7(globalConfig)) {
10352
+ const globalConfig = path14.join(os5.homedir(), ".config", "opencode", "codebase-index.json");
10353
+ if (existsSync8(globalConfig)) {
9209
10354
  return parseJsonConfigFile(globalConfig);
9210
10355
  }
9211
10356
  return {};
@@ -9214,24 +10359,24 @@ function getIndexRootPath(projectRoot, scope) {
9214
10359
  return scope === "global" ? getGlobalIndexPath() : resolveProjectIndexPath(projectRoot, scope);
9215
10360
  }
9216
10361
  function getLocalProjectIndexRoot(projectRoot) {
9217
- return path12.join(projectRoot, ".opencode", "index");
10362
+ return path14.join(projectRoot, ".opencode", "index");
9218
10363
  }
9219
10364
  function getLocalProjectConfigPath(projectRoot) {
9220
- return path12.join(projectRoot, ".opencode", "codebase-index.json");
10365
+ return path14.join(projectRoot, ".opencode", "codebase-index.json");
9221
10366
  }
9222
10367
  function clearIndexRoot(projectRoot, scope) {
9223
10368
  const indexRoot = scope === "global" ? getIndexRootPath(projectRoot, scope) : getLocalProjectIndexRoot(projectRoot);
9224
- if (existsSync7(indexRoot)) {
9225
- rmSync(indexRoot, { recursive: true, force: true });
10369
+ if (existsSync8(indexRoot)) {
10370
+ rmSync2(indexRoot, { recursive: true, force: true });
9226
10371
  }
9227
10372
  }
9228
10373
  function ensureLocalEvalProjectConfig(projectRoot, configPath) {
9229
10374
  const localConfigPath = getLocalProjectConfigPath(projectRoot);
9230
10375
  const resolvedConfigPath = configPath ? toAbsolute(projectRoot, configPath) : resolveProjectConfigPath(projectRoot);
9231
- if (!configPath && existsSync7(localConfigPath)) {
10376
+ if (!configPath && existsSync8(localConfigPath)) {
9232
10377
  return localConfigPath;
9233
10378
  }
9234
- if (!existsSync7(resolvedConfigPath) || resolvedConfigPath === localConfigPath) {
10379
+ if (!existsSync8(resolvedConfigPath) || resolvedConfigPath === localConfigPath) {
9235
10380
  return resolvedConfigPath;
9236
10381
  }
9237
10382
  const sourceConfig = normalizeEvalConfigKnowledgeBases(
@@ -9239,8 +10384,8 @@ function ensureLocalEvalProjectConfig(projectRoot, configPath) {
9239
10384
  projectRoot,
9240
10385
  resolvedConfigPath
9241
10386
  );
9242
- mkdirSync3(path12.dirname(localConfigPath), { recursive: true });
9243
- writeFileSync3(localConfigPath, JSON.stringify(sourceConfig, null, 2), "utf-8");
10387
+ mkdirSync4(path14.dirname(localConfigPath), { recursive: true });
10388
+ writeFileSync4(localConfigPath, JSON.stringify(sourceConfig, null, 2), "utf-8");
9244
10389
  return localConfigPath;
9245
10390
  }
9246
10391
  function loadParsedConfig(projectRoot, configPath) {
@@ -9273,9 +10418,9 @@ function getEmbeddingCostPer1MTokens(embeddingProvider) {
9273
10418
  }
9274
10419
 
9275
10420
  // src/eval/schema.ts
9276
- import { readFileSync as readFileSync8 } from "fs";
10421
+ import { readFileSync as readFileSync9 } from "fs";
9277
10422
  function parseJsonFile(filePath) {
9278
- const content = readFileSync8(filePath, "utf-8");
10423
+ const content = readFileSync9(filePath, "utf-8");
9279
10424
  try {
9280
10425
  return JSON.parse(content);
9281
10426
  } catch (error) {
@@ -9289,23 +10434,23 @@ function isRecord2(value) {
9289
10434
  function isStringArray3(value) {
9290
10435
  return Array.isArray(value) && value.every((item) => typeof item === "string");
9291
10436
  }
9292
- function asPositiveNumber(value, path25) {
10437
+ function asPositiveNumber(value, path27) {
9293
10438
  if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
9294
- throw new Error(`${path25} must be a non-negative number`);
10439
+ throw new Error(`${path27} must be a non-negative number`);
9295
10440
  }
9296
10441
  return value;
9297
10442
  }
9298
- function parseQueryType(value, path25) {
10443
+ function parseQueryType(value, path27) {
9299
10444
  if (value === "definition" || value === "implementation-intent" || value === "similarity" || value === "keyword-heavy") {
9300
10445
  return value;
9301
10446
  }
9302
10447
  throw new Error(
9303
- `${path25} must be one of: definition, implementation-intent, similarity, keyword-heavy`
10448
+ `${path27} must be one of: definition, implementation-intent, similarity, keyword-heavy`
9304
10449
  );
9305
10450
  }
9306
- function parseExpected(input, path25) {
10451
+ function parseExpected(input, path27) {
9307
10452
  if (!isRecord2(input)) {
9308
- throw new Error(`${path25} must be an object`);
10453
+ throw new Error(`${path27} must be an object`);
9309
10454
  }
9310
10455
  const filePathRaw = input.filePath;
9311
10456
  const acceptableFilesRaw = input.acceptableFiles;
@@ -9314,16 +10459,16 @@ function parseExpected(input, path25) {
9314
10459
  const filePath = typeof filePathRaw === "string" ? filePathRaw : void 0;
9315
10460
  const acceptableFiles = isStringArray3(acceptableFilesRaw) ? acceptableFilesRaw : void 0;
9316
10461
  if (!filePath && (!acceptableFiles || acceptableFiles.length === 0)) {
9317
- throw new Error(`${path25} must include either expected.filePath or expected.acceptableFiles`);
10462
+ throw new Error(`${path27} must include either expected.filePath or expected.acceptableFiles`);
9318
10463
  }
9319
10464
  if (acceptableFilesRaw !== void 0 && !isStringArray3(acceptableFilesRaw)) {
9320
- throw new Error(`${path25}.acceptableFiles must be an array of strings`);
10465
+ throw new Error(`${path27}.acceptableFiles must be an array of strings`);
9321
10466
  }
9322
10467
  if (symbolRaw !== void 0 && typeof symbolRaw !== "string") {
9323
- throw new Error(`${path25}.symbol must be a string when provided`);
10468
+ throw new Error(`${path27}.symbol must be a string when provided`);
9324
10469
  }
9325
10470
  if (branchRaw !== void 0 && typeof branchRaw !== "string") {
9326
- throw new Error(`${path25}.branch must be a string when provided`);
10471
+ throw new Error(`${path27}.branch must be a string when provided`);
9327
10472
  }
9328
10473
  return {
9329
10474
  filePath,
@@ -9333,25 +10478,25 @@ function parseExpected(input, path25) {
9333
10478
  };
9334
10479
  }
9335
10480
  function parseQuery(input, index) {
9336
- const path25 = `queries[${index}]`;
10481
+ const path27 = `queries[${index}]`;
9337
10482
  if (!isRecord2(input)) {
9338
- throw new Error(`${path25} must be an object`);
10483
+ throw new Error(`${path27} must be an object`);
9339
10484
  }
9340
10485
  const id = input.id;
9341
10486
  const query = input.query;
9342
10487
  const queryType = input.queryType;
9343
10488
  const expected = input.expected;
9344
10489
  if (typeof id !== "string" || id.trim().length === 0) {
9345
- throw new Error(`${path25}.id must be a non-empty string`);
10490
+ throw new Error(`${path27}.id must be a non-empty string`);
9346
10491
  }
9347
10492
  if (typeof query !== "string" || query.trim().length === 0) {
9348
- throw new Error(`${path25}.query must be a non-empty string`);
10493
+ throw new Error(`${path27}.query must be a non-empty string`);
9349
10494
  }
9350
10495
  return {
9351
10496
  id,
9352
10497
  query,
9353
- queryType: parseQueryType(queryType, `${path25}.queryType`),
9354
- expected: parseExpected(expected, `${path25}.expected`)
10498
+ queryType: parseQueryType(queryType, `${path27}.queryType`),
10499
+ expected: parseExpected(expected, `${path27}.expected`)
9355
10500
  };
9356
10501
  }
9357
10502
  function parseGoldenDataset(raw, sourceLabel) {
@@ -9534,13 +10679,13 @@ async function runEvaluation(options) {
9534
10679
  };
9535
10680
  const outputDir = createRunDirectory(toAbsolute(options.projectRoot, options.outputRoot));
9536
10681
  const perQueryArtifact = buildPerQueryArtifact(perQuery);
9537
- writeJson(path13.join(outputDir, "summary.json"), summary);
9538
- writeJson(path13.join(outputDir, "per-query.json"), perQueryArtifact);
10682
+ writeJson(path15.join(outputDir, "summary.json"), summary);
10683
+ writeJson(path15.join(outputDir, "per-query.json"), perQueryArtifact);
9539
10684
  let comparison;
9540
10685
  if (againstPath) {
9541
10686
  const baseline = loadSummary(againstPath);
9542
10687
  comparison = compareSummaries(summary, baseline, againstPath);
9543
- writeJson(path13.join(outputDir, "compare.json"), comparison);
10688
+ writeJson(path15.join(outputDir, "compare.json"), comparison);
9544
10689
  }
9545
10690
  let gate;
9546
10691
  if (options.ciMode) {
@@ -9550,10 +10695,10 @@ async function runEvaluation(options) {
9550
10695
  const budget = loadBudget(budgetPath);
9551
10696
  if (!comparison && budget.baselinePath) {
9552
10697
  const resolvedBaseline = toAbsolute(options.projectRoot, budget.baselinePath);
9553
- if (existsSync8(resolvedBaseline)) {
10698
+ if (existsSync9(resolvedBaseline)) {
9554
10699
  const baselineSummary = loadSummary(resolvedBaseline);
9555
10700
  comparison = compareSummaries(summary, baselineSummary, resolvedBaseline);
9556
- writeJson(path13.join(outputDir, "compare.json"), comparison);
10701
+ writeJson(path15.join(outputDir, "compare.json"), comparison);
9557
10702
  } else if (budget.failOnMissingBaseline) {
9558
10703
  throw new Error(
9559
10704
  `Budget baseline is missing: ${resolvedBaseline}. Set failOnMissingBaseline=false to allow CI run without baseline.`
@@ -9563,7 +10708,7 @@ async function runEvaluation(options) {
9563
10708
  gate = evaluateBudgetGate(budget, summary, comparison);
9564
10709
  }
9565
10710
  const markdown = createSummaryMarkdown(summary, comparison, gate);
9566
- writeText(path13.join(outputDir, "summary.md"), markdown);
10711
+ writeText(path15.join(outputDir, "summary.md"), markdown);
9567
10712
  return { outputDir, summary, perQuery, comparison, gate };
9568
10713
  } finally {
9569
10714
  await indexer.close();
@@ -9621,23 +10766,23 @@ async function runSweep(options, sweep) {
9621
10766
  bestByMrrAt10,
9622
10767
  bestByP95Latency
9623
10768
  };
9624
- writeJson(path13.join(outputDir, "compare.json"), aggregate);
10769
+ writeJson(path15.join(outputDir, "compare.json"), aggregate);
9625
10770
  const md = createSummaryMarkdown(
9626
10771
  bestByHitAt5?.summary ?? runs[0].summary,
9627
10772
  bestByHitAt5?.comparison,
9628
10773
  void 0,
9629
10774
  aggregate
9630
10775
  );
9631
- writeText(path13.join(outputDir, "summary.md"), md);
9632
- writeJson(path13.join(outputDir, "summary.json"), bestByHitAt5?.summary ?? runs[0].summary);
10776
+ writeText(path15.join(outputDir, "summary.md"), md);
10777
+ writeJson(path15.join(outputDir, "summary.json"), bestByHitAt5?.summary ?? runs[0].summary);
9633
10778
  return { outputDir, aggregate };
9634
10779
  }
9635
10780
 
9636
10781
  // src/eval/cli.ts
9637
- import * as path15 from "path";
10782
+ import * as path17 from "path";
9638
10783
 
9639
10784
  // src/eval/cli-parser.ts
9640
- import * as path14 from "path";
10785
+ import * as path16 from "path";
9641
10786
  function printUsage() {
9642
10787
  console.log(`
9643
10788
  Usage:
@@ -9709,12 +10854,12 @@ function parseEvalArgs(argv, cwd) {
9709
10854
  const arg = argv[i];
9710
10855
  const next = argv[i + 1];
9711
10856
  if (arg === "--project" && next) {
9712
- parsed.projectRoot = path14.resolve(cwd, next);
10857
+ parsed.projectRoot = path16.resolve(cwd, next);
9713
10858
  i += 1;
9714
10859
  continue;
9715
10860
  }
9716
10861
  if (arg === "--config" && next) {
9717
- parsed.configPath = path14.resolve(cwd, next);
10862
+ parsed.configPath = path16.resolve(cwd, next);
9718
10863
  i += 1;
9719
10864
  continue;
9720
10865
  }
@@ -9910,22 +11055,22 @@ async function handleEvalCommand(args, cwd) {
9910
11055
  if (!parsed.againstPath.endsWith(".json")) {
9911
11056
  throw new Error("eval diff --against must point to a summary JSON file");
9912
11057
  }
9913
- const currentSummary = loadSummary(path15.resolve(parsed.projectRoot, currentPath), {
11058
+ const currentSummary = loadSummary(path17.resolve(parsed.projectRoot, currentPath), {
9914
11059
  allowLegacyDiversityMetrics: true
9915
11060
  });
9916
- const baselineSummary = loadSummary(path15.resolve(parsed.projectRoot, parsed.againstPath), {
11061
+ const baselineSummary = loadSummary(path17.resolve(parsed.projectRoot, parsed.againstPath), {
9917
11062
  allowLegacyDiversityMetrics: true
9918
11063
  });
9919
11064
  const comparison = compareSummaries(
9920
11065
  currentSummary,
9921
11066
  baselineSummary,
9922
- path15.resolve(parsed.projectRoot, parsed.againstPath)
11067
+ path17.resolve(parsed.projectRoot, parsed.againstPath)
9923
11068
  );
9924
- const outputDir = createRunDirectory(path15.resolve(parsed.projectRoot, parsed.outputRoot));
11069
+ const outputDir = createRunDirectory(path17.resolve(parsed.projectRoot, parsed.outputRoot));
9925
11070
  const summaryMd = createSummaryMarkdown(currentSummary, comparison);
9926
- writeJson(path15.join(outputDir, "compare.json"), comparison);
9927
- writeText(path15.join(outputDir, "summary.md"), summaryMd);
9928
- writeJson(path15.join(outputDir, "summary.json"), currentSummary);
11071
+ writeJson(path17.join(outputDir, "compare.json"), comparison);
11072
+ writeText(path17.join(outputDir, "summary.md"), summaryMd);
11073
+ writeJson(path17.join(outputDir, "summary.json"), currentSummary);
9929
11074
  console.log(`Eval diff complete. Artifacts: ${outputDir}`);
9930
11075
  return 0;
9931
11076
  }
@@ -9934,7 +11079,7 @@ async function handleEvalCommand(args, cwd) {
9934
11079
 
9935
11080
  // src/mcp-server.ts
9936
11081
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9937
- import { readFileSync as readFileSync10 } from "fs";
11082
+ import { readFileSync as readFileSync11 } from "fs";
9938
11083
 
9939
11084
  // src/mcp-server/register-prompts.ts
9940
11085
  import { z } from "zod";
@@ -10125,6 +11270,10 @@ function formatStatus(status) {
10125
11270
  lines.push(`Failed batches: ${status.failedBatchesPath}`);
10126
11271
  }
10127
11272
  }
11273
+ if (status.warning) {
11274
+ lines.push("");
11275
+ lines.push(`INDEX WARNING: ${status.warning}`);
11276
+ }
10128
11277
  if (status.compatibility && !status.compatibility.compatible) {
10129
11278
  lines.push("");
10130
11279
  lines.push(`COMPATIBILITY WARNING: ${status.compatibility.reason}`);
@@ -10170,6 +11319,17 @@ function calculatePercentage(progress) {
10170
11319
  if (progress.phase === "storing") return 95;
10171
11320
  return 0;
10172
11321
  }
11322
+ function formatCodebasePeek(results) {
11323
+ if (results.length === 0) {
11324
+ return "No matching code found. Try a different query or run index_codebase first.";
11325
+ }
11326
+ const formatted = results.map((r, idx) => {
11327
+ const location = `${r.filePath}:${r.startLine}-${r.endLine}`;
11328
+ const name = r.name ? `"${r.name}"` : "(anonymous)";
11329
+ return `[${idx + 1}] ${r.chunkType} ${name} at ${location} (score: ${r.score.toFixed(2)})${formatBlame(r)}`;
11330
+ });
11331
+ return formatted.join("\n");
11332
+ }
10173
11333
  function formatHealthCheck(result) {
10174
11334
  if (result.resetCorruptedIndex) {
10175
11335
  return result.warning ?? "Detected a corrupted local index and reset it. Run index_codebase to rebuild search data.";
@@ -10198,16 +11358,68 @@ function formatHealthCheck(result) {
10198
11358
  }
10199
11359
  return lines.join("\n");
10200
11360
  }
11361
+ function formatCallGraphCallers(name, callers, relationshipType) {
11362
+ if (callers.length === 0) {
11363
+ return `No callers found for "${name}"${relationshipType ? ` with type ${relationshipType}` : ""}. It may not be called by any tracked function, or the index needs updating.`;
11364
+ }
11365
+ const formatted = callers.map((edge, index) => {
11366
+ const confidence = edge.confidence !== "Direct" ? ` [${edge.confidence.toLowerCase()}]` : "";
11367
+ return `[${index + 1}] \u2190 from ${edge.fromSymbolName ?? "<unknown>"} in ${edge.fromSymbolFilePath ?? "<unknown file>"} [${edge.fromSymbolId}] (${edge.callType})${confidence} at line ${edge.line}${edge.isResolved ? " [resolved]" : " [unresolved]"}`;
11368
+ });
11369
+ return `"${name}" is called by ${callers.length} function(s):
11370
+
11371
+ ${formatted.join("\n")}`;
11372
+ }
11373
+ function formatCallGraphCallees(symbolId, callees, relationshipType) {
11374
+ if (callees.length === 0) {
11375
+ return `No callees found for symbol ${symbolId}${relationshipType ? ` with type ${relationshipType}` : ""}. The function may not call any other tracked functions.`;
11376
+ }
11377
+ return callees.map((edge, index) => {
11378
+ const confidence = edge.confidence !== "Direct" ? ` [${edge.confidence.toLowerCase()}]` : "";
11379
+ return `[${index + 1}] \u2192 ${edge.targetName} (${edge.callType})${confidence} at line ${edge.line}${edge.isResolved ? ` [resolved: ${edge.toSymbolId}]` : " [unresolved]"}`;
11380
+ }).join("\n");
11381
+ }
11382
+ function formatCallGraphPath(from, to, path27) {
11383
+ if (path27.length === 0) {
11384
+ return `No path found between "${from}" and "${to}". They may be in disconnected components, or the call graph index needs updating.`;
11385
+ }
11386
+ const formatted = path27.map((hop, index) => {
11387
+ const prefix = index === 0 ? "[start]" : `--${hop.callType}-->`;
11388
+ const location = hop.filePath ? ` (${hop.filePath}:${hop.line})` : "";
11389
+ return `${prefix} ${hop.symbolName}${location}`;
11390
+ });
11391
+ return `Path (${path27.length} hops):
11392
+ ${formatted.join("\n")}`;
11393
+ }
10201
11394
  function formatResultHeader(result, index) {
10202
11395
  return result.name ? `[${index + 1}] ${result.chunkType} "${result.name}" in ${result.filePath}:${result.startLine}-${result.endLine}` : `[${index + 1}] ${result.chunkType} in ${result.filePath}:${result.startLine}-${result.endLine}`;
10203
11396
  }
11397
+ function formatBlame(result) {
11398
+ if (!result.blame) {
11399
+ return "";
11400
+ }
11401
+ const date = new Date(result.blame.committedAt * 1e3).toISOString().slice(0, 10);
11402
+ return `
11403
+ ${result.blame.sha.slice(0, 7)} | ${result.blame.author} | ${date} | ${result.blame.summary}`;
11404
+ }
10204
11405
  function formatDefinitionLookup(results, query) {
10205
11406
  if (results.length === 0) {
10206
11407
  return `No definition found for "${query}". Try codebase_search for broader discovery, or verify the symbol name.`;
10207
11408
  }
10208
11409
  const formatted = results.map((r, idx) => {
10209
11410
  const header = formatResultHeader(r, idx);
10210
- return `${header} (score: ${r.score.toFixed(2)})
11411
+ return `${header} (score: ${r.score.toFixed(2)})${formatBlame(r)}
11412
+ \`\`\`
11413
+ ${truncateContent(r.content)}
11414
+ \`\`\``;
11415
+ });
11416
+ return formatted.join("\n\n");
11417
+ }
11418
+ function formatSearchResults(results, scoreFormat = "similarity") {
11419
+ const formatted = results.map((r, idx) => {
11420
+ const header = formatResultHeader(r, idx);
11421
+ const scoreLabel = scoreFormat === "similarity" ? `(similarity: ${(r.score * 100).toFixed(1)}%)` : `(score: ${r.score.toFixed(2)})`;
11422
+ return `${header} ${scoreLabel}${formatBlame(r)}
10211
11423
  \`\`\`
10212
11424
  ${truncateContent(r.content)}
10213
11425
  \`\`\``;
@@ -10267,12 +11479,12 @@ function formatPrImpact(result) {
10267
11479
  }
10268
11480
 
10269
11481
  // src/tools/operations.ts
10270
- import { existsSync as existsSync11, realpathSync, statSync as statSync3 } from "fs";
10271
- import * as path19 from "path";
11482
+ import { existsSync as existsSync12, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
11483
+ import * as path21 from "path";
10272
11484
 
10273
11485
  // src/config/merger.ts
10274
- import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
10275
- import * as path16 from "path";
11486
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
11487
+ import * as path18 from "path";
10276
11488
  var PROJECT_OVERRIDE_KEYS = [
10277
11489
  "embeddingProvider",
10278
11490
  "customProvider",
@@ -10305,8 +11517,8 @@ function mergeUniqueStringArray(values) {
10305
11517
  return [...new Set(values.map((value) => String(value).trim()))];
10306
11518
  }
10307
11519
  function normalizeKnowledgeBasePath(value) {
10308
- let normalized = path16.normalize(String(value).trim());
10309
- const root = path16.parse(normalized).root;
11520
+ let normalized = path18.normalize(String(value).trim());
11521
+ const root = path18.parse(normalized).root;
10310
11522
  while (normalized.length > root.length && /[\\/]$/.test(normalized)) {
10311
11523
  normalized = normalized.slice(0, -1);
10312
11524
  }
@@ -10340,11 +11552,11 @@ function validateConfigLayerShape(rawConfig, filePath) {
10340
11552
  return rawConfig;
10341
11553
  }
10342
11554
  function loadJsonFile(filePath) {
10343
- if (!existsSync9(filePath)) {
11555
+ if (!existsSync10(filePath)) {
10344
11556
  return null;
10345
11557
  }
10346
11558
  try {
10347
- const content = readFileSync9(filePath, "utf-8");
11559
+ const content = readFileSync10(filePath, "utf-8");
10348
11560
  return validateConfigLayerShape(JSON.parse(content), filePath);
10349
11561
  } catch (error) {
10350
11562
  if (error instanceof Error && error.message.startsWith("Config file ")) {
@@ -10359,8 +11571,8 @@ function loadConfigFile(filePath) {
10359
11571
  }
10360
11572
  function materializeLocalProjectConfig(projectRoot, config, host = "opencode") {
10361
11573
  const localConfigPath = resolveWritableProjectConfigPath(projectRoot, host);
10362
- mkdirSync4(path16.dirname(localConfigPath), { recursive: true });
10363
- writeFileSync4(localConfigPath, JSON.stringify(config, null, 2), "utf-8");
11574
+ mkdirSync5(path18.dirname(localConfigPath), { recursive: true });
11575
+ writeFileSync5(localConfigPath, JSON.stringify(config, null, 2), "utf-8");
10364
11576
  return localConfigPath;
10365
11577
  }
10366
11578
  function loadProjectConfigLayer(projectRoot, host = "opencode") {
@@ -10370,7 +11582,7 @@ function loadProjectConfigLayer(projectRoot, host = "opencode") {
10370
11582
  return {};
10371
11583
  }
10372
11584
  const normalizedConfig = { ...projectConfig };
10373
- const projectConfigBaseDir = path16.dirname(path16.dirname(projectConfigPath));
11585
+ const projectConfigBaseDir = path18.dirname(path18.dirname(projectConfigPath));
10374
11586
  if (Array.isArray(normalizedConfig.knowledgeBases)) {
10375
11587
  normalizedConfig.knowledgeBases = resolveInheritedKnowledgeBaseEntries(
10376
11588
  normalizedConfig.knowledgeBases,
@@ -10434,19 +11646,19 @@ function loadMergedConfig(projectRoot, host = "opencode") {
10434
11646
  }
10435
11647
 
10436
11648
  // src/tools/knowledge-base-paths.ts
10437
- import * as path17 from "path";
11649
+ import * as path19 from "path";
10438
11650
  function resolveConfigPathValue(value, baseDir) {
10439
11651
  const trimmed = value.trim();
10440
11652
  if (!trimmed) {
10441
11653
  return trimmed;
10442
11654
  }
10443
- const absolutePath = path17.isAbsolute(trimmed) ? trimmed : path17.resolve(baseDir, trimmed);
10444
- return path17.normalize(absolutePath);
11655
+ const absolutePath = path19.isAbsolute(trimmed) ? trimmed : path19.resolve(baseDir, trimmed);
11656
+ return path19.normalize(absolutePath);
10445
11657
  }
10446
11658
 
10447
11659
  // src/tools/config-state.ts
10448
- import { existsSync as existsSync10, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
10449
- import * as path18 from "path";
11660
+ import { existsSync as existsSync11, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
11661
+ import * as path20 from "path";
10450
11662
  function normalizeKnowledgeBasePaths(config, projectRoot) {
10451
11663
  const normalized = { ...config };
10452
11664
  if (Array.isArray(normalized.knowledgeBases)) {
@@ -10470,6 +11682,24 @@ function loadRuntimeConfig(projectRoot, host = "opencode") {
10470
11682
  var indexerCache = /* @__PURE__ */ new Map();
10471
11683
  var configCache = /* @__PURE__ */ new Map();
10472
11684
  var defaultProjectRoots = /* @__PURE__ */ new Map();
11685
+ function getIndexBusyResult(error) {
11686
+ if (!isIndexLockContentionError(error)) return null;
11687
+ const owner = error.owner;
11688
+ const ownerText = owner ? `PID ${owner.pid}, operation ${owner.operation}, since ${owner.startedAt}` : "unreadable owner";
11689
+ if (error.reason === "legacy-lock") {
11690
+ return {
11691
+ kind: "busy",
11692
+ text: `INDEX_BUSY: legacy lock format detected (${ownerText}). Verify the PID and remove this lock manually only if it is stale.`
11693
+ };
11694
+ }
11695
+ if (error.reason === "unknown-owner") {
11696
+ return {
11697
+ kind: "busy",
11698
+ text: `INDEX_BUSY: unreadable or remote lock owner (${ownerText}). Automatic recovery was refused; manual verification is required.`
11699
+ };
11700
+ }
11701
+ return { kind: "busy", text: `INDEX_BUSY: another index operation is already in progress (${ownerText}).` };
11702
+ }
10473
11703
  function getProjectRoot(projectRoot, host) {
10474
11704
  if (projectRoot) {
10475
11705
  return projectRoot;
@@ -10514,8 +11744,8 @@ function refreshIndexerForDirectory(projectRoot, host = "opencode", config = par
10514
11744
  }
10515
11745
  function shouldForceLocalizeProjectIndex(projectRoot, host = "opencode") {
10516
11746
  const root = getProjectRoot(projectRoot, host);
10517
- const localIndexPath = path19.join(root, getHostProjectIndexRelativePath(host));
10518
- if (existsSync11(localIndexPath)) {
11747
+ const localIndexPath = path21.join(root, getHostProjectIndexRelativePath(host));
11748
+ if (existsSync12(localIndexPath)) {
10519
11749
  return false;
10520
11750
  }
10521
11751
  const inheritedIndexPath = resolveWorktreeFallbackProjectIndexPath(root, host);
@@ -10529,7 +11759,10 @@ async function searchCodebase(projectRoot, host, query, options = {}) {
10529
11759
  chunkType: options.chunkType,
10530
11760
  contextLines: options.contextLines,
10531
11761
  metadataOnly: options.metadataOnly,
10532
- definitionIntent: options.definitionIntent
11762
+ definitionIntent: options.definitionIntent,
11763
+ blameAuthor: options.blameAuthor,
11764
+ blameSha: options.blameSha,
11765
+ blameSince: options.blameSince
10533
11766
  });
10534
11767
  }
10535
11768
  async function findSimilarCode(projectRoot, host, code, options = {}) {
@@ -10568,35 +11801,46 @@ async function getCallGraphPath(projectRoot, host, from, to, maxDepth) {
10568
11801
  async function runIndexCodebase(projectRoot, host, args, onProgress) {
10569
11802
  const root = getProjectRoot(projectRoot, host);
10570
11803
  const key = getIndexerCacheKey(root, host);
10571
- const cachedConfig = configCache.get(key);
10572
11804
  let indexer = getIndexerForProject(root, host);
10573
- if (args.estimateOnly) {
10574
- return { kind: "estimate", estimate: await indexer.estimateCost() };
10575
- }
10576
- if (args.force) {
10577
- if (shouldForceLocalizeProjectIndex(root, host)) {
10578
- materializeLocalProjectConfig(root, loadProjectConfigLayer(root, host), host);
10579
- refreshIndexerForDirectory(root, host, cachedConfig);
10580
- indexer = getIndexerForProject(root, host);
10581
- }
10582
- await indexer.clearIndex();
10583
- refreshIndexerForDirectory(root, host, cachedConfig);
10584
- indexer = getIndexerForProject(root, host);
10585
- }
10586
- const stats = await indexer.index((progress) => {
10587
- if (onProgress) {
10588
- return onProgress(formatProgressTitle(progress), {
10589
- phase: progress.phase,
10590
- filesProcessed: progress.filesProcessed,
10591
- totalFiles: progress.totalFiles,
10592
- chunksProcessed: progress.chunksProcessed,
10593
- totalChunks: progress.totalChunks,
10594
- percentage: calculatePercentage(progress)
11805
+ const runtimeConfig = configCache.get(key);
11806
+ try {
11807
+ if (args.estimateOnly) {
11808
+ return { kind: "estimate", estimate: await indexer.estimateCost() };
11809
+ }
11810
+ const runIndex = async (target) => {
11811
+ const operation = args.force ? target.forceIndex.bind(target) : target.index.bind(target);
11812
+ return operation((progress) => {
11813
+ if (onProgress) {
11814
+ return onProgress(formatProgressTitle(progress), {
11815
+ phase: progress.phase,
11816
+ filesProcessed: progress.filesProcessed,
11817
+ totalFiles: progress.totalFiles,
11818
+ chunksProcessed: progress.chunksProcessed,
11819
+ totalChunks: progress.totalChunks,
11820
+ percentage: calculatePercentage(progress)
11821
+ });
11822
+ }
11823
+ return Promise.resolve();
10595
11824
  });
11825
+ };
11826
+ let stats;
11827
+ if (args.force && shouldForceLocalizeProjectIndex(root, host)) {
11828
+ const inheritedIndexPath = resolveProjectIndexPath(root, runtimeConfig.scope, host);
11829
+ stats = await withIndexLock(inheritedIndexPath, "force-index", async () => {
11830
+ materializeLocalProjectConfig(root, loadProjectConfigLayer(root, host), host);
11831
+ refreshIndexerForDirectory(root, host, runtimeConfig);
11832
+ indexer = getIndexerForProject(root, host);
11833
+ return runIndex(indexer);
11834
+ }, { completeRecoveries: false });
11835
+ } else {
11836
+ stats = await runIndex(indexer);
10596
11837
  }
10597
- return Promise.resolve();
10598
- });
10599
- return { kind: "stats", stats };
11838
+ return { kind: "stats", stats };
11839
+ } catch (error) {
11840
+ const busyResult = getIndexBusyResult(error);
11841
+ if (!busyResult) throw error;
11842
+ return busyResult;
11843
+ }
10600
11844
  }
10601
11845
  async function getIndexStatus(projectRoot, host) {
10602
11846
  const indexer = getIndexerForProject(projectRoot, host);
@@ -10606,6 +11850,15 @@ async function getIndexHealthCheck(projectRoot, host) {
10606
11850
  const indexer = getIndexerForProject(projectRoot, host);
10607
11851
  return indexer.healthCheck();
10608
11852
  }
11853
+ async function runIndexHealthCheck(projectRoot, host) {
11854
+ try {
11855
+ return { kind: "health", health: await getIndexHealthCheck(projectRoot, host) };
11856
+ } catch (error) {
11857
+ const busyResult = getIndexBusyResult(error);
11858
+ if (!busyResult) throw error;
11859
+ return busyResult;
11860
+ }
11861
+ }
10609
11862
  async function getPrImpact(projectRoot, host, params) {
10610
11863
  const indexer = getIndexerForProject(projectRoot, host);
10611
11864
  return indexer.getPrImpact({
@@ -10671,13 +11924,6 @@ async function getIndexLogs(projectRoot, host, args) {
10671
11924
  }
10672
11925
 
10673
11926
  // src/mcp-server/shared.ts
10674
- var MAX_CONTENT_LINES2 = 30;
10675
- function truncateContent2(content) {
10676
- const lines = content.split("\n");
10677
- if (lines.length <= MAX_CONTENT_LINES2) return content;
10678
- return lines.slice(0, MAX_CONTENT_LINES2).join("\n") + `
10679
- // ... (${lines.length - MAX_CONTENT_LINES2} more lines)`;
10680
- }
10681
11927
  var CHUNK_TYPE_ENUM = [
10682
11928
  "function",
10683
11929
  "class",
@@ -10703,7 +11949,10 @@ function registerMcpTools(server, runtime) {
10703
11949
  fileType: z2.string().optional().describe("Filter by file extension (e.g., 'ts', 'py', 'rs')"),
10704
11950
  directory: z2.string().optional().describe("Filter by directory path (e.g., 'src/utils', 'lib')"),
10705
11951
  chunkType: z2.enum(CHUNK_TYPE_ENUM).optional().describe("Filter by code chunk type"),
10706
- contextLines: z2.number().optional().describe("Number of extra lines to include before/after each match (default: 0)")
11952
+ contextLines: z2.number().optional().describe("Number of extra lines to include before/after each match (default: 0)"),
11953
+ blameAuthor: z2.string().optional().describe("Filter by git blame author name or email"),
11954
+ blameSha: z2.string().optional().describe("Filter by git blame commit SHA or prefix"),
11955
+ blameSince: z2.string().optional().describe("Filter to chunks last changed on or after this date")
10707
11956
  },
10708
11957
  async (args) => {
10709
11958
  const results = await searchCodebase(runtime.projectRoot, runtime.host, args.query, {
@@ -10711,21 +11960,17 @@ function registerMcpTools(server, runtime) {
10711
11960
  fileType: args.fileType,
10712
11961
  directory: args.directory,
10713
11962
  chunkType: args.chunkType,
10714
- contextLines: args.contextLines
11963
+ contextLines: args.contextLines,
11964
+ blameAuthor: args.blameAuthor,
11965
+ blameSha: args.blameSha,
11966
+ blameSince: args.blameSince
10715
11967
  });
10716
11968
  if (results.length === 0) {
10717
11969
  return { content: [{ type: "text", text: "No matching code found. Try a different query or run index_codebase first." }] };
10718
11970
  }
10719
- const formatted = results.map((r, idx) => {
10720
- const header = r.name ? `[${idx + 1}] ${r.chunkType} "${r.name}" in ${r.filePath}:${r.startLine}-${r.endLine}` : `[${idx + 1}] ${r.chunkType} in ${r.filePath}:${r.startLine}-${r.endLine}`;
10721
- return `${header} (score: ${r.score.toFixed(2)})
10722
- \`\`\`
10723
- ${truncateContent2(r.content)}
10724
- \`\`\``;
10725
- });
10726
11971
  return { content: [{ type: "text", text: `Found ${results.length} results for "${args.query}":
10727
11972
 
10728
- ${formatted.join("\n\n")}` }] };
11973
+ ${formatSearchResults(results, "score")}` }] };
10729
11974
  }
10730
11975
  );
10731
11976
  server.tool(
@@ -10736,7 +11981,10 @@ ${formatted.join("\n\n")}` }] };
10736
11981
  limit: z2.number().optional().default(10).describe("Maximum number of results to return"),
10737
11982
  fileType: z2.string().optional().describe("Filter by file extension (e.g., 'ts', 'py', 'rs')"),
10738
11983
  directory: z2.string().optional().describe("Filter by directory path (e.g., 'src/utils', 'lib')"),
10739
- chunkType: z2.enum(CHUNK_TYPE_ENUM).optional().describe("Filter by code chunk type")
11984
+ chunkType: z2.enum(CHUNK_TYPE_ENUM).optional().describe("Filter by code chunk type"),
11985
+ blameAuthor: z2.string().optional().describe("Filter by git blame author name or email"),
11986
+ blameSha: z2.string().optional().describe("Filter by git blame commit SHA or prefix"),
11987
+ blameSince: z2.string().optional().describe("Filter to chunks last changed on or after this date")
10740
11988
  },
10741
11989
  async (args) => {
10742
11990
  const results = await searchCodebase(runtime.projectRoot, runtime.host, args.query, {
@@ -10744,19 +11992,17 @@ ${formatted.join("\n\n")}` }] };
10744
11992
  fileType: args.fileType,
10745
11993
  directory: args.directory,
10746
11994
  chunkType: args.chunkType,
10747
- metadataOnly: true
11995
+ metadataOnly: true,
11996
+ blameAuthor: args.blameAuthor,
11997
+ blameSha: args.blameSha,
11998
+ blameSince: args.blameSince
10748
11999
  });
10749
12000
  if (results.length === 0) {
10750
12001
  return { content: [{ type: "text", text: "No matching code found. Try a different query or run index_codebase first." }] };
10751
12002
  }
10752
- const formatted = results.map((r, idx) => {
10753
- const location = `${r.filePath}:${r.startLine}-${r.endLine}`;
10754
- const name = r.name ? `"${r.name}"` : "(anonymous)";
10755
- return `[${idx + 1}] ${r.chunkType} ${name} at ${location} (score: ${r.score.toFixed(2)})`;
10756
- });
10757
12003
  return { content: [{ type: "text", text: `Found ${results.length} locations for "${args.query}":
10758
12004
 
10759
- ${formatted.join("\n")}
12005
+ ${formatCodebasePeek(results)}
10760
12006
 
10761
12007
  Use Read tool to examine specific files.` }] };
10762
12008
  }
@@ -10771,8 +12017,13 @@ Use Read tool to examine specific files.` }] };
10771
12017
  },
10772
12018
  async (args) => {
10773
12019
  const result = await runIndexCodebase(runtime.projectRoot, runtime.host, args);
10774
- const text = result.kind === "estimate" ? formatCostEstimate(result.estimate) : formatIndexStats(result.stats, args.verbose ?? false);
10775
- return { content: [{ type: "text", text }] };
12020
+ if (result.kind === "estimate") {
12021
+ return { content: [{ type: "text", text: formatCostEstimate(result.estimate) }] };
12022
+ }
12023
+ if (result.kind === "busy") {
12024
+ return { content: [{ type: "text", text: result.text }], isError: true };
12025
+ }
12026
+ return { content: [{ type: "text", text: formatIndexStats(result.stats, args.verbose ?? false) }] };
10776
12027
  }
10777
12028
  );
10778
12029
  server.tool(
@@ -10789,8 +12040,11 @@ Use Read tool to examine specific files.` }] };
10789
12040
  "Check index health and remove stale entries from deleted files. Run this to clean up the index after files have been deleted.",
10790
12041
  {},
10791
12042
  async () => {
10792
- const result = await getIndexHealthCheck(runtime.projectRoot, runtime.host);
10793
- return { content: [{ type: "text", text: formatHealthCheck(result) }] };
12043
+ const result = await runIndexHealthCheck(runtime.projectRoot, runtime.host);
12044
+ if (result.kind === "busy") {
12045
+ return { content: [{ type: "text", text: result.text }], isError: true };
12046
+ }
12047
+ return { content: [{ type: "text", text: formatHealthCheck(result.health) }] };
10794
12048
  }
10795
12049
  );
10796
12050
  server.tool(
@@ -10837,16 +12091,9 @@ Use Read tool to examine specific files.` }] };
10837
12091
  if (results.length === 0) {
10838
12092
  return { content: [{ type: "text", text: "No similar code found. Try a different snippet or run index_codebase first." }] };
10839
12093
  }
10840
- const formatted = results.map((r, idx) => {
10841
- const header = r.name ? `[${idx + 1}] ${r.chunkType} "${r.name}" in ${r.filePath}:${r.startLine}-${r.endLine}` : `[${idx + 1}] ${r.chunkType} in ${r.filePath}:${r.startLine}-${r.endLine}`;
10842
- return `${header} (similarity: ${(r.score * 100).toFixed(1)}%)
10843
- \`\`\`
10844
- ${truncateContent2(r.content)}
10845
- \`\`\``;
10846
- });
10847
12094
  return { content: [{ type: "text", text: `Found ${results.length} similar code blocks:
10848
12095
 
10849
- ${formatted.join("\n\n")}` }] };
12096
+ ${formatSearchResults(results)}` }] };
10850
12097
  }
10851
12098
  );
10852
12099
  server.tool(
@@ -10882,28 +12129,10 @@ ${formatted.join("\n\n")}` }] };
10882
12129
  return { content: [{ type: "text", text: "Error: 'symbolId' is required when direction is 'callees'." }] };
10883
12130
  }
10884
12131
  const { callees } = await getCallGraphData(runtime.projectRoot, runtime.host, args);
10885
- if (callees.length === 0) {
10886
- return { content: [{ type: "text", text: `No callees found for symbol ${args.symbolId}${args.relationshipType ? ` with type ${args.relationshipType}` : ""}.` }] };
10887
- }
10888
- const formatted2 = callees.map((e, i) => {
10889
- const conf = e.confidence !== "Direct" ? ` [${e.confidence.toLowerCase()}]` : "";
10890
- return `[${i + 1}] \u2192 ${e.targetName} (${e.callType})${conf} at line ${e.line}${e.isResolved ? ` [resolved: ${e.toSymbolId}]` : " [unresolved]"}`;
10891
- });
10892
- return { content: [{ type: "text", text: `Callees (${callees.length}):
10893
-
10894
- ${formatted2.join("\n")}` }] };
12132
+ return { content: [{ type: "text", text: formatCallGraphCallees(args.symbolId, callees, args.relationshipType) }] };
10895
12133
  }
10896
12134
  const { callers } = await getCallGraphData(runtime.projectRoot, runtime.host, args);
10897
- if (callers.length === 0) {
10898
- return { content: [{ type: "text", text: `No callers found for "${args.name}"${args.relationshipType ? ` with type ${args.relationshipType}` : ""}.` }] };
10899
- }
10900
- const formatted = callers.map((e, i) => {
10901
- const conf = e.confidence !== "Direct" ? ` [${e.confidence.toLowerCase()}]` : "";
10902
- return `[${i + 1}] \u2190 from ${e.fromSymbolName ?? "<unknown>"} in ${e.fromSymbolFilePath ?? "<unknown file>"} [${e.fromSymbolId}] (${e.callType})${conf} at line ${e.line}${e.isResolved ? " [resolved]" : " [unresolved]"}`;
10903
- });
10904
- return { content: [{ type: "text", text: `"${args.name}" is called by ${callers.length} function(s):
10905
-
10906
- ${formatted.join("\n")}` }] };
12135
+ return { content: [{ type: "text", text: formatCallGraphCallers(args.name, callers, args.relationshipType) }] };
10907
12136
  }
10908
12137
  );
10909
12138
  server.tool(
@@ -10915,17 +12144,8 @@ ${formatted.join("\n")}` }] };
10915
12144
  maxDepth: z2.number().optional().default(10).describe("Maximum traversal depth (default: 10)")
10916
12145
  },
10917
12146
  async (args) => {
10918
- const path25 = await getCallGraphPath(runtime.projectRoot, runtime.host, args.from, args.to, args.maxDepth);
10919
- if (path25.length === 0) {
10920
- return { content: [{ type: "text", text: `No path found between "${args.from}" and "${args.to}". They may be in disconnected components, or the call graph index needs updating.` }] };
10921
- }
10922
- const formatted = path25.map((hop, i) => {
10923
- const prefix = i === 0 ? "[start]" : `--${hop.callType}-->`;
10924
- const location = hop.filePath ? ` (${hop.filePath}:${hop.line})` : "";
10925
- return `${prefix} ${hop.symbolName}${location}`;
10926
- });
10927
- return { content: [{ type: "text", text: `Path (${path25.length} hops):
10928
- ${formatted.join("\n")}` }] };
12147
+ const path27 = await getCallGraphPath(runtime.projectRoot, runtime.host, args.from, args.to, args.maxDepth);
12148
+ return { content: [{ type: "text", text: formatCallGraphPath(args.from, args.to, path27) }] };
10929
12149
  }
10930
12150
  );
10931
12151
  server.tool(
@@ -10960,7 +12180,7 @@ ${formatted.join("\n")}` }] };
10960
12180
 
10961
12181
  // src/mcp-server.ts
10962
12182
  function getPackageVersion() {
10963
- const raw = JSON.parse(readFileSync10(new URL("../package.json", import.meta.url), "utf-8"));
12183
+ const raw = JSON.parse(readFileSync11(new URL("../package.json", import.meta.url), "utf-8"));
10964
12184
  if (raw && typeof raw === "object" && "version" in raw && typeof raw.version === "string") {
10965
12185
  return raw.version;
10966
12186
  }
@@ -10981,18 +12201,37 @@ function createMcpServer(projectRoot, config, host = "opencode") {
10981
12201
  }
10982
12202
 
10983
12203
  // src/utils/auto-index.ts
12204
+ var INITIAL_RETRY_DELAY_MS = 50;
12205
+ var MAX_RETRY_DELAY_MS = 500;
12206
+ var autoIndexStates = /* @__PURE__ */ new WeakMap();
10984
12207
  function getErrorMessage3(error) {
10985
12208
  return error instanceof Error ? error.message : String(error);
10986
12209
  }
10987
- function startAutoIndex(indexer, projectRoot) {
10988
- indexer.initialize().then(() => {
10989
- indexer.index().catch((error) => {
10990
- console.error(`[codebase-index] Auto-index failed for "${projectRoot}": ${getErrorMessage3(error)}`);
10991
- });
12210
+ function runAutoIndex(indexer, projectRoot, state) {
12211
+ void indexer.index().then(() => {
12212
+ autoIndexStates.delete(indexer);
10992
12213
  }).catch((error) => {
10993
- console.error(`[codebase-index] Auto-index initialization failed for "${projectRoot}": ${getErrorMessage3(error)}`);
12214
+ if (!isTransientIndexLockContention(error)) {
12215
+ autoIndexStates.delete(indexer);
12216
+ console.error(`[codebase-index] Auto-index failed for "${projectRoot}": ${getErrorMessage3(error)}`);
12217
+ return;
12218
+ }
12219
+ const retryDelayMs = state.retryDelayMs;
12220
+ state.retryDelayMs = Math.min(retryDelayMs * 2, MAX_RETRY_DELAY_MS);
12221
+ const retryTimer = setTimeout(() => {
12222
+ runAutoIndex(indexer, projectRoot, state);
12223
+ }, retryDelayMs);
12224
+ retryTimer.unref?.();
10994
12225
  });
10995
12226
  }
12227
+ function startAutoIndex(indexer, projectRoot) {
12228
+ if (autoIndexStates.has(indexer)) return;
12229
+ const state = {
12230
+ retryDelayMs: INITIAL_RETRY_DELAY_MS
12231
+ };
12232
+ autoIndexStates.set(indexer, state);
12233
+ runAutoIndex(indexer, projectRoot, state);
12234
+ }
10996
12235
 
10997
12236
  // node_modules/chokidar/index.js
10998
12237
  import { EventEmitter as EventEmitter2 } from "events";
@@ -11084,7 +12323,7 @@ var ReaddirpStream = class extends Readable {
11084
12323
  this._directoryFilter = normalizeFilter(opts.directoryFilter);
11085
12324
  const statMethod = opts.lstat ? lstat : stat;
11086
12325
  if (wantBigintFsStats) {
11087
- this._stat = (path25) => statMethod(path25, { bigint: true });
12326
+ this._stat = (path27) => statMethod(path27, { bigint: true });
11088
12327
  } else {
11089
12328
  this._stat = statMethod;
11090
12329
  }
@@ -11109,8 +12348,8 @@ var ReaddirpStream = class extends Readable {
11109
12348
  const par = this.parent;
11110
12349
  const fil = par && par.files;
11111
12350
  if (fil && fil.length > 0) {
11112
- const { path: path25, depth } = par;
11113
- const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path25));
12351
+ const { path: path27, depth } = par;
12352
+ const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path27));
11114
12353
  const awaited = await Promise.all(slice);
11115
12354
  for (const entry of awaited) {
11116
12355
  if (!entry)
@@ -11150,20 +12389,20 @@ var ReaddirpStream = class extends Readable {
11150
12389
  this.reading = false;
11151
12390
  }
11152
12391
  }
11153
- async _exploreDir(path25, depth) {
12392
+ async _exploreDir(path27, depth) {
11154
12393
  let files;
11155
12394
  try {
11156
- files = await readdir(path25, this._rdOptions);
12395
+ files = await readdir(path27, this._rdOptions);
11157
12396
  } catch (error) {
11158
12397
  this._onError(error);
11159
12398
  }
11160
- return { files, depth, path: path25 };
12399
+ return { files, depth, path: path27 };
11161
12400
  }
11162
- async _formatEntry(dirent, path25) {
12401
+ async _formatEntry(dirent, path27) {
11163
12402
  let entry;
11164
12403
  const basename5 = this._isDirent ? dirent.name : dirent;
11165
12404
  try {
11166
- const fullPath = presolve(pjoin(path25, basename5));
12405
+ const fullPath = presolve(pjoin(path27, basename5));
11167
12406
  entry = { path: prelative(this._root, fullPath), fullPath, basename: basename5 };
11168
12407
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
11169
12408
  } catch (err) {
@@ -11563,16 +12802,16 @@ var delFromSet = (main2, prop, item) => {
11563
12802
  };
11564
12803
  var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
11565
12804
  var FsWatchInstances = /* @__PURE__ */ new Map();
11566
- function createFsWatchInstance(path25, options, listener, errHandler, emitRaw) {
12805
+ function createFsWatchInstance(path27, options, listener, errHandler, emitRaw) {
11567
12806
  const handleEvent = (rawEvent, evPath) => {
11568
- listener(path25);
11569
- emitRaw(rawEvent, evPath, { watchedPath: path25 });
11570
- if (evPath && path25 !== evPath) {
11571
- fsWatchBroadcast(sp.resolve(path25, evPath), KEY_LISTENERS, sp.join(path25, evPath));
12807
+ listener(path27);
12808
+ emitRaw(rawEvent, evPath, { watchedPath: path27 });
12809
+ if (evPath && path27 !== evPath) {
12810
+ fsWatchBroadcast(sp.resolve(path27, evPath), KEY_LISTENERS, sp.join(path27, evPath));
11572
12811
  }
11573
12812
  };
11574
12813
  try {
11575
- return fs_watch(path25, {
12814
+ return fs_watch(path27, {
11576
12815
  persistent: options.persistent
11577
12816
  }, handleEvent);
11578
12817
  } catch (error) {
@@ -11588,12 +12827,12 @@ var fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
11588
12827
  listener(val1, val2, val3);
11589
12828
  });
11590
12829
  };
11591
- var setFsWatchListener = (path25, fullPath, options, handlers) => {
12830
+ var setFsWatchListener = (path27, fullPath, options, handlers) => {
11592
12831
  const { listener, errHandler, rawEmitter } = handlers;
11593
12832
  let cont = FsWatchInstances.get(fullPath);
11594
12833
  let watcher;
11595
12834
  if (!options.persistent) {
11596
- watcher = createFsWatchInstance(path25, options, listener, errHandler, rawEmitter);
12835
+ watcher = createFsWatchInstance(path27, options, listener, errHandler, rawEmitter);
11597
12836
  if (!watcher)
11598
12837
  return;
11599
12838
  return watcher.close.bind(watcher);
@@ -11604,7 +12843,7 @@ var setFsWatchListener = (path25, fullPath, options, handlers) => {
11604
12843
  addAndConvert(cont, KEY_RAW, rawEmitter);
11605
12844
  } else {
11606
12845
  watcher = createFsWatchInstance(
11607
- path25,
12846
+ path27,
11608
12847
  options,
11609
12848
  fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS),
11610
12849
  errHandler,
@@ -11619,7 +12858,7 @@ var setFsWatchListener = (path25, fullPath, options, handlers) => {
11619
12858
  cont.watcherUnusable = true;
11620
12859
  if (isWindows && error.code === "EPERM") {
11621
12860
  try {
11622
- const fd = await open(path25, "r");
12861
+ const fd = await open(path27, "r");
11623
12862
  await fd.close();
11624
12863
  broadcastErr(error);
11625
12864
  } catch (err) {
@@ -11650,7 +12889,7 @@ var setFsWatchListener = (path25, fullPath, options, handlers) => {
11650
12889
  };
11651
12890
  };
11652
12891
  var FsWatchFileInstances = /* @__PURE__ */ new Map();
11653
- var setFsWatchFileListener = (path25, fullPath, options, handlers) => {
12892
+ var setFsWatchFileListener = (path27, fullPath, options, handlers) => {
11654
12893
  const { listener, rawEmitter } = handlers;
11655
12894
  let cont = FsWatchFileInstances.get(fullPath);
11656
12895
  const copts = cont && cont.options;
@@ -11672,7 +12911,7 @@ var setFsWatchFileListener = (path25, fullPath, options, handlers) => {
11672
12911
  });
11673
12912
  const currmtime = curr.mtimeMs;
11674
12913
  if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
11675
- foreach(cont.listeners, (listener2) => listener2(path25, curr));
12914
+ foreach(cont.listeners, (listener2) => listener2(path27, curr));
11676
12915
  }
11677
12916
  })
11678
12917
  };
@@ -11702,13 +12941,13 @@ var NodeFsHandler = class {
11702
12941
  * @param listener on fs change
11703
12942
  * @returns closer for the watcher instance
11704
12943
  */
11705
- _watchWithNodeFs(path25, listener) {
12944
+ _watchWithNodeFs(path27, listener) {
11706
12945
  const opts = this.fsw.options;
11707
- const directory = sp.dirname(path25);
11708
- const basename5 = sp.basename(path25);
12946
+ const directory = sp.dirname(path27);
12947
+ const basename5 = sp.basename(path27);
11709
12948
  const parent = this.fsw._getWatchedDir(directory);
11710
12949
  parent.add(basename5);
11711
- const absolutePath = sp.resolve(path25);
12950
+ const absolutePath = sp.resolve(path27);
11712
12951
  const options = {
11713
12952
  persistent: opts.persistent
11714
12953
  };
@@ -11718,12 +12957,12 @@ var NodeFsHandler = class {
11718
12957
  if (opts.usePolling) {
11719
12958
  const enableBin = opts.interval !== opts.binaryInterval;
11720
12959
  options.interval = enableBin && isBinaryPath(basename5) ? opts.binaryInterval : opts.interval;
11721
- closer = setFsWatchFileListener(path25, absolutePath, options, {
12960
+ closer = setFsWatchFileListener(path27, absolutePath, options, {
11722
12961
  listener,
11723
12962
  rawEmitter: this.fsw._emitRaw
11724
12963
  });
11725
12964
  } else {
11726
- closer = setFsWatchListener(path25, absolutePath, options, {
12965
+ closer = setFsWatchListener(path27, absolutePath, options, {
11727
12966
  listener,
11728
12967
  errHandler: this._boundHandleError,
11729
12968
  rawEmitter: this.fsw._emitRaw
@@ -11745,7 +12984,7 @@ var NodeFsHandler = class {
11745
12984
  let prevStats = stats;
11746
12985
  if (parent.has(basename5))
11747
12986
  return;
11748
- const listener = async (path25, newStats) => {
12987
+ const listener = async (path27, newStats) => {
11749
12988
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
11750
12989
  return;
11751
12990
  if (!newStats || newStats.mtimeMs === 0) {
@@ -11759,11 +12998,11 @@ var NodeFsHandler = class {
11759
12998
  this.fsw._emit(EV.CHANGE, file, newStats2);
11760
12999
  }
11761
13000
  if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats2.ino) {
11762
- this.fsw._closeFile(path25);
13001
+ this.fsw._closeFile(path27);
11763
13002
  prevStats = newStats2;
11764
13003
  const closer2 = this._watchWithNodeFs(file, listener);
11765
13004
  if (closer2)
11766
- this.fsw._addPathCloser(path25, closer2);
13005
+ this.fsw._addPathCloser(path27, closer2);
11767
13006
  } else {
11768
13007
  prevStats = newStats2;
11769
13008
  }
@@ -11795,7 +13034,7 @@ var NodeFsHandler = class {
11795
13034
  * @param item basename of this item
11796
13035
  * @returns true if no more processing is needed for this entry.
11797
13036
  */
11798
- async _handleSymlink(entry, directory, path25, item) {
13037
+ async _handleSymlink(entry, directory, path27, item) {
11799
13038
  if (this.fsw.closed) {
11800
13039
  return;
11801
13040
  }
@@ -11805,7 +13044,7 @@ var NodeFsHandler = class {
11805
13044
  this.fsw._incrReadyCount();
11806
13045
  let linkPath;
11807
13046
  try {
11808
- linkPath = await fsrealpath(path25);
13047
+ linkPath = await fsrealpath(path27);
11809
13048
  } catch (e) {
11810
13049
  this.fsw._emitReady();
11811
13050
  return true;
@@ -11815,12 +13054,12 @@ var NodeFsHandler = class {
11815
13054
  if (dir.has(item)) {
11816
13055
  if (this.fsw._symlinkPaths.get(full) !== linkPath) {
11817
13056
  this.fsw._symlinkPaths.set(full, linkPath);
11818
- this.fsw._emit(EV.CHANGE, path25, entry.stats);
13057
+ this.fsw._emit(EV.CHANGE, path27, entry.stats);
11819
13058
  }
11820
13059
  } else {
11821
13060
  dir.add(item);
11822
13061
  this.fsw._symlinkPaths.set(full, linkPath);
11823
- this.fsw._emit(EV.ADD, path25, entry.stats);
13062
+ this.fsw._emit(EV.ADD, path27, entry.stats);
11824
13063
  }
11825
13064
  this.fsw._emitReady();
11826
13065
  return true;
@@ -11850,9 +13089,9 @@ var NodeFsHandler = class {
11850
13089
  return;
11851
13090
  }
11852
13091
  const item = entry.path;
11853
- let path25 = sp.join(directory, item);
13092
+ let path27 = sp.join(directory, item);
11854
13093
  current.add(item);
11855
- if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path25, item)) {
13094
+ if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path27, item)) {
11856
13095
  return;
11857
13096
  }
11858
13097
  if (this.fsw.closed) {
@@ -11861,8 +13100,8 @@ var NodeFsHandler = class {
11861
13100
  }
11862
13101
  if (item === target || !target && !previous.has(item)) {
11863
13102
  this.fsw._incrReadyCount();
11864
- path25 = sp.join(dir, sp.relative(dir, path25));
11865
- this._addToNodeFs(path25, initialAdd, wh, depth + 1);
13103
+ path27 = sp.join(dir, sp.relative(dir, path27));
13104
+ this._addToNodeFs(path27, initialAdd, wh, depth + 1);
11866
13105
  }
11867
13106
  }).on(EV.ERROR, this._boundHandleError);
11868
13107
  return new Promise((resolve15, reject) => {
@@ -11931,13 +13170,13 @@ var NodeFsHandler = class {
11931
13170
  * @param depth Child path actually targeted for watch
11932
13171
  * @param target Child path actually targeted for watch
11933
13172
  */
11934
- async _addToNodeFs(path25, initialAdd, priorWh, depth, target) {
13173
+ async _addToNodeFs(path27, initialAdd, priorWh, depth, target) {
11935
13174
  const ready = this.fsw._emitReady;
11936
- if (this.fsw._isIgnored(path25) || this.fsw.closed) {
13175
+ if (this.fsw._isIgnored(path27) || this.fsw.closed) {
11937
13176
  ready();
11938
13177
  return false;
11939
13178
  }
11940
- const wh = this.fsw._getWatchHelpers(path25);
13179
+ const wh = this.fsw._getWatchHelpers(path27);
11941
13180
  if (priorWh) {
11942
13181
  wh.filterPath = (entry) => priorWh.filterPath(entry);
11943
13182
  wh.filterDir = (entry) => priorWh.filterDir(entry);
@@ -11953,8 +13192,8 @@ var NodeFsHandler = class {
11953
13192
  const follow = this.fsw.options.followSymlinks;
11954
13193
  let closer;
11955
13194
  if (stats.isDirectory()) {
11956
- const absPath = sp.resolve(path25);
11957
- const targetPath = follow ? await fsrealpath(path25) : path25;
13195
+ const absPath = sp.resolve(path27);
13196
+ const targetPath = follow ? await fsrealpath(path27) : path27;
11958
13197
  if (this.fsw.closed)
11959
13198
  return;
11960
13199
  closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
@@ -11964,29 +13203,29 @@ var NodeFsHandler = class {
11964
13203
  this.fsw._symlinkPaths.set(absPath, targetPath);
11965
13204
  }
11966
13205
  } else if (stats.isSymbolicLink()) {
11967
- const targetPath = follow ? await fsrealpath(path25) : path25;
13206
+ const targetPath = follow ? await fsrealpath(path27) : path27;
11968
13207
  if (this.fsw.closed)
11969
13208
  return;
11970
13209
  const parent = sp.dirname(wh.watchPath);
11971
13210
  this.fsw._getWatchedDir(parent).add(wh.watchPath);
11972
13211
  this.fsw._emit(EV.ADD, wh.watchPath, stats);
11973
- closer = await this._handleDir(parent, stats, initialAdd, depth, path25, wh, targetPath);
13212
+ closer = await this._handleDir(parent, stats, initialAdd, depth, path27, wh, targetPath);
11974
13213
  if (this.fsw.closed)
11975
13214
  return;
11976
13215
  if (targetPath !== void 0) {
11977
- this.fsw._symlinkPaths.set(sp.resolve(path25), targetPath);
13216
+ this.fsw._symlinkPaths.set(sp.resolve(path27), targetPath);
11978
13217
  }
11979
13218
  } else {
11980
13219
  closer = this._handleFile(wh.watchPath, stats, initialAdd);
11981
13220
  }
11982
13221
  ready();
11983
13222
  if (closer)
11984
- this.fsw._addPathCloser(path25, closer);
13223
+ this.fsw._addPathCloser(path27, closer);
11985
13224
  return false;
11986
13225
  } catch (error) {
11987
13226
  if (this.fsw._handleError(error)) {
11988
13227
  ready();
11989
- return path25;
13228
+ return path27;
11990
13229
  }
11991
13230
  }
11992
13231
  }
@@ -12018,35 +13257,35 @@ function createPattern(matcher) {
12018
13257
  if (matcher.path === string)
12019
13258
  return true;
12020
13259
  if (matcher.recursive) {
12021
- const relative10 = sp2.relative(matcher.path, string);
12022
- if (!relative10) {
13260
+ const relative11 = sp2.relative(matcher.path, string);
13261
+ if (!relative11) {
12023
13262
  return false;
12024
13263
  }
12025
- return !relative10.startsWith("..") && !sp2.isAbsolute(relative10);
13264
+ return !relative11.startsWith("..") && !sp2.isAbsolute(relative11);
12026
13265
  }
12027
13266
  return false;
12028
13267
  };
12029
13268
  }
12030
13269
  return () => false;
12031
13270
  }
12032
- function normalizePath2(path25) {
12033
- if (typeof path25 !== "string")
13271
+ function normalizePath2(path27) {
13272
+ if (typeof path27 !== "string")
12034
13273
  throw new Error("string expected");
12035
- path25 = sp2.normalize(path25);
12036
- path25 = path25.replace(/\\/g, "/");
13274
+ path27 = sp2.normalize(path27);
13275
+ path27 = path27.replace(/\\/g, "/");
12037
13276
  let prepend = false;
12038
- if (path25.startsWith("//"))
13277
+ if (path27.startsWith("//"))
12039
13278
  prepend = true;
12040
- path25 = path25.replace(DOUBLE_SLASH_RE, "/");
13279
+ path27 = path27.replace(DOUBLE_SLASH_RE, "/");
12041
13280
  if (prepend)
12042
- path25 = "/" + path25;
12043
- return path25;
13281
+ path27 = "/" + path27;
13282
+ return path27;
12044
13283
  }
12045
13284
  function matchPatterns(patterns, testString, stats) {
12046
- const path25 = normalizePath2(testString);
13285
+ const path27 = normalizePath2(testString);
12047
13286
  for (let index = 0; index < patterns.length; index++) {
12048
13287
  const pattern = patterns[index];
12049
- if (pattern(path25, stats)) {
13288
+ if (pattern(path27, stats)) {
12050
13289
  return true;
12051
13290
  }
12052
13291
  }
@@ -12084,19 +13323,19 @@ var toUnix = (string) => {
12084
13323
  }
12085
13324
  return str;
12086
13325
  };
12087
- var normalizePathToUnix = (path25) => toUnix(sp2.normalize(toUnix(path25)));
12088
- var normalizeIgnored = (cwd = "") => (path25) => {
12089
- if (typeof path25 === "string") {
12090
- return normalizePathToUnix(sp2.isAbsolute(path25) ? path25 : sp2.join(cwd, path25));
13326
+ var normalizePathToUnix = (path27) => toUnix(sp2.normalize(toUnix(path27)));
13327
+ var normalizeIgnored = (cwd = "") => (path27) => {
13328
+ if (typeof path27 === "string") {
13329
+ return normalizePathToUnix(sp2.isAbsolute(path27) ? path27 : sp2.join(cwd, path27));
12091
13330
  } else {
12092
- return path25;
13331
+ return path27;
12093
13332
  }
12094
13333
  };
12095
- var getAbsolutePath = (path25, cwd) => {
12096
- if (sp2.isAbsolute(path25)) {
12097
- return path25;
13334
+ var getAbsolutePath = (path27, cwd) => {
13335
+ if (sp2.isAbsolute(path27)) {
13336
+ return path27;
12098
13337
  }
12099
- return sp2.join(cwd, path25);
13338
+ return sp2.join(cwd, path27);
12100
13339
  };
12101
13340
  var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
12102
13341
  var DirEntry = class {
@@ -12161,10 +13400,10 @@ var WatchHelper = class {
12161
13400
  dirParts;
12162
13401
  followSymlinks;
12163
13402
  statMethod;
12164
- constructor(path25, follow, fsw) {
13403
+ constructor(path27, follow, fsw) {
12165
13404
  this.fsw = fsw;
12166
- const watchPath = path25;
12167
- this.path = path25 = path25.replace(REPLACER_RE, "");
13405
+ const watchPath = path27;
13406
+ this.path = path27 = path27.replace(REPLACER_RE, "");
12168
13407
  this.watchPath = watchPath;
12169
13408
  this.fullWatchPath = sp2.resolve(watchPath);
12170
13409
  this.dirParts = [];
@@ -12304,20 +13543,20 @@ var FSWatcher = class extends EventEmitter2 {
12304
13543
  this._closePromise = void 0;
12305
13544
  let paths = unifyPaths(paths_);
12306
13545
  if (cwd) {
12307
- paths = paths.map((path25) => {
12308
- const absPath = getAbsolutePath(path25, cwd);
13546
+ paths = paths.map((path27) => {
13547
+ const absPath = getAbsolutePath(path27, cwd);
12309
13548
  return absPath;
12310
13549
  });
12311
13550
  }
12312
- paths.forEach((path25) => {
12313
- this._removeIgnoredPath(path25);
13551
+ paths.forEach((path27) => {
13552
+ this._removeIgnoredPath(path27);
12314
13553
  });
12315
13554
  this._userIgnored = void 0;
12316
13555
  if (!this._readyCount)
12317
13556
  this._readyCount = 0;
12318
13557
  this._readyCount += paths.length;
12319
- Promise.all(paths.map(async (path25) => {
12320
- const res = await this._nodeFsHandler._addToNodeFs(path25, !_internal, void 0, 0, _origAdd);
13558
+ Promise.all(paths.map(async (path27) => {
13559
+ const res = await this._nodeFsHandler._addToNodeFs(path27, !_internal, void 0, 0, _origAdd);
12321
13560
  if (res)
12322
13561
  this._emitReady();
12323
13562
  return res;
@@ -12339,17 +13578,17 @@ var FSWatcher = class extends EventEmitter2 {
12339
13578
  return this;
12340
13579
  const paths = unifyPaths(paths_);
12341
13580
  const { cwd } = this.options;
12342
- paths.forEach((path25) => {
12343
- if (!sp2.isAbsolute(path25) && !this._closers.has(path25)) {
13581
+ paths.forEach((path27) => {
13582
+ if (!sp2.isAbsolute(path27) && !this._closers.has(path27)) {
12344
13583
  if (cwd)
12345
- path25 = sp2.join(cwd, path25);
12346
- path25 = sp2.resolve(path25);
13584
+ path27 = sp2.join(cwd, path27);
13585
+ path27 = sp2.resolve(path27);
12347
13586
  }
12348
- this._closePath(path25);
12349
- this._addIgnoredPath(path25);
12350
- if (this._watched.has(path25)) {
13587
+ this._closePath(path27);
13588
+ this._addIgnoredPath(path27);
13589
+ if (this._watched.has(path27)) {
12351
13590
  this._addIgnoredPath({
12352
- path: path25,
13591
+ path: path27,
12353
13592
  recursive: true
12354
13593
  });
12355
13594
  }
@@ -12413,38 +13652,38 @@ var FSWatcher = class extends EventEmitter2 {
12413
13652
  * @param stats arguments to be passed with event
12414
13653
  * @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
12415
13654
  */
12416
- async _emit(event, path25, stats) {
13655
+ async _emit(event, path27, stats) {
12417
13656
  if (this.closed)
12418
13657
  return;
12419
13658
  const opts = this.options;
12420
13659
  if (isWindows)
12421
- path25 = sp2.normalize(path25);
13660
+ path27 = sp2.normalize(path27);
12422
13661
  if (opts.cwd)
12423
- path25 = sp2.relative(opts.cwd, path25);
12424
- const args = [path25];
13662
+ path27 = sp2.relative(opts.cwd, path27);
13663
+ const args = [path27];
12425
13664
  if (stats != null)
12426
13665
  args.push(stats);
12427
13666
  const awf = opts.awaitWriteFinish;
12428
13667
  let pw;
12429
- if (awf && (pw = this._pendingWrites.get(path25))) {
13668
+ if (awf && (pw = this._pendingWrites.get(path27))) {
12430
13669
  pw.lastChange = /* @__PURE__ */ new Date();
12431
13670
  return this;
12432
13671
  }
12433
13672
  if (opts.atomic) {
12434
13673
  if (event === EVENTS.UNLINK) {
12435
- this._pendingUnlinks.set(path25, [event, ...args]);
13674
+ this._pendingUnlinks.set(path27, [event, ...args]);
12436
13675
  setTimeout(() => {
12437
- this._pendingUnlinks.forEach((entry, path26) => {
13676
+ this._pendingUnlinks.forEach((entry, path28) => {
12438
13677
  this.emit(...entry);
12439
13678
  this.emit(EVENTS.ALL, ...entry);
12440
- this._pendingUnlinks.delete(path26);
13679
+ this._pendingUnlinks.delete(path28);
12441
13680
  });
12442
13681
  }, typeof opts.atomic === "number" ? opts.atomic : 100);
12443
13682
  return this;
12444
13683
  }
12445
- if (event === EVENTS.ADD && this._pendingUnlinks.has(path25)) {
13684
+ if (event === EVENTS.ADD && this._pendingUnlinks.has(path27)) {
12446
13685
  event = EVENTS.CHANGE;
12447
- this._pendingUnlinks.delete(path25);
13686
+ this._pendingUnlinks.delete(path27);
12448
13687
  }
12449
13688
  }
12450
13689
  if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) {
@@ -12462,16 +13701,16 @@ var FSWatcher = class extends EventEmitter2 {
12462
13701
  this.emitWithAll(event, args);
12463
13702
  }
12464
13703
  };
12465
- this._awaitWriteFinish(path25, awf.stabilityThreshold, event, awfEmit);
13704
+ this._awaitWriteFinish(path27, awf.stabilityThreshold, event, awfEmit);
12466
13705
  return this;
12467
13706
  }
12468
13707
  if (event === EVENTS.CHANGE) {
12469
- const isThrottled = !this._throttle(EVENTS.CHANGE, path25, 50);
13708
+ const isThrottled = !this._throttle(EVENTS.CHANGE, path27, 50);
12470
13709
  if (isThrottled)
12471
13710
  return this;
12472
13711
  }
12473
13712
  if (opts.alwaysStat && stats === void 0 && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) {
12474
- const fullPath = opts.cwd ? sp2.join(opts.cwd, path25) : path25;
13713
+ const fullPath = opts.cwd ? sp2.join(opts.cwd, path27) : path27;
12475
13714
  let stats2;
12476
13715
  try {
12477
13716
  stats2 = await stat3(fullPath);
@@ -12502,23 +13741,23 @@ var FSWatcher = class extends EventEmitter2 {
12502
13741
  * @param timeout duration of time to suppress duplicate actions
12503
13742
  * @returns tracking object or false if action should be suppressed
12504
13743
  */
12505
- _throttle(actionType, path25, timeout) {
13744
+ _throttle(actionType, path27, timeout) {
12506
13745
  if (!this._throttled.has(actionType)) {
12507
13746
  this._throttled.set(actionType, /* @__PURE__ */ new Map());
12508
13747
  }
12509
13748
  const action = this._throttled.get(actionType);
12510
13749
  if (!action)
12511
13750
  throw new Error("invalid throttle");
12512
- const actionPath = action.get(path25);
13751
+ const actionPath = action.get(path27);
12513
13752
  if (actionPath) {
12514
13753
  actionPath.count++;
12515
13754
  return false;
12516
13755
  }
12517
13756
  let timeoutObject;
12518
13757
  const clear = () => {
12519
- const item = action.get(path25);
13758
+ const item = action.get(path27);
12520
13759
  const count = item ? item.count : 0;
12521
- action.delete(path25);
13760
+ action.delete(path27);
12522
13761
  clearTimeout(timeoutObject);
12523
13762
  if (item)
12524
13763
  clearTimeout(item.timeoutObject);
@@ -12526,7 +13765,7 @@ var FSWatcher = class extends EventEmitter2 {
12526
13765
  };
12527
13766
  timeoutObject = setTimeout(clear, timeout);
12528
13767
  const thr = { timeoutObject, clear, count: 0 };
12529
- action.set(path25, thr);
13768
+ action.set(path27, thr);
12530
13769
  return thr;
12531
13770
  }
12532
13771
  _incrReadyCount() {
@@ -12540,44 +13779,44 @@ var FSWatcher = class extends EventEmitter2 {
12540
13779
  * @param event
12541
13780
  * @param awfEmit Callback to be called when ready for event to be emitted.
12542
13781
  */
12543
- _awaitWriteFinish(path25, threshold, event, awfEmit) {
13782
+ _awaitWriteFinish(path27, threshold, event, awfEmit) {
12544
13783
  const awf = this.options.awaitWriteFinish;
12545
13784
  if (typeof awf !== "object")
12546
13785
  return;
12547
13786
  const pollInterval = awf.pollInterval;
12548
13787
  let timeoutHandler;
12549
- let fullPath = path25;
12550
- if (this.options.cwd && !sp2.isAbsolute(path25)) {
12551
- fullPath = sp2.join(this.options.cwd, path25);
13788
+ let fullPath = path27;
13789
+ if (this.options.cwd && !sp2.isAbsolute(path27)) {
13790
+ fullPath = sp2.join(this.options.cwd, path27);
12552
13791
  }
12553
13792
  const now = /* @__PURE__ */ new Date();
12554
13793
  const writes = this._pendingWrites;
12555
13794
  function awaitWriteFinishFn(prevStat) {
12556
13795
  statcb(fullPath, (err, curStat) => {
12557
- if (err || !writes.has(path25)) {
13796
+ if (err || !writes.has(path27)) {
12558
13797
  if (err && err.code !== "ENOENT")
12559
13798
  awfEmit(err);
12560
13799
  return;
12561
13800
  }
12562
13801
  const now2 = Number(/* @__PURE__ */ new Date());
12563
13802
  if (prevStat && curStat.size !== prevStat.size) {
12564
- writes.get(path25).lastChange = now2;
13803
+ writes.get(path27).lastChange = now2;
12565
13804
  }
12566
- const pw = writes.get(path25);
13805
+ const pw = writes.get(path27);
12567
13806
  const df = now2 - pw.lastChange;
12568
13807
  if (df >= threshold) {
12569
- writes.delete(path25);
13808
+ writes.delete(path27);
12570
13809
  awfEmit(void 0, curStat);
12571
13810
  } else {
12572
13811
  timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
12573
13812
  }
12574
13813
  });
12575
13814
  }
12576
- if (!writes.has(path25)) {
12577
- writes.set(path25, {
13815
+ if (!writes.has(path27)) {
13816
+ writes.set(path27, {
12578
13817
  lastChange: now,
12579
13818
  cancelWait: () => {
12580
- writes.delete(path25);
13819
+ writes.delete(path27);
12581
13820
  clearTimeout(timeoutHandler);
12582
13821
  return event;
12583
13822
  }
@@ -12588,8 +13827,8 @@ var FSWatcher = class extends EventEmitter2 {
12588
13827
  /**
12589
13828
  * Determines whether user has asked to ignore this path.
12590
13829
  */
12591
- _isIgnored(path25, stats) {
12592
- if (this.options.atomic && DOT_RE.test(path25))
13830
+ _isIgnored(path27, stats) {
13831
+ if (this.options.atomic && DOT_RE.test(path27))
12593
13832
  return true;
12594
13833
  if (!this._userIgnored) {
12595
13834
  const { cwd } = this.options;
@@ -12599,17 +13838,17 @@ var FSWatcher = class extends EventEmitter2 {
12599
13838
  const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
12600
13839
  this._userIgnored = anymatch(list, void 0);
12601
13840
  }
12602
- return this._userIgnored(path25, stats);
13841
+ return this._userIgnored(path27, stats);
12603
13842
  }
12604
- _isntIgnored(path25, stat4) {
12605
- return !this._isIgnored(path25, stat4);
13843
+ _isntIgnored(path27, stat4) {
13844
+ return !this._isIgnored(path27, stat4);
12606
13845
  }
12607
13846
  /**
12608
13847
  * Provides a set of common helpers and properties relating to symlink handling.
12609
13848
  * @param path file or directory pattern being watched
12610
13849
  */
12611
- _getWatchHelpers(path25) {
12612
- return new WatchHelper(path25, this.options.followSymlinks, this);
13850
+ _getWatchHelpers(path27) {
13851
+ return new WatchHelper(path27, this.options.followSymlinks, this);
12613
13852
  }
12614
13853
  // Directory helpers
12615
13854
  // -----------------
@@ -12641,63 +13880,63 @@ var FSWatcher = class extends EventEmitter2 {
12641
13880
  * @param item base path of item/directory
12642
13881
  */
12643
13882
  _remove(directory, item, isDirectory) {
12644
- const path25 = sp2.join(directory, item);
12645
- const fullPath = sp2.resolve(path25);
12646
- isDirectory = isDirectory != null ? isDirectory : this._watched.has(path25) || this._watched.has(fullPath);
12647
- if (!this._throttle("remove", path25, 100))
13883
+ const path27 = sp2.join(directory, item);
13884
+ const fullPath = sp2.resolve(path27);
13885
+ isDirectory = isDirectory != null ? isDirectory : this._watched.has(path27) || this._watched.has(fullPath);
13886
+ if (!this._throttle("remove", path27, 100))
12648
13887
  return;
12649
13888
  if (!isDirectory && this._watched.size === 1) {
12650
13889
  this.add(directory, item, true);
12651
13890
  }
12652
- const wp = this._getWatchedDir(path25);
13891
+ const wp = this._getWatchedDir(path27);
12653
13892
  const nestedDirectoryChildren = wp.getChildren();
12654
- nestedDirectoryChildren.forEach((nested) => this._remove(path25, nested));
13893
+ nestedDirectoryChildren.forEach((nested) => this._remove(path27, nested));
12655
13894
  const parent = this._getWatchedDir(directory);
12656
13895
  const wasTracked = parent.has(item);
12657
13896
  parent.remove(item);
12658
13897
  if (this._symlinkPaths.has(fullPath)) {
12659
13898
  this._symlinkPaths.delete(fullPath);
12660
13899
  }
12661
- let relPath = path25;
13900
+ let relPath = path27;
12662
13901
  if (this.options.cwd)
12663
- relPath = sp2.relative(this.options.cwd, path25);
13902
+ relPath = sp2.relative(this.options.cwd, path27);
12664
13903
  if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
12665
13904
  const event = this._pendingWrites.get(relPath).cancelWait();
12666
13905
  if (event === EVENTS.ADD)
12667
13906
  return;
12668
13907
  }
12669
- this._watched.delete(path25);
13908
+ this._watched.delete(path27);
12670
13909
  this._watched.delete(fullPath);
12671
13910
  const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK;
12672
- if (wasTracked && !this._isIgnored(path25))
12673
- this._emit(eventName, path25);
12674
- this._closePath(path25);
13911
+ if (wasTracked && !this._isIgnored(path27))
13912
+ this._emit(eventName, path27);
13913
+ this._closePath(path27);
12675
13914
  }
12676
13915
  /**
12677
13916
  * Closes all watchers for a path
12678
13917
  */
12679
- _closePath(path25) {
12680
- this._closeFile(path25);
12681
- const dir = sp2.dirname(path25);
12682
- this._getWatchedDir(dir).remove(sp2.basename(path25));
13918
+ _closePath(path27) {
13919
+ this._closeFile(path27);
13920
+ const dir = sp2.dirname(path27);
13921
+ this._getWatchedDir(dir).remove(sp2.basename(path27));
12683
13922
  }
12684
13923
  /**
12685
13924
  * Closes only file-specific watchers
12686
13925
  */
12687
- _closeFile(path25) {
12688
- const closers = this._closers.get(path25);
13926
+ _closeFile(path27) {
13927
+ const closers = this._closers.get(path27);
12689
13928
  if (!closers)
12690
13929
  return;
12691
13930
  closers.forEach((closer) => closer());
12692
- this._closers.delete(path25);
13931
+ this._closers.delete(path27);
12693
13932
  }
12694
- _addPathCloser(path25, closer) {
13933
+ _addPathCloser(path27, closer) {
12695
13934
  if (!closer)
12696
13935
  return;
12697
- let list = this._closers.get(path25);
13936
+ let list = this._closers.get(path27);
12698
13937
  if (!list) {
12699
13938
  list = [];
12700
- this._closers.set(path25, list);
13939
+ this._closers.set(path27, list);
12701
13940
  }
12702
13941
  list.push(closer);
12703
13942
  }
@@ -12727,7 +13966,7 @@ function watch(paths, options = {}) {
12727
13966
  var chokidar_default = { watch, FSWatcher };
12728
13967
 
12729
13968
  // src/watcher/file-watcher.ts
12730
- import * as path20 from "path";
13969
+ import * as path22 from "path";
12731
13970
  var FileWatcher = class {
12732
13971
  watcher = null;
12733
13972
  projectRoot;
@@ -12738,6 +13977,8 @@ var FileWatcher = class {
12738
13977
  debounceTimer = null;
12739
13978
  debounceMs = 1e3;
12740
13979
  onChanges = null;
13980
+ readyPromise = null;
13981
+ resolveReady = null;
12741
13982
  constructor(projectRoot, config, host = "opencode", options = {}) {
12742
13983
  this.projectRoot = projectRoot;
12743
13984
  this.config = config;
@@ -12753,15 +13994,15 @@ var FileWatcher = class {
12753
13994
  const watchTargets = this.configPath ? [this.projectRoot, this.configPath] : this.projectRoot;
12754
13995
  this.watcher = chokidar_default.watch(watchTargets, {
12755
13996
  ignored: (filePath) => {
12756
- const relativePath = path20.relative(this.projectRoot, filePath);
13997
+ const relativePath = path22.relative(this.projectRoot, filePath);
12757
13998
  if (!relativePath) return false;
12758
13999
  if (this.isProjectConfigPathOrAncestor(relativePath)) {
12759
14000
  return false;
12760
14001
  }
12761
- if (hasFilteredPathSegment(relativePath, path20.sep)) {
14002
+ if (hasFilteredPathSegment(relativePath, path22.sep)) {
12762
14003
  return true;
12763
14004
  }
12764
- if (isRestrictedDirectory(relativePath, path20.sep)) {
14005
+ if (isRestrictedDirectory(relativePath, path22.sep)) {
12765
14006
  return true;
12766
14007
  }
12767
14008
  if (ignoreFilter.ignores(relativePath)) {
@@ -12776,6 +14017,13 @@ var FileWatcher = class {
12776
14017
  pollInterval: 100
12777
14018
  }
12778
14019
  });
14020
+ this.readyPromise = new Promise((resolve15) => {
14021
+ this.resolveReady = resolve15;
14022
+ });
14023
+ this.watcher.once("ready", () => {
14024
+ this.resolveReady?.();
14025
+ this.resolveReady = null;
14026
+ });
12779
14027
  this.watcher.on("error", (error) => {
12780
14028
  const err = error instanceof Error ? error : null;
12781
14029
  if (err?.code === "EPERM" || err?.code === "EACCES") {
@@ -12807,24 +14055,24 @@ var FileWatcher = class {
12807
14055
  this.scheduleFlush();
12808
14056
  }
12809
14057
  isProjectConfigPath(filePath) {
12810
- const relativePath = path20.relative(this.projectRoot, filePath);
12811
- const normalizedRelativePath = path20.normalize(relativePath);
14058
+ const relativePath = path22.relative(this.projectRoot, filePath);
14059
+ const normalizedRelativePath = path22.normalize(relativePath);
12812
14060
  return this.getProjectConfigRelativePaths().some((configPath) => configPath === normalizedRelativePath);
12813
14061
  }
12814
14062
  isProjectConfigPathOrAncestor(relativePath) {
12815
- const normalizedRelativePath = path20.normalize(relativePath);
14063
+ const normalizedRelativePath = path22.normalize(relativePath);
12816
14064
  return this.getProjectConfigRelativePaths().some(
12817
- (configPath) => configPath === normalizedRelativePath || configPath.startsWith(`${normalizedRelativePath}${path20.sep}`)
14065
+ (configPath) => configPath === normalizedRelativePath || configPath.startsWith(`${normalizedRelativePath}${path22.sep}`)
12818
14066
  );
12819
14067
  }
12820
14068
  getProjectConfigRelativePaths() {
12821
14069
  if (this.configPath) {
12822
- return [path20.normalize(path20.relative(this.projectRoot, this.configPath))];
14070
+ return [path22.normalize(path22.relative(this.projectRoot, this.configPath))];
12823
14071
  }
12824
14072
  return [
12825
14073
  resolveProjectConfigPath(this.projectRoot, this.host),
12826
14074
  resolveWritableProjectConfigPath(this.projectRoot, this.host)
12827
- ].map((configPath) => path20.normalize(path20.relative(this.projectRoot, configPath)));
14075
+ ].map((configPath) => path22.normalize(path22.relative(this.projectRoot, configPath)));
12828
14076
  }
12829
14077
  scheduleFlush() {
12830
14078
  if (this.debounceTimer) {
@@ -12839,7 +14087,7 @@ var FileWatcher = class {
12839
14087
  return;
12840
14088
  }
12841
14089
  const changes = Array.from(this.pendingChanges.entries()).map(
12842
- ([path25, type]) => ({ path: path25, type })
14090
+ ([path27, type]) => ({ path: path27, type })
12843
14091
  );
12844
14092
  this.pendingChanges.clear();
12845
14093
  try {
@@ -12848,25 +14096,32 @@ var FileWatcher = class {
12848
14096
  console.error("Error handling file changes:", error);
12849
14097
  }
12850
14098
  }
12851
- stop() {
14099
+ async stop() {
12852
14100
  if (this.debounceTimer) {
12853
14101
  clearTimeout(this.debounceTimer);
12854
14102
  this.debounceTimer = null;
12855
14103
  }
12856
14104
  if (this.watcher) {
12857
- this.watcher.close();
14105
+ const watcher = this.watcher;
12858
14106
  this.watcher = null;
14107
+ await watcher.close();
12859
14108
  }
14109
+ this.resolveReady?.();
14110
+ this.resolveReady = null;
14111
+ this.readyPromise = null;
12860
14112
  this.pendingChanges.clear();
12861
14113
  this.onChanges = null;
12862
14114
  }
12863
14115
  isRunning() {
12864
14116
  return this.watcher !== null;
12865
14117
  }
14118
+ async waitUntilReady() {
14119
+ await (this.readyPromise ?? Promise.resolve());
14120
+ }
12866
14121
  };
12867
14122
 
12868
14123
  // src/watcher/git-head-watcher.ts
12869
- import * as path21 from "path";
14124
+ import * as path23 from "path";
12870
14125
  var GitHeadWatcher = class {
12871
14126
  watcher = null;
12872
14127
  projectRoot;
@@ -12888,7 +14143,7 @@ var GitHeadWatcher = class {
12888
14143
  this.onBranchChange = handler;
12889
14144
  this.currentBranch = getCurrentBranch(this.projectRoot);
12890
14145
  const headPath = getHeadPath(this.projectRoot);
12891
- const refsPath = path21.join(this.projectRoot, ".git", "refs", "heads");
14146
+ const refsPath = path23.join(this.projectRoot, ".git", "refs", "heads");
12892
14147
  this.watcher = chokidar_default.watch([headPath, refsPath], {
12893
14148
  persistent: true,
12894
14149
  ignoreInitial: true,
@@ -12925,14 +14180,15 @@ var GitHeadWatcher = class {
12925
14180
  getCurrentBranch() {
12926
14181
  return this.currentBranch;
12927
14182
  }
12928
- stop() {
14183
+ async stop() {
12929
14184
  if (this.debounceTimer) {
12930
14185
  clearTimeout(this.debounceTimer);
12931
14186
  this.debounceTimer = null;
12932
14187
  }
12933
14188
  if (this.watcher) {
12934
- this.watcher.close();
14189
+ const watcher = this.watcher;
12935
14190
  this.watcher = null;
14191
+ await watcher.close();
12936
14192
  }
12937
14193
  this.onBranchChange = null;
12938
14194
  }
@@ -12950,6 +14206,8 @@ var BackgroundReindexer = class {
12950
14206
  running = false;
12951
14207
  pending = false;
12952
14208
  stopped = false;
14209
+ retryTimer = null;
14210
+ retryDelayMs = 50;
12953
14211
  request() {
12954
14212
  if (this.stopped) {
12955
14213
  return;
@@ -12960,9 +14218,13 @@ var BackgroundReindexer = class {
12960
14218
  stop() {
12961
14219
  this.stopped = true;
12962
14220
  this.pending = false;
14221
+ if (this.retryTimer) {
14222
+ clearTimeout(this.retryTimer);
14223
+ this.retryTimer = null;
14224
+ }
12963
14225
  }
12964
14226
  drain() {
12965
- if (this.stopped || this.running || !this.pending) {
14227
+ if (this.stopped || this.running || this.retryTimer || !this.pending) {
12966
14228
  return;
12967
14229
  }
12968
14230
  this.pending = false;
@@ -12970,13 +14232,29 @@ var BackgroundReindexer = class {
12970
14232
  void this.run();
12971
14233
  }
12972
14234
  async run() {
14235
+ let retryAfterContention = false;
12973
14236
  try {
12974
14237
  await this.runIndex();
14238
+ this.retryDelayMs = 50;
12975
14239
  } catch (error) {
12976
- console.error("[codebase-index] Background reindex failed:", error);
14240
+ if (isTransientIndexLockContention(error)) {
14241
+ this.pending = true;
14242
+ retryAfterContention = true;
14243
+ } else {
14244
+ console.error("[codebase-index] Background reindex failed:", error);
14245
+ }
12977
14246
  } finally {
12978
14247
  this.running = false;
12979
- this.drain();
14248
+ if (retryAfterContention && !this.stopped) {
14249
+ const delay = this.retryDelayMs;
14250
+ this.retryDelayMs = Math.min(this.retryDelayMs * 2, 500);
14251
+ this.retryTimer = setTimeout(() => {
14252
+ this.retryTimer = null;
14253
+ this.drain();
14254
+ }, delay);
14255
+ } else {
14256
+ this.drain();
14257
+ }
12980
14258
  }
12981
14259
  }
12982
14260
  };
@@ -13010,10 +14288,12 @@ function createWatcherWithIndexer(getIndexer, projectRoot, config, host = "openc
13010
14288
  return {
13011
14289
  fileWatcher,
13012
14290
  gitWatcher,
13013
- stop() {
14291
+ whenReady() {
14292
+ return fileWatcher.waitUntilReady();
14293
+ },
14294
+ async stop() {
13014
14295
  backgroundReindexer.stop();
13015
- fileWatcher.stop();
13016
- gitWatcher?.stop();
14296
+ await Promise.all([fileWatcher.stop(), gitWatcher?.stop()]);
13017
14297
  }
13018
14298
  };
13019
14299
  }
@@ -13032,7 +14312,7 @@ function getConfigPaths(projectRoot, host, options) {
13032
14312
 
13033
14313
  // src/tools/visualize/activity.ts
13034
14314
  import { execFileSync } from "child_process";
13035
- import * as path22 from "path";
14315
+ import * as path24 from "path";
13036
14316
  function attachRecentActivity(data, projectRoot) {
13037
14317
  const activity = readGitActivity(projectRoot);
13038
14318
  const changes = activity.size > 0 ? buildGitChanges(data, activity, projectRoot) : buildGraphChanges(data);
@@ -13194,7 +14474,7 @@ function normalizePath3(filePath) {
13194
14474
  return filePath.replace(/\\/g, "/");
13195
14475
  }
13196
14476
  function toGitRelativePath(projectRoot, filePath) {
13197
- const relativePath = path22.isAbsolute(filePath) ? path22.relative(projectRoot, filePath) : filePath;
14477
+ const relativePath = path24.isAbsolute(filePath) ? path24.relative(projectRoot, filePath) : filePath;
13198
14478
  return normalizePath3(relativePath);
13199
14479
  }
13200
14480
 
@@ -13452,7 +14732,7 @@ render();
13452
14732
  }
13453
14733
 
13454
14734
  // src/tools/visualize/transform.ts
13455
- import * as path23 from "path";
14735
+ import * as path25 from "path";
13456
14736
 
13457
14737
  // src/tools/visualize/modules.ts
13458
14738
  var MAX_MODULES = 18;
@@ -13585,8 +14865,8 @@ function compactModules(prefixToNodes) {
13585
14865
  function deriveModules(nodes) {
13586
14866
  const initial = /* @__PURE__ */ new Map();
13587
14867
  for (const node of nodes) {
13588
- const relative10 = stripToProjectRelative(node.filePath);
13589
- const prefix = modulePrefixFromRelativePath(relative10);
14868
+ const relative11 = stripToProjectRelative(node.filePath);
14869
+ const prefix = modulePrefixFromRelativePath(relative11);
13590
14870
  if (!initial.has(prefix)) initial.set(prefix, []);
13591
14871
  initial.get(prefix)?.push(node);
13592
14872
  }
@@ -13712,7 +14992,7 @@ function transformForVisualization(symbols, edges, options = {}) {
13712
14992
  filePath: s.filePath,
13713
14993
  kind: s.kind,
13714
14994
  line: s.startLine,
13715
- directory: path23.dirname(s.filePath),
14995
+ directory: path25.dirname(s.filePath),
13716
14996
  moduleId: "",
13717
14997
  moduleLabel: ""
13718
14998
  }));
@@ -13740,9 +15020,9 @@ function parseArgs(argv) {
13740
15020
  let host = "opencode";
13741
15021
  for (let i = 2; i < argv.length; i++) {
13742
15022
  if (argv[i] === "--project" && argv[i + 1]) {
13743
- project = path24.resolve(argv[++i]);
15023
+ project = path26.resolve(argv[++i]);
13744
15024
  } else if (argv[i] === "--config" && argv[i + 1]) {
13745
- config = path24.resolve(argv[++i]);
15025
+ config = path26.resolve(argv[++i]);
13746
15026
  } else if (argv[i] === "--host" && argv[i + 1]) {
13747
15027
  host = parseHostMode(argv[++i]);
13748
15028
  } else if (argv[i] === "--host") {
@@ -13755,7 +15035,7 @@ function loadCliRawConfig(args) {
13755
15035
  return args.config ? loadConfigFile(args.config) : loadMergedConfig(args.project, args.host);
13756
15036
  }
13757
15037
  function isCliEntrypoint(moduleUrl, argvPath) {
13758
- return argvPath !== void 0 && realpathSync2(fileURLToPath2(moduleUrl)) === realpathSync2(argvPath);
15038
+ return argvPath !== void 0 && realpathSync3(fileURLToPath2(moduleUrl)) === realpathSync3(argvPath);
13759
15039
  }
13760
15040
  function parseVisualizeArgs(argv, cwd) {
13761
15041
  let project = cwd;
@@ -13765,7 +15045,7 @@ function parseVisualizeArgs(argv, cwd) {
13765
15045
  for (let i = 0; i < argv.length; i++) {
13766
15046
  const arg = argv[i];
13767
15047
  if (arg === "--project" && argv[i + 1]) {
13768
- project = path24.resolve(argv[++i]);
15048
+ project = path26.resolve(argv[++i]);
13769
15049
  } else if (arg === "--max" && argv[i + 1]) {
13770
15050
  maxNodes = Number(argv[++i]);
13771
15051
  } else if (arg.startsWith("--max=") || arg.startsWith("max=")) {
@@ -13802,8 +15082,8 @@ async function handleVisualizeCommand(argv, cwd) {
13802
15082
  console.error("No connected symbols found. Retry with: npm run visualize -- orphans");
13803
15083
  return 1;
13804
15084
  }
13805
- const outputPath = path24.join(os5.tmpdir(), `call-graph-${Date.now()}.html`);
13806
- writeFileSync6(outputPath, generateVisualizationHtml(vizData), "utf-8");
15085
+ const outputPath = path26.join(os6.tmpdir(), `call-graph-${Date.now()}.html`);
15086
+ writeFileSync7(outputPath, generateVisualizationHtml(vizData), "utf-8");
13807
15087
  console.log(`Temporal call graph visualization generated: ${outputPath}`);
13808
15088
  console.log(`Nodes: ${vizData.nodes.length} | Edges: ${vizData.edges.length}`);
13809
15089
  console.log(`Recent change lenses: ${vizData.changes?.length ?? 0}`);
@@ -13832,7 +15112,7 @@ async function main() {
13832
15112
  const transport = new StdioServerTransport();
13833
15113
  await server.connect(transport);
13834
15114
  let watcher = null;
13835
- const isHomeDir = path24.resolve(args.project) === path24.resolve(os5.homedir());
15115
+ const isHomeDir = path26.resolve(args.project) === path26.resolve(os6.homedir());
13836
15116
  const isValidProject = !isHomeDir && (!config.indexing.requireProjectMarker || hasProjectMarker(args.project));
13837
15117
  if (config.indexing.autoIndex && isValidProject) {
13838
15118
  const indexer = getIndexerForProject(args.project, args.host);
@@ -13847,14 +15127,25 @@ async function main() {
13847
15127
  args.config ? { configPath: args.config } : {}
13848
15128
  );
13849
15129
  }
13850
- const shutdown = () => {
13851
- watcher?.stop();
13852
- server.close().catch(() => {
13853
- });
13854
- process.exit(0);
15130
+ let shuttingDown = false;
15131
+ const shutdown = async () => {
15132
+ if (shuttingDown) return;
15133
+ shuttingDown = true;
15134
+ try {
15135
+ await watcher?.stop();
15136
+ await server.close();
15137
+ process.exit(0);
15138
+ } catch (error) {
15139
+ console.error("Failed to stop MCP server cleanly:", error);
15140
+ process.exit(1);
15141
+ }
13855
15142
  };
13856
- process.on("SIGINT", shutdown);
13857
- process.on("SIGTERM", shutdown);
15143
+ process.on("SIGINT", () => {
15144
+ void shutdown();
15145
+ });
15146
+ process.on("SIGTERM", () => {
15147
+ void shutdown();
15148
+ });
13858
15149
  }
13859
15150
  function handleMainError(error) {
13860
15151
  if (error instanceof Error && error.message.startsWith("Invalid host mode")) {