opencode-codebase-index 0.14.0 → 0.16.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.cjs CHANGED
@@ -496,7 +496,7 @@ var require_ignore = __commonJS({
496
496
  // path matching.
497
497
  // - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
498
498
  // @returns {TestResult} true if a file is ignored
499
- test(path26, checkUnignored, mode) {
499
+ test(path27, checkUnignored, mode) {
500
500
  let ignored = false;
501
501
  let unignored = false;
502
502
  let matchedRule;
@@ -505,7 +505,7 @@ var require_ignore = __commonJS({
505
505
  if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) {
506
506
  return;
507
507
  }
508
- const matched = rule[mode].test(path26);
508
+ const matched = rule[mode].test(path27);
509
509
  if (!matched) {
510
510
  return;
511
511
  }
@@ -526,17 +526,17 @@ var require_ignore = __commonJS({
526
526
  var throwError = (message, Ctor) => {
527
527
  throw new Ctor(message);
528
528
  };
529
- var checkPath = (path26, originalPath, doThrow) => {
530
- if (!isString(path26)) {
529
+ var checkPath = (path27, originalPath, doThrow) => {
530
+ if (!isString(path27)) {
531
531
  return doThrow(
532
532
  `path must be a string, but got \`${originalPath}\``,
533
533
  TypeError
534
534
  );
535
535
  }
536
- if (!path26) {
536
+ if (!path27) {
537
537
  return doThrow(`path must not be empty`, TypeError);
538
538
  }
539
- if (checkPath.isNotRelative(path26)) {
539
+ if (checkPath.isNotRelative(path27)) {
540
540
  const r = "`path.relative()`d";
541
541
  return doThrow(
542
542
  `path should be a ${r} string, but got "${originalPath}"`,
@@ -545,7 +545,7 @@ var require_ignore = __commonJS({
545
545
  }
546
546
  return true;
547
547
  };
548
- var isNotRelative = (path26) => REGEX_TEST_INVALID_PATH.test(path26);
548
+ var isNotRelative = (path27) => REGEX_TEST_INVALID_PATH.test(path27);
549
549
  checkPath.isNotRelative = isNotRelative;
550
550
  checkPath.convert = (p) => p;
551
551
  var Ignore2 = class {
@@ -575,19 +575,19 @@ var require_ignore = __commonJS({
575
575
  }
576
576
  // @returns {TestResult}
577
577
  _test(originalPath, cache, checkUnignored, slices) {
578
- const path26 = originalPath && checkPath.convert(originalPath);
578
+ const path27 = originalPath && checkPath.convert(originalPath);
579
579
  checkPath(
580
- path26,
580
+ path27,
581
581
  originalPath,
582
582
  this._strictPathCheck ? throwError : RETURN_FALSE
583
583
  );
584
- return this._t(path26, cache, checkUnignored, slices);
584
+ return this._t(path27, cache, checkUnignored, slices);
585
585
  }
586
- checkIgnore(path26) {
587
- if (!REGEX_TEST_TRAILING_SLASH.test(path26)) {
588
- return this.test(path26);
586
+ checkIgnore(path27) {
587
+ if (!REGEX_TEST_TRAILING_SLASH.test(path27)) {
588
+ return this.test(path27);
589
589
  }
590
- const slices = path26.split(SLASH2).filter(Boolean);
590
+ const slices = path27.split(SLASH2).filter(Boolean);
591
591
  slices.pop();
592
592
  if (slices.length) {
593
593
  const parent = this._t(
@@ -600,18 +600,18 @@ var require_ignore = __commonJS({
600
600
  return parent;
601
601
  }
602
602
  }
603
- return this._rules.test(path26, false, MODE_CHECK_IGNORE);
603
+ return this._rules.test(path27, false, MODE_CHECK_IGNORE);
604
604
  }
605
- _t(path26, cache, checkUnignored, slices) {
606
- if (path26 in cache) {
607
- return cache[path26];
605
+ _t(path27, cache, checkUnignored, slices) {
606
+ if (path27 in cache) {
607
+ return cache[path27];
608
608
  }
609
609
  if (!slices) {
610
- slices = path26.split(SLASH2).filter(Boolean);
610
+ slices = path27.split(SLASH2).filter(Boolean);
611
611
  }
612
612
  slices.pop();
613
613
  if (!slices.length) {
614
- return cache[path26] = this._rules.test(path26, checkUnignored, MODE_IGNORE);
614
+ return cache[path27] = this._rules.test(path27, checkUnignored, MODE_IGNORE);
615
615
  }
616
616
  const parent = this._t(
617
617
  slices.join(SLASH2) + SLASH2,
@@ -619,29 +619,29 @@ var require_ignore = __commonJS({
619
619
  checkUnignored,
620
620
  slices
621
621
  );
622
- return cache[path26] = parent.ignored ? parent : this._rules.test(path26, checkUnignored, MODE_IGNORE);
622
+ return cache[path27] = parent.ignored ? parent : this._rules.test(path27, checkUnignored, MODE_IGNORE);
623
623
  }
624
- ignores(path26) {
625
- return this._test(path26, this._ignoreCache, false).ignored;
624
+ ignores(path27) {
625
+ return this._test(path27, this._ignoreCache, false).ignored;
626
626
  }
627
627
  createFilter() {
628
- return (path26) => !this.ignores(path26);
628
+ return (path27) => !this.ignores(path27);
629
629
  }
630
630
  filter(paths) {
631
631
  return makeArray(paths).filter(this.createFilter());
632
632
  }
633
633
  // @returns {TestResult}
634
- test(path26) {
635
- return this._test(path26, this._testCache, true);
634
+ test(path27) {
635
+ return this._test(path27, this._testCache, true);
636
636
  }
637
637
  };
638
638
  var factory = (options) => new Ignore2(options);
639
- var isPathValid = (path26) => checkPath(path26 && checkPath.convert(path26), path26, RETURN_FALSE);
639
+ var isPathValid = (path27) => checkPath(path27 && checkPath.convert(path27), path27, RETURN_FALSE);
640
640
  var setupWindows = () => {
641
641
  const makePosix = (str) => /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str) ? str : str.replace(/\\/g, "/");
642
642
  checkPath.convert = makePosix;
643
643
  const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
644
- checkPath.isNotRelative = (path26) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path26) || isNotRelative(path26);
644
+ checkPath.isNotRelative = (path27) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path27) || isNotRelative(path27);
645
645
  };
646
646
  if (
647
647
  // Detect `process` so that it can run in browsers.
@@ -665,17 +665,17 @@ __export(cli_exports, {
665
665
  });
666
666
  module.exports = __toCommonJS(cli_exports);
667
667
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
668
- var import_fs15 = require("fs");
669
- var os5 = __toESM(require("os"), 1);
670
- var path25 = __toESM(require("path"), 1);
668
+ var import_fs16 = require("fs");
669
+ var os6 = __toESM(require("os"), 1);
670
+ var path26 = __toESM(require("path"), 1);
671
671
  var import_url2 = require("url");
672
672
 
673
673
  // src/config/constants.ts
674
674
  var DEFAULT_INCLUDE = [
675
- "**/*.{ts,tsx,js,jsx,mjs,cjs}",
675
+ "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}",
676
676
  "**/*.{py,pyi}",
677
- "**/*.{go,rs,java,kt,scala}",
678
- "**/*.{c,cpp,cc,h,hpp}",
677
+ "**/*.{go,rs,java,cs,kt,scala}",
678
+ "**/*.{c,cpp,cc,cxx,h,hpp,hxx}",
679
679
  "**/*.{rb,php,inc,swift}",
680
680
  "**/*.{cls,trigger}",
681
681
  "**/*.{vue,svelte,astro}",
@@ -1058,7 +1058,7 @@ var autoDetectProviders = AUTO_DETECT_PROVIDER_ORDER.filter(
1058
1058
  );
1059
1059
 
1060
1060
  // src/config/host.ts
1061
- var HOST_MODES = ["opencode", "codex", "claude", "pi"];
1061
+ var HOST_MODES = ["opencode", "codex", "claude", "pi", "jcode"];
1062
1062
  function isSupportedHostMode(value) {
1063
1063
  return HOST_MODES.includes(value);
1064
1064
  }
@@ -1113,9 +1113,9 @@ var import_fs = require("fs");
1113
1113
  var path = __toESM(require("path"), 1);
1114
1114
 
1115
1115
  // src/eval/report-formatters.ts
1116
- function assertFiniteNumber(value, path26) {
1116
+ function assertFiniteNumber(value, path27) {
1117
1117
  if (typeof value !== "number" || Number.isNaN(value) || !Number.isFinite(value)) {
1118
- throw new Error(`${path26} must be a finite number`);
1118
+ throw new Error(`${path27} must be a finite number`);
1119
1119
  }
1120
1120
  return value;
1121
1121
  }
@@ -1303,13 +1303,13 @@ function buildPerQueryArtifact(perQuery) {
1303
1303
  }
1304
1304
 
1305
1305
  // src/eval/runner.ts
1306
- var import_fs10 = require("fs");
1307
- var path14 = __toESM(require("path"), 1);
1306
+ var import_fs11 = require("fs");
1307
+ var path15 = __toESM(require("path"), 1);
1308
1308
  var import_perf_hooks2 = require("perf_hooks");
1309
1309
 
1310
1310
  // src/indexer/index.ts
1311
- var import_fs7 = require("fs");
1312
- var path11 = __toESM(require("path"), 1);
1311
+ var import_fs8 = require("fs");
1312
+ var path12 = __toESM(require("path"), 1);
1313
1313
  var import_perf_hooks = require("perf_hooks");
1314
1314
  var import_child_process3 = require("child_process");
1315
1315
  var import_util3 = require("util");
@@ -2591,17 +2591,17 @@ function validateExternalUrl(urlString) {
2591
2591
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
2592
2592
  return { valid: false, reason: `Blocked protocol: ${parsed.protocol}` };
2593
2593
  }
2594
- const hostname = parsed.hostname.toLowerCase();
2595
- if (BLOCKED_HOSTNAMES.has(hostname)) {
2596
- return { valid: false, reason: `Blocked: cloud metadata service (${hostname})` };
2594
+ const hostname2 = parsed.hostname.toLowerCase();
2595
+ if (BLOCKED_HOSTNAMES.has(hostname2)) {
2596
+ return { valid: false, reason: `Blocked: cloud metadata service (${hostname2})` };
2597
2597
  }
2598
2598
  for (const pattern of BLOCKED_METADATA_IPS) {
2599
- if (pattern.test(hostname)) {
2600
- return { valid: false, reason: `Blocked: cloud metadata IP (${hostname})` };
2599
+ if (pattern.test(hostname2)) {
2600
+ return { valid: false, reason: `Blocked: cloud metadata IP (${hostname2})` };
2601
2601
  }
2602
2602
  }
2603
- if (/^169\.254\./.test(hostname)) {
2604
- return { valid: false, reason: `Blocked: link-local address (${hostname})` };
2603
+ if (/^169\.254\./.test(hostname2)) {
2604
+ return { valid: false, reason: `Blocked: link-local address (${hostname2})` };
2605
2605
  }
2606
2606
  return { valid: true };
2607
2607
  }
@@ -3792,6 +3792,12 @@ function createMockNativeBinding() {
3792
3792
  constructor() {
3793
3793
  throw error;
3794
3794
  }
3795
+ static openReadOnly() {
3796
+ throw error;
3797
+ }
3798
+ static createEmptyReadOnly() {
3799
+ throw error;
3800
+ }
3795
3801
  close() {
3796
3802
  throw error;
3797
3803
  }
@@ -3895,6 +3901,12 @@ var VectorStore = class {
3895
3901
  load() {
3896
3902
  this.inner.load();
3897
3903
  }
3904
+ loadStrict() {
3905
+ this.inner.loadStrict();
3906
+ }
3907
+ hasFingerprint() {
3908
+ return this.inner.hasFingerprint();
3909
+ }
3898
3910
  count() {
3899
3911
  return this.inner.count();
3900
3912
  }
@@ -4177,12 +4189,24 @@ var InvertedIndex = class {
4177
4189
  return this.inner.documentCount();
4178
4190
  }
4179
4191
  };
4180
- var Database = class {
4192
+ var Database = class _Database {
4181
4193
  inner;
4182
4194
  closed = false;
4183
4195
  constructor(dbPath) {
4184
4196
  this.inner = new native.Database(dbPath);
4185
4197
  }
4198
+ static fromNative(inner) {
4199
+ const database = Object.create(_Database.prototype);
4200
+ database.inner = inner;
4201
+ database.closed = false;
4202
+ return database;
4203
+ }
4204
+ static openReadOnly(dbPath) {
4205
+ return _Database.fromNative(native.Database.openReadOnly(dbPath));
4206
+ }
4207
+ static createEmptyReadOnly() {
4208
+ return _Database.fromNative(native.Database.createEmptyReadOnly());
4209
+ }
4186
4210
  throwIfClosed() {
4187
4211
  if (this.closed) {
4188
4212
  throw new Error("Database is closed");
@@ -4651,6 +4675,9 @@ function getHostProjectIndexRelativePath(host) {
4651
4675
  function hasHostProjectConfig(projectRoot, host) {
4652
4676
  return (0, import_fs6.existsSync)(path8.join(projectRoot, getProjectConfigRelativePath(host)));
4653
4677
  }
4678
+ function hasHostGlobalConfig(host) {
4679
+ return (0, import_fs6.existsSync)(getGlobalConfigPath(host));
4680
+ }
4654
4681
  function getGlobalIndexPath(host = "opencode") {
4655
4682
  switch (host) {
4656
4683
  case "opencode":
@@ -4690,6 +4717,9 @@ function resolveGlobalIndexPath(host = "opencode") {
4690
4717
  return hostIndexPath;
4691
4718
  }
4692
4719
  if (host !== "opencode") {
4720
+ if (hasHostGlobalConfig(host)) {
4721
+ return hostIndexPath;
4722
+ }
4693
4723
  const legacyIndexPath = getGlobalIndexPath("opencode");
4694
4724
  if ((0, import_fs6.existsSync)(legacyIndexPath)) {
4695
4725
  return legacyIndexPath;
@@ -4906,6 +4936,422 @@ async function getChunkGitBlame(projectRoot, filePath, startLine, endLine) {
4906
4936
  }
4907
4937
  }
4908
4938
 
4939
+ // src/indexer/index-lock.ts
4940
+ var import_crypto = require("crypto");
4941
+ var import_fs7 = require("fs");
4942
+ var os4 = __toESM(require("os"), 1);
4943
+ var path11 = __toESM(require("path"), 1);
4944
+ var OWNER_FILE_NAME = "owner.json";
4945
+ var RECLAIM_DIRECTORY_NAME = "reclaiming";
4946
+ var RECOVERY_MARKER_PREFIX = "indexing.lock.recovery.";
4947
+ 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;
4948
+ var VALID_OPERATIONS = /* @__PURE__ */ new Set([
4949
+ "initialize",
4950
+ "index",
4951
+ "force-index",
4952
+ "clear",
4953
+ "health-check",
4954
+ "retry-failed-batches",
4955
+ "recovery"
4956
+ ]);
4957
+ var temporaryCounter = 0;
4958
+ function getErrorCode(error) {
4959
+ return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
4960
+ }
4961
+ function retryTransientFilesystemOperation(operation) {
4962
+ let lastError;
4963
+ for (let attempt = 0; attempt < 3; attempt += 1) {
4964
+ try {
4965
+ operation();
4966
+ return;
4967
+ } catch (error) {
4968
+ lastError = error;
4969
+ const code = getErrorCode(error);
4970
+ if (code !== "EBUSY" && code !== "EPERM") throw error;
4971
+ if (attempt < 2) {
4972
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, (attempt + 1) * 10);
4973
+ }
4974
+ }
4975
+ }
4976
+ throw lastError;
4977
+ }
4978
+ function parseOwner(value) {
4979
+ if (typeof value !== "object" || value === null) return null;
4980
+ const candidate = value;
4981
+ if (!Number.isInteger(candidate.pid) || (candidate.pid ?? 0) <= 0) return null;
4982
+ if (typeof candidate.hostname !== "string" || candidate.hostname.length === 0) return null;
4983
+ if (typeof candidate.startedAt !== "string" || Number.isNaN(Date.parse(candidate.startedAt))) return null;
4984
+ if (typeof candidate.operation !== "string" || !VALID_OPERATIONS.has(candidate.operation)) return null;
4985
+ if (typeof candidate.token !== "string" || !UUID_PATTERN.test(candidate.token)) return null;
4986
+ return candidate;
4987
+ }
4988
+ function parseReclaimOwner(value) {
4989
+ if (typeof value !== "object" || value === null) return null;
4990
+ const candidate = value;
4991
+ if (!Number.isInteger(candidate.pid) || (candidate.pid ?? 0) <= 0) return null;
4992
+ if (typeof candidate.hostname !== "string" || candidate.hostname.length === 0) return null;
4993
+ if (typeof candidate.startedAt !== "string" || Number.isNaN(Date.parse(candidate.startedAt))) return null;
4994
+ if (typeof candidate.token !== "string" || !UUID_PATTERN.test(candidate.token)) return null;
4995
+ if (typeof candidate.expectedOwnerToken !== "string" || !UUID_PATTERN.test(candidate.expectedOwnerToken)) return null;
4996
+ return candidate;
4997
+ }
4998
+ function readJsonDirectory(directoryPath, parser) {
4999
+ try {
5000
+ return parser(JSON.parse((0, import_fs7.readFileSync)(path11.join(directoryPath, OWNER_FILE_NAME), "utf-8")));
5001
+ } catch {
5002
+ return null;
5003
+ }
5004
+ }
5005
+ function readDirectoryOwner(lockPath) {
5006
+ return readJsonDirectory(lockPath, parseOwner);
5007
+ }
5008
+ function readReclaimOwner(markerPath) {
5009
+ return readJsonDirectory(markerPath, parseReclaimOwner);
5010
+ }
5011
+ function readRecoveryOwner(markerPath) {
5012
+ return readDirectoryOwner(markerPath);
5013
+ }
5014
+ function readLegacyOwner(lockPath) {
5015
+ try {
5016
+ const parsed = JSON.parse((0, import_fs7.readFileSync)(lockPath, "utf-8"));
5017
+ if (!Number.isInteger(parsed.pid) || Number(parsed.pid) <= 0) return null;
5018
+ if (typeof parsed.startedAt !== "string" || Number.isNaN(Date.parse(parsed.startedAt))) return null;
5019
+ return {
5020
+ pid: Number(parsed.pid),
5021
+ hostname: typeof parsed.hostname === "string" ? parsed.hostname : os4.hostname(),
5022
+ startedAt: parsed.startedAt,
5023
+ operation: typeof parsed.operation === "string" && VALID_OPERATIONS.has(parsed.operation) ? parsed.operation : "index",
5024
+ token: typeof parsed.token === "string" ? parsed.token : "legacy-v0.14.0"
5025
+ };
5026
+ } catch {
5027
+ return null;
5028
+ }
5029
+ }
5030
+ function getOwnerLiveness(owner) {
5031
+ if (owner.hostname !== os4.hostname()) return "unknown";
5032
+ try {
5033
+ process.kill(owner.pid, 0);
5034
+ return "alive";
5035
+ } catch (error) {
5036
+ const code = getErrorCode(error);
5037
+ if (code === "ESRCH") return "dead";
5038
+ if (code === "EPERM") return "alive";
5039
+ return "unknown";
5040
+ }
5041
+ }
5042
+ function sameOwner(left, right) {
5043
+ return left.pid === right.pid && left.hostname === right.hostname && left.token === right.token;
5044
+ }
5045
+ function sameReclaimOwner(left, right) {
5046
+ return left.pid === right.pid && left.hostname === right.hostname && left.token === right.token && left.expectedOwnerToken === right.expectedOwnerToken;
5047
+ }
5048
+ function publishJsonDirectory(finalPath, value) {
5049
+ const candidatePath = `${finalPath}.candidate.${process.pid}.${(0, import_crypto.randomUUID)()}`;
5050
+ (0, import_fs7.mkdirSync)(candidatePath, { mode: 448 });
5051
+ try {
5052
+ (0, import_fs7.writeFileSync)(path11.join(candidatePath, OWNER_FILE_NAME), JSON.stringify(value), {
5053
+ encoding: "utf-8",
5054
+ flag: "wx",
5055
+ mode: 384
5056
+ });
5057
+ if ((0, import_fs7.existsSync)(finalPath)) return false;
5058
+ try {
5059
+ (0, import_fs7.renameSync)(candidatePath, finalPath);
5060
+ return true;
5061
+ } catch (error) {
5062
+ if ((0, import_fs7.existsSync)(finalPath)) return false;
5063
+ throw error;
5064
+ }
5065
+ } finally {
5066
+ if ((0, import_fs7.existsSync)(candidatePath)) (0, import_fs7.rmSync)(candidatePath, { recursive: true, force: true });
5067
+ }
5068
+ }
5069
+ function createOwner(operation) {
5070
+ return {
5071
+ pid: process.pid,
5072
+ hostname: os4.hostname(),
5073
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5074
+ operation,
5075
+ token: (0, import_crypto.randomUUID)()
5076
+ };
5077
+ }
5078
+ function recoveryMarkerPath(indexPath, owner) {
5079
+ return path11.join(indexPath, `${RECOVERY_MARKER_PREFIX}${owner.token}`);
5080
+ }
5081
+ function publishRecoveryMarker(indexPath, owner) {
5082
+ const markerPath = recoveryMarkerPath(indexPath, owner);
5083
+ if (!publishJsonDirectory(markerPath, owner)) {
5084
+ const markerOwner = readRecoveryOwner(markerPath);
5085
+ if (!markerOwner || !sameOwner(markerOwner, owner)) {
5086
+ throw new IndexLockContentionError(markerPath, markerOwner, "unknown-owner");
5087
+ }
5088
+ }
5089
+ return markerPath;
5090
+ }
5091
+ function getPendingRecoveries(indexPath) {
5092
+ const recoveries = [];
5093
+ const markerNames = (0, import_fs7.readdirSync)(indexPath).filter((name) => {
5094
+ if (!name.startsWith(RECOVERY_MARKER_PREFIX)) return false;
5095
+ return UUID_PATTERN.test(name.slice(RECOVERY_MARKER_PREFIX.length));
5096
+ }).sort();
5097
+ for (const markerName of markerNames) {
5098
+ const markerPath = path11.join(indexPath, markerName);
5099
+ const markerToken = markerName.slice(RECOVERY_MARKER_PREFIX.length);
5100
+ let markerStats;
5101
+ try {
5102
+ markerStats = (0, import_fs7.lstatSync)(markerPath);
5103
+ } catch (error) {
5104
+ if (getErrorCode(error) === "ENOENT") continue;
5105
+ throw error;
5106
+ }
5107
+ if (!markerStats.isDirectory()) {
5108
+ throw new IndexLockContentionError(markerPath, null, "unknown-owner");
5109
+ }
5110
+ const owner = readRecoveryOwner(markerPath);
5111
+ if (!owner || owner.token !== markerToken || getOwnerLiveness(owner) !== "dead") {
5112
+ throw new IndexLockContentionError(markerPath, owner, "unknown-owner");
5113
+ }
5114
+ recoveries.push({ owner, markerPath });
5115
+ }
5116
+ recoveries.sort((left, right) => {
5117
+ const byTimestamp = left.owner.startedAt.localeCompare(right.owner.startedAt);
5118
+ return byTimestamp !== 0 ? byTimestamp : left.markerPath.localeCompare(right.markerPath);
5119
+ });
5120
+ return recoveries;
5121
+ }
5122
+ function cleanupDeadPublicationCandidates(indexPath) {
5123
+ const candidatePattern = /^indexing\.lock(?:\.recovery\.[0-9a-f-]{36})?\.candidate\.(\d+)\.[0-9a-f-]{36}$/i;
5124
+ for (const entry of (0, import_fs7.readdirSync)(indexPath)) {
5125
+ const match = candidatePattern.exec(entry);
5126
+ if (!match) continue;
5127
+ const pid = Number(match[1]);
5128
+ if (!Number.isInteger(pid) || pid <= 0) continue;
5129
+ if (getOwnerLiveness({ pid, hostname: os4.hostname() }) !== "dead") continue;
5130
+ (0, import_fs7.rmSync)(path11.join(indexPath, entry), { recursive: true, force: true });
5131
+ }
5132
+ }
5133
+ function removeDeadReclaimMarker(lockPath, expectedOwner) {
5134
+ const markerPath = path11.join(lockPath, RECLAIM_DIRECTORY_NAME);
5135
+ const marker = readReclaimOwner(markerPath);
5136
+ if (!marker || marker.expectedOwnerToken !== expectedOwner.token || getOwnerLiveness(marker) !== "dead") {
5137
+ return false;
5138
+ }
5139
+ const currentOwner = readDirectoryOwner(lockPath);
5140
+ if (!currentOwner || !sameOwner(currentOwner, expectedOwner) || getOwnerLiveness(currentOwner) !== "dead") {
5141
+ return false;
5142
+ }
5143
+ const claimedMarkerPath = `${markerPath}.stale.${marker.pid}.${marker.token}.${(0, import_crypto.randomUUID)()}`;
5144
+ try {
5145
+ (0, import_fs7.renameSync)(markerPath, claimedMarkerPath);
5146
+ } catch (error) {
5147
+ if (getErrorCode(error) === "ENOENT") return false;
5148
+ throw error;
5149
+ }
5150
+ const claimedMarker = readReclaimOwner(claimedMarkerPath);
5151
+ const ownerAfterClaim = readDirectoryOwner(lockPath);
5152
+ if (!claimedMarker || !sameReclaimOwner(claimedMarker, marker) || !ownerAfterClaim || !sameOwner(ownerAfterClaim, expectedOwner) || getOwnerLiveness(ownerAfterClaim) !== "dead") {
5153
+ if (!(0, import_fs7.existsSync)(markerPath) && (0, import_fs7.existsSync)(claimedMarkerPath)) {
5154
+ (0, import_fs7.renameSync)(claimedMarkerPath, markerPath);
5155
+ }
5156
+ return false;
5157
+ }
5158
+ (0, import_fs7.rmSync)(claimedMarkerPath, { recursive: true, force: true });
5159
+ return true;
5160
+ }
5161
+ function reclaimDeadOwner(indexPath, lockPath, expectedOwner) {
5162
+ const reclaimPath = path11.join(lockPath, RECLAIM_DIRECTORY_NAME);
5163
+ const reclaimOwner = {
5164
+ pid: process.pid,
5165
+ hostname: os4.hostname(),
5166
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5167
+ token: (0, import_crypto.randomUUID)(),
5168
+ expectedOwnerToken: expectedOwner.token
5169
+ };
5170
+ for (let attempt = 0; attempt < 2; attempt += 1) {
5171
+ if (publishJsonDirectory(reclaimPath, reclaimOwner)) break;
5172
+ if (attempt === 0 && removeDeadReclaimMarker(lockPath, expectedOwner)) continue;
5173
+ return false;
5174
+ }
5175
+ try {
5176
+ const currentReclaimer = readReclaimOwner(reclaimPath);
5177
+ const currentOwner = readDirectoryOwner(lockPath);
5178
+ if (!currentReclaimer || !sameReclaimOwner(currentReclaimer, reclaimOwner) || !currentOwner || !sameOwner(currentOwner, expectedOwner) || getOwnerLiveness(currentOwner) !== "dead") {
5179
+ return false;
5180
+ }
5181
+ publishRecoveryMarker(indexPath, expectedOwner);
5182
+ const ownerBeforeQuarantine = readDirectoryOwner(lockPath);
5183
+ const reclaimerBeforeQuarantine = readReclaimOwner(reclaimPath);
5184
+ if (!ownerBeforeQuarantine || !sameOwner(ownerBeforeQuarantine, expectedOwner) || getOwnerLiveness(ownerBeforeQuarantine) !== "dead" || !reclaimerBeforeQuarantine || !sameReclaimOwner(reclaimerBeforeQuarantine, reclaimOwner)) {
5185
+ return false;
5186
+ }
5187
+ const quarantinePath = `${lockPath}.stale.${expectedOwner.token}.${reclaimOwner.token}`;
5188
+ (0, import_fs7.renameSync)(lockPath, quarantinePath);
5189
+ (0, import_fs7.rmSync)(quarantinePath, { recursive: true, force: true });
5190
+ return true;
5191
+ } catch (error) {
5192
+ if (getErrorCode(error) === "ENOENT") return false;
5193
+ throw error;
5194
+ }
5195
+ }
5196
+ var IndexLockContentionError = class extends Error {
5197
+ constructor(lockPath, owner, reason) {
5198
+ const ownerDescription = owner ? `PID ${owner.pid} on ${owner.hostname}, operation ${owner.operation}, since ${owner.startedAt}` : "an unreadable owner";
5199
+ super(`Index mutation already in progress: ${ownerDescription}`);
5200
+ this.lockPath = lockPath;
5201
+ this.owner = owner;
5202
+ this.reason = reason;
5203
+ this.name = "IndexLockContentionError";
5204
+ }
5205
+ lockPath;
5206
+ owner;
5207
+ reason;
5208
+ code = "INDEX_BUSY";
5209
+ };
5210
+ function isIndexLockContentionError(error) {
5211
+ return error instanceof IndexLockContentionError || typeof error === "object" && error !== null && "code" in error && error.code === "INDEX_BUSY";
5212
+ }
5213
+ function isTransientIndexLockContention(error) {
5214
+ if (!isIndexLockContentionError(error) || !("reason" in error)) return false;
5215
+ return error.reason === "active" || error.reason === "reclaiming";
5216
+ }
5217
+ function acquireIndexLock(indexPath, operation) {
5218
+ (0, import_fs7.mkdirSync)(indexPath, { recursive: true });
5219
+ const canonicalIndexPath = import_fs7.realpathSync.native(indexPath);
5220
+ const lockPath = path11.join(canonicalIndexPath, "indexing.lock");
5221
+ cleanupDeadPublicationCandidates(canonicalIndexPath);
5222
+ for (let attempt = 0; attempt < 6; attempt += 1) {
5223
+ const owner = createOwner(operation);
5224
+ if (publishJsonDirectory(lockPath, owner)) {
5225
+ const lease = {
5226
+ canonicalIndexPath,
5227
+ lockPath,
5228
+ owner,
5229
+ recoveries: []
5230
+ };
5231
+ try {
5232
+ lease.recoveries = getPendingRecoveries(canonicalIndexPath);
5233
+ return lease;
5234
+ } catch (error) {
5235
+ releaseIndexLock(lease);
5236
+ throw error;
5237
+ }
5238
+ }
5239
+ let stats;
5240
+ try {
5241
+ stats = (0, import_fs7.lstatSync)(lockPath);
5242
+ } catch (error) {
5243
+ if (getErrorCode(error) === "ENOENT") continue;
5244
+ throw error;
5245
+ }
5246
+ if (!stats.isDirectory()) {
5247
+ const legacyOwner = readLegacyOwner(lockPath);
5248
+ throw new IndexLockContentionError(lockPath, legacyOwner, "legacy-lock");
5249
+ }
5250
+ const existingOwner = readDirectoryOwner(lockPath);
5251
+ if (!existingOwner) {
5252
+ throw new IndexLockContentionError(lockPath, null, "unknown-owner");
5253
+ }
5254
+ const liveness = getOwnerLiveness(existingOwner);
5255
+ if (liveness !== "dead") {
5256
+ throw new IndexLockContentionError(lockPath, existingOwner, liveness === "alive" ? "active" : "unknown-owner");
5257
+ }
5258
+ if (!reclaimDeadOwner(canonicalIndexPath, lockPath, existingOwner)) {
5259
+ throw new IndexLockContentionError(lockPath, existingOwner, "reclaiming");
5260
+ }
5261
+ }
5262
+ throw new IndexLockContentionError(lockPath, null, "reclaiming");
5263
+ }
5264
+ function releaseIndexLock(lease) {
5265
+ const currentOwner = readDirectoryOwner(lease.lockPath);
5266
+ if (!currentOwner || !sameOwner(currentOwner, lease.owner)) return false;
5267
+ const releasePath = `${lease.lockPath}.release.${lease.owner.pid}.${lease.owner.token}`;
5268
+ try {
5269
+ retryTransientFilesystemOperation(() => (0, import_fs7.renameSync)(lease.lockPath, releasePath));
5270
+ } catch (error) {
5271
+ if (getErrorCode(error) === "ENOENT") return false;
5272
+ throw error;
5273
+ }
5274
+ const claimedOwner = readDirectoryOwner(releasePath);
5275
+ if (!claimedOwner || !sameOwner(claimedOwner, lease.owner)) {
5276
+ if (!(0, import_fs7.existsSync)(lease.lockPath) && (0, import_fs7.existsSync)(releasePath)) {
5277
+ (0, import_fs7.renameSync)(releasePath, lease.lockPath);
5278
+ }
5279
+ return false;
5280
+ }
5281
+ try {
5282
+ retryTransientFilesystemOperation(() => (0, import_fs7.rmSync)(releasePath, { recursive: true, force: true }));
5283
+ } catch (error) {
5284
+ console.error(`[codebase-index] Lease released but tombstone cleanup failed: ${releasePath}`, error);
5285
+ }
5286
+ return true;
5287
+ }
5288
+ async function withIndexLock(indexPath, operation, callback, options = {}) {
5289
+ const lease = acquireIndexLock(indexPath, operation);
5290
+ let result;
5291
+ let callbackError;
5292
+ let callbackFailed = false;
5293
+ try {
5294
+ result = await callback(lease);
5295
+ } catch (error) {
5296
+ callbackFailed = true;
5297
+ callbackError = error;
5298
+ }
5299
+ if (!callbackFailed && options.completeRecoveries !== false) {
5300
+ try {
5301
+ completeLeaseRecovery(lease);
5302
+ } catch (error) {
5303
+ callbackFailed = true;
5304
+ callbackError = error;
5305
+ }
5306
+ }
5307
+ let releaseError;
5308
+ try {
5309
+ if (!releaseIndexLock(lease)) {
5310
+ releaseError = new Error(`Lost ownership of index mutation lease ${lease.owner.token}`);
5311
+ }
5312
+ } catch (error) {
5313
+ releaseError = error;
5314
+ }
5315
+ if (releaseError !== void 0) {
5316
+ if (callbackFailed) throw new AggregateError([callbackError, releaseError], "Index mutation and lease release both failed");
5317
+ throw releaseError;
5318
+ }
5319
+ if (callbackFailed) throw callbackError;
5320
+ return result;
5321
+ }
5322
+ function createLeaseTemporaryPath(targetPath, owner, kind = "tmp") {
5323
+ if (kind === "bak") return `${targetPath}.bak.${owner.pid}.${owner.token}`;
5324
+ temporaryCounter += 1;
5325
+ return `${targetPath}.tmp.${owner.pid}.${owner.token}.${temporaryCounter}`;
5326
+ }
5327
+ function removeLeaseTemporaryPath(temporaryPath) {
5328
+ if ((0, import_fs7.existsSync)(temporaryPath)) (0, import_fs7.rmSync)(temporaryPath, { recursive: true, force: true });
5329
+ }
5330
+ function recoverLeaseArtifacts(indexPath, owner, backupTargets) {
5331
+ for (const targetPath of backupTargets) {
5332
+ const backupPath = createLeaseTemporaryPath(targetPath, owner, "bak");
5333
+ if (!(0, import_fs7.existsSync)(backupPath)) continue;
5334
+ if ((0, import_fs7.existsSync)(targetPath)) (0, import_fs7.rmSync)(targetPath, { recursive: true, force: true });
5335
+ (0, import_fs7.renameSync)(backupPath, targetPath);
5336
+ }
5337
+ const temporaryOwnerMarker = `.tmp.${owner.pid}.${owner.token}.`;
5338
+ for (const entry of (0, import_fs7.readdirSync)(indexPath)) {
5339
+ if (!entry.includes(temporaryOwnerMarker)) continue;
5340
+ (0, import_fs7.rmSync)(path11.join(indexPath, entry), { recursive: true, force: true });
5341
+ }
5342
+ }
5343
+ function completeLeaseRecovery(lease) {
5344
+ for (const recovery of lease.recoveries) {
5345
+ const markerOwner = readRecoveryOwner(recovery.markerPath);
5346
+ if (!markerOwner || !sameOwner(markerOwner, recovery.owner)) {
5347
+ throw new Error(`Recovery marker ownership changed: ${recovery.markerPath}`);
5348
+ }
5349
+ }
5350
+ for (const recovery of lease.recoveries) {
5351
+ (0, import_fs7.rmSync)(recovery.markerPath, { recursive: true, force: true });
5352
+ }
5353
+ }
5354
+
4909
5355
  // src/indexer/index.ts
4910
5356
  var CALL_GRAPH_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "tsx", "javascript", "jsx", "python", "go", "rust", "php", "apex", "zig", "gdscript", "matlab", "bash"]);
4911
5357
  var CASE_INSENSITIVE_LANGUAGES = /* @__PURE__ */ new Set(["apex"]);
@@ -4987,6 +5433,7 @@ function isSqliteCorruptionError(error) {
4987
5433
  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");
4988
5434
  }
4989
5435
  var STARTUP_WARNING_METADATA_KEY = "index.startupWarning";
5436
+ var READER_ARTIFACT_RETRY_INTERVAL_MS = 1e3;
4990
5437
  function metadataFromBlame(blame) {
4991
5438
  if (!blame) {
4992
5439
  return {};
@@ -5184,9 +5631,9 @@ function hasAllEmbeddingParts(parts, expectedPartCount) {
5184
5631
  return true;
5185
5632
  }
5186
5633
  function isPathWithinRoot(filePath, rootPath) {
5187
- const normalizedFilePath = path11.resolve(filePath);
5188
- const normalizedRoot = path11.resolve(rootPath);
5189
- return normalizedFilePath === normalizedRoot || normalizedFilePath.startsWith(`${normalizedRoot}${path11.sep}`);
5634
+ const normalizedFilePath = path12.resolve(filePath);
5635
+ const normalizedRoot = path12.resolve(rootPath);
5636
+ return normalizedFilePath === normalizedRoot || normalizedFilePath.startsWith(`${normalizedRoot}${path12.sep}`);
5190
5637
  }
5191
5638
  var rankingQueryTokenCache = /* @__PURE__ */ new Map();
5192
5639
  var rankingNameTokenCache = /* @__PURE__ */ new Map();
@@ -6252,26 +6699,133 @@ var Indexer = class {
6252
6699
  queryCacheTtlMs = 5 * 60 * 1e3;
6253
6700
  querySimilarityThreshold = 0.85;
6254
6701
  indexCompatibility = null;
6255
- indexingLockPath = "";
6702
+ activeIndexLease = null;
6703
+ initializationPromise = null;
6704
+ initializationMode = "none";
6705
+ readIssues = [];
6706
+ retiredDatabases = [];
6707
+ readerArtifactFingerprint = null;
6708
+ writerArtifactFingerprint = null;
6709
+ readerArtifactRetryAfter = /* @__PURE__ */ new Map();
6256
6710
  constructor(projectRoot, config, host = "opencode") {
6257
6711
  this.projectRoot = projectRoot;
6258
6712
  this.config = config;
6259
6713
  this.host = host;
6260
6714
  this.indexPath = this.getIndexPath();
6261
- this.fileHashCachePath = path11.join(this.indexPath, "file-hashes.json");
6262
- this.failedBatchesPath = path11.join(this.indexPath, "failed-batches.json");
6263
- this.indexingLockPath = path11.join(this.indexPath, "indexing.lock");
6715
+ this.fileHashCachePath = path12.join(this.indexPath, "file-hashes.json");
6716
+ this.failedBatchesPath = path12.join(this.indexPath, "failed-batches.json");
6264
6717
  this.logger = initializeLogger(config.debug);
6265
6718
  }
6266
6719
  getIndexPath() {
6267
6720
  return resolveProjectIndexPath(this.projectRoot, this.config.scope, this.host);
6268
6721
  }
6722
+ isLocalProjectIndexPath() {
6723
+ const localProjectIndexPaths = [path12.join(this.projectRoot, getHostProjectIndexRelativePath(this.host))];
6724
+ if (this.host !== "opencode") {
6725
+ localProjectIndexPaths.push(path12.join(this.projectRoot, getHostProjectIndexRelativePath("opencode")));
6726
+ }
6727
+ return localProjectIndexPaths.some((localPath) => {
6728
+ if (!(0, import_fs8.existsSync)(localPath) || !(0, import_fs8.existsSync)(this.indexPath)) {
6729
+ return path12.resolve(this.indexPath) === path12.resolve(localPath);
6730
+ }
6731
+ const indexStats = (0, import_fs8.statSync)(this.indexPath);
6732
+ const localStats = (0, import_fs8.statSync)(localPath);
6733
+ return indexStats.dev === localStats.dev && indexStats.ino === localStats.ino;
6734
+ });
6735
+ }
6736
+ resetLoadedIndexState(retireDatabase = false) {
6737
+ if (this.database) {
6738
+ if (retireDatabase) {
6739
+ this.retiredDatabases.push(this.database);
6740
+ } else {
6741
+ this.database.close();
6742
+ }
6743
+ }
6744
+ this.store = null;
6745
+ this.invertedIndex = null;
6746
+ this.database = null;
6747
+ this.provider = null;
6748
+ this.configuredProviderInfo = null;
6749
+ this.reranker = null;
6750
+ this.indexCompatibility = null;
6751
+ this.initializationMode = "none";
6752
+ this.readIssues = [];
6753
+ this.readerArtifactFingerprint = null;
6754
+ this.writerArtifactFingerprint = null;
6755
+ this.readerArtifactRetryAfter.clear();
6756
+ this.fileHashCache.clear();
6757
+ }
6758
+ refreshLoadedIndexState() {
6759
+ if (!this.store || !this.invertedIndex || !this.configuredProviderInfo) return;
6760
+ this.store.load();
6761
+ this.invertedIndex.load();
6762
+ this.fileHashCache.clear();
6763
+ this.loadFileHashCache();
6764
+ this.indexCompatibility = this.validateIndexCompatibility(this.configuredProviderInfo);
6765
+ this.readIssues = [];
6766
+ this.readerArtifactRetryAfter.clear();
6767
+ }
6768
+ async withIndexMutationLease(operation, callback) {
6769
+ const lease = acquireIndexLock(this.indexPath, operation);
6770
+ this.indexPath = lease.canonicalIndexPath;
6771
+ this.fileHashCachePath = path12.join(this.indexPath, "file-hashes.json");
6772
+ this.failedBatchesPath = path12.join(this.indexPath, "failed-batches.json");
6773
+ this.activeIndexLease = lease;
6774
+ let result;
6775
+ let callbackError;
6776
+ let callbackFailed = false;
6777
+ try {
6778
+ result = await callback(lease.recoveries.map(({ owner }) => owner));
6779
+ } catch (error) {
6780
+ callbackFailed = true;
6781
+ callbackError = error;
6782
+ }
6783
+ if (!callbackFailed) {
6784
+ try {
6785
+ completeLeaseRecovery(lease);
6786
+ this.writerArtifactFingerprint = this.captureReaderArtifactFingerprint();
6787
+ } catch (error) {
6788
+ callbackFailed = true;
6789
+ callbackError = error;
6790
+ }
6791
+ }
6792
+ let releaseError;
6793
+ try {
6794
+ if (!releaseIndexLock(lease)) {
6795
+ releaseError = new Error(`Lost ownership of index mutation lease ${lease.owner.token}`);
6796
+ this.writerArtifactFingerprint = null;
6797
+ if (this.activeIndexLease?.owner.token === lease.owner.token) {
6798
+ this.activeIndexLease = null;
6799
+ }
6800
+ } else if (this.activeIndexLease?.owner.token === lease.owner.token) {
6801
+ this.activeIndexLease = null;
6802
+ }
6803
+ } catch (error) {
6804
+ releaseError = error;
6805
+ this.writerArtifactFingerprint = null;
6806
+ if (!(0, import_fs8.existsSync)(lease.lockPath) && this.activeIndexLease?.owner.token === lease.owner.token) {
6807
+ this.activeIndexLease = null;
6808
+ }
6809
+ }
6810
+ if (releaseError !== void 0) {
6811
+ if (callbackFailed) throw new AggregateError([callbackError, releaseError], "Index mutation and lease release both failed");
6812
+ throw releaseError;
6813
+ }
6814
+ if (callbackFailed) throw callbackError;
6815
+ return result;
6816
+ }
6817
+ requireActiveLease() {
6818
+ if (!this.activeIndexLease) {
6819
+ throw new Error("Index mutation attempted without an active interprocess lease");
6820
+ }
6821
+ return this.activeIndexLease;
6822
+ }
6269
6823
  loadFileHashCache() {
6270
- if (!(0, import_fs7.existsSync)(this.fileHashCachePath)) {
6824
+ if (!(0, import_fs8.existsSync)(this.fileHashCachePath)) {
6271
6825
  return;
6272
6826
  }
6273
6827
  try {
6274
- const data = (0, import_fs7.readFileSync)(this.fileHashCachePath, "utf-8");
6828
+ const data = (0, import_fs8.readFileSync)(this.fileHashCachePath, "utf-8");
6275
6829
  const parsed = JSON.parse(data);
6276
6830
  this.fileHashCache = new Map(Object.entries(parsed));
6277
6831
  } catch (error) {
@@ -6291,15 +6845,26 @@ var Indexer = class {
6291
6845
  this.atomicWriteSync(this.fileHashCachePath, JSON.stringify(obj));
6292
6846
  }
6293
6847
  atomicWriteSync(targetPath, data) {
6294
- const tempPath = `${targetPath}.tmp`;
6295
- (0, import_fs7.mkdirSync)(path11.dirname(targetPath), { recursive: true });
6296
- (0, import_fs7.writeFileSync)(tempPath, data);
6297
- (0, import_fs7.renameSync)(tempPath, targetPath);
6848
+ const lease = this.requireActiveLease();
6849
+ const tempPath = createLeaseTemporaryPath(targetPath, lease.owner, "tmp");
6850
+ (0, import_fs8.mkdirSync)(path12.dirname(targetPath), { recursive: true });
6851
+ try {
6852
+ (0, import_fs8.writeFileSync)(tempPath, data);
6853
+ (0, import_fs8.renameSync)(tempPath, targetPath);
6854
+ } finally {
6855
+ removeLeaseTemporaryPath(tempPath);
6856
+ }
6857
+ }
6858
+ saveInvertedIndex(invertedIndex) {
6859
+ this.atomicWriteSync(
6860
+ path12.join(this.indexPath, "inverted-index.json"),
6861
+ invertedIndex.serialize()
6862
+ );
6298
6863
  }
6299
6864
  getScopedRoots() {
6300
- const roots = /* @__PURE__ */ new Set([path11.resolve(this.projectRoot)]);
6865
+ const roots = /* @__PURE__ */ new Set([path12.resolve(this.projectRoot)]);
6301
6866
  for (const kbRoot of this.config.knowledgeBases) {
6302
- roots.add(path11.resolve(this.projectRoot, kbRoot));
6867
+ roots.add(path12.resolve(this.projectRoot, kbRoot));
6303
6868
  }
6304
6869
  return Array.from(roots);
6305
6870
  }
@@ -6308,29 +6873,29 @@ var Indexer = class {
6308
6873
  if (this.config.scope !== "global") {
6309
6874
  return branchName;
6310
6875
  }
6311
- const projectHash = hashContent(path11.resolve(this.projectRoot)).slice(0, 16);
6876
+ const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6312
6877
  return `${projectHash}:${branchName}`;
6313
6878
  }
6314
6879
  getBranchCatalogKeyFor(branchName) {
6315
6880
  if (this.config.scope !== "global") {
6316
6881
  return branchName;
6317
6882
  }
6318
- const projectHash = hashContent(path11.resolve(this.projectRoot)).slice(0, 16);
6883
+ const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6319
6884
  return `${projectHash}:${branchName}`;
6320
6885
  }
6321
6886
  getLegacyBranchCatalogKey() {
6322
6887
  return this.currentBranch || "default";
6323
6888
  }
6324
6889
  getLegacyMigrationMetadataKey() {
6325
- const projectHash = hashContent(path11.resolve(this.projectRoot)).slice(0, 16);
6890
+ const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6326
6891
  return `index.globalBranchMigration.${projectHash}`;
6327
6892
  }
6328
6893
  getProjectEmbeddingStrategyMetadataKey() {
6329
- const projectHash = hashContent(path11.resolve(this.projectRoot)).slice(0, 16);
6894
+ const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6330
6895
  return `index.embeddingStrategyVersion.${projectHash}`;
6331
6896
  }
6332
6897
  getProjectForceReembedMetadataKey() {
6333
- const projectHash = hashContent(path11.resolve(this.projectRoot)).slice(0, 16);
6898
+ const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6334
6899
  return `index.forceReembed.${projectHash}`;
6335
6900
  }
6336
6901
  hasProjectForceReembedPending() {
@@ -6424,7 +6989,7 @@ var Indexer = class {
6424
6989
  if (!this.database) {
6425
6990
  return { chunkIds, symbolIds };
6426
6991
  }
6427
- const projectRootPath = path11.resolve(this.projectRoot);
6992
+ const projectRootPath = path12.resolve(this.projectRoot);
6428
6993
  const projectLocalFilePaths = /* @__PURE__ */ new Set([
6429
6994
  ...Array.from(this.fileHashCache.keys()).filter(
6430
6995
  (filePath) => this.isFileInCurrentScope(filePath, roots) && isPathWithinRoot(filePath, projectRootPath)
@@ -6447,7 +7012,7 @@ var Indexer = class {
6447
7012
  if (this.config.scope !== "global") {
6448
7013
  return this.getBranchCatalogCleanupKeys();
6449
7014
  }
6450
- const projectHash = hashContent(path11.resolve(this.projectRoot)).slice(0, 16);
7015
+ const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6451
7016
  const keys = /* @__PURE__ */ new Set();
6452
7017
  const projectChunkIdSet = new Set(projectChunkIds);
6453
7018
  const projectSymbolIdSet = new Set(projectSymbolIds);
@@ -6531,7 +7096,7 @@ var Indexer = class {
6531
7096
  if (!this.database || this.config.scope !== "global") {
6532
7097
  return false;
6533
7098
  }
6534
- const projectHash = hashContent(path11.resolve(this.projectRoot)).slice(0, 16);
7099
+ const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6535
7100
  const roots = this.getScopedRoots();
6536
7101
  const { chunkIds: projectLocalChunkIds, symbolIds: projectLocalSymbolIds } = this.getProjectLocalScopedOwnershipIds(roots);
6537
7102
  return this.database.getAllBranches().some(
@@ -6565,7 +7130,7 @@ var Indexer = class {
6565
7130
  ...Array.from(this.fileHashCache.keys()).filter((filePath) => this.isFileInCurrentScope(filePath, roots)),
6566
7131
  ...scopedEntries.map(({ metadata }) => metadata.filePath)
6567
7132
  ]);
6568
- const projectRootPath = path11.resolve(this.projectRoot);
7133
+ const projectRootPath = path12.resolve(this.projectRoot);
6569
7134
  const projectLocalFilePaths = new Set(
6570
7135
  Array.from(filePaths).filter((filePath) => isPathWithinRoot(filePath, projectRootPath))
6571
7136
  );
@@ -6627,34 +7192,27 @@ var Indexer = class {
6627
7192
  database.gcOrphanEmbeddings();
6628
7193
  database.gcOrphanChunks();
6629
7194
  store.save();
6630
- invertedIndex.save();
7195
+ this.saveInvertedIndex(invertedIndex);
6631
7196
  return {
6632
7197
  removedChunkIds: removedChunkIdList,
6633
7198
  hasForeignData: allMetadata.some(({ metadata }) => !this.isFileInCurrentScope(metadata.filePath, roots))
6634
7199
  };
6635
7200
  }
6636
- checkForInterruptedIndexing() {
6637
- return (0, import_fs7.existsSync)(this.indexingLockPath);
6638
- }
6639
- acquireIndexingLock() {
6640
- const lockData = {
6641
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
6642
- pid: process.pid
6643
- };
6644
- (0, import_fs7.writeFileSync)(this.indexingLockPath, JSON.stringify(lockData));
6645
- }
6646
- releaseIndexingLock() {
6647
- if ((0, import_fs7.existsSync)(this.indexingLockPath)) {
6648
- (0, import_fs7.unlinkSync)(this.indexingLockPath);
7201
+ async recoverFromInterruptedIndexingUnlocked(owners) {
7202
+ for (const owner of owners) {
7203
+ this.logger.warn("Detected interrupted indexing session, recovering...", {
7204
+ pid: owner.pid,
7205
+ hostname: owner.hostname,
7206
+ operation: owner.operation,
7207
+ startedAt: owner.startedAt
7208
+ });
6649
7209
  }
6650
- }
6651
- async recoverFromInterruptedIndexing() {
6652
- this.logger.warn("Detected interrupted indexing session, recovering...");
6653
- if ((0, import_fs7.existsSync)(this.fileHashCachePath)) {
6654
- (0, import_fs7.unlinkSync)(this.fileHashCachePath);
7210
+ if (this.config.scope === "global") {
7211
+ if ((0, import_fs8.existsSync)(this.fileHashCachePath)) {
7212
+ (0, import_fs8.unlinkSync)(this.fileHashCachePath);
7213
+ }
7214
+ await this.healthCheckUnlocked();
6655
7215
  }
6656
- await this.healthCheck();
6657
- this.releaseIndexingLock();
6658
7216
  this.logger.info("Recovery complete, next index will re-process all files");
6659
7217
  }
6660
7218
  loadFailedBatches(maxChunkTokens) {
@@ -6670,10 +7228,10 @@ var Indexer = class {
6670
7228
  }
6671
7229
  }
6672
7230
  loadSerializedFailedBatches() {
6673
- if (!(0, import_fs7.existsSync)(this.failedBatchesPath)) {
7231
+ if (!(0, import_fs8.existsSync)(this.failedBatchesPath)) {
6674
7232
  return [];
6675
7233
  }
6676
- const data = (0, import_fs7.readFileSync)(this.failedBatchesPath, "utf-8");
7234
+ const data = (0, import_fs8.readFileSync)(this.failedBatchesPath, "utf-8");
6677
7235
  const parsed = JSON.parse(data);
6678
7236
  return parsed.map((batch) => {
6679
7237
  const chunks = Array.isArray(batch.chunks) ? batch.chunks : [];
@@ -6690,15 +7248,15 @@ var Indexer = class {
6690
7248
  }
6691
7249
  saveFailedBatches(batches) {
6692
7250
  if (batches.length === 0) {
6693
- if ((0, import_fs7.existsSync)(this.failedBatchesPath)) {
7251
+ if ((0, import_fs8.existsSync)(this.failedBatchesPath)) {
6694
7252
  try {
6695
- (0, import_fs7.unlinkSync)(this.failedBatchesPath);
7253
+ (0, import_fs8.unlinkSync)(this.failedBatchesPath);
6696
7254
  } catch {
6697
7255
  }
6698
7256
  }
6699
7257
  return;
6700
7258
  }
6701
- (0, import_fs7.writeFileSync)(this.failedBatchesPath, JSON.stringify(batches, null, 2));
7259
+ this.atomicWriteSync(this.failedBatchesPath, JSON.stringify(batches, null, 2));
6702
7260
  }
6703
7261
  collectRetryableFailedChunks(currentFileHashes, unchangedFilePaths, maxChunkTokens) {
6704
7262
  const retryableById = /* @__PURE__ */ new Map();
@@ -6878,7 +7436,7 @@ var Indexer = class {
6878
7436
  const intent = isLikelyImplementationPath(candidate.metadata.filePath) ? "implementation" : "doc_or_test";
6879
7437
  parts.push(`intent_hint: ${intent}`);
6880
7438
  try {
6881
- const fileContent = await import_fs7.promises.readFile(candidate.metadata.filePath, "utf-8");
7439
+ const fileContent = await import_fs8.promises.readFile(candidate.metadata.filePath, "utf-8");
6882
7440
  const lines = fileContent.split("\n");
6883
7441
  const snippetStartLine = Math.max(1, candidate.metadata.startLine - 2);
6884
7442
  const snippetEndLine = Math.min(lines.length, candidate.metadata.endLine + 2);
@@ -6892,6 +7450,222 @@ var Indexer = class {
6892
7450
  return parts.join("\n");
6893
7451
  }
6894
7452
  async initialize() {
7453
+ if (this.initializationPromise) {
7454
+ await this.initializationPromise;
7455
+ }
7456
+ if (this.isInitializedFor("reader")) {
7457
+ return;
7458
+ }
7459
+ await this.initializeOnce("reader", [], { skipAutoGc: true });
7460
+ }
7461
+ async initializeOnce(mode, recoveredOwners, options) {
7462
+ if (this.initializationPromise) {
7463
+ await this.initializationPromise;
7464
+ if (this.isInitializedFor(mode)) {
7465
+ return;
7466
+ }
7467
+ return this.initializeOnce(mode, recoveredOwners, options);
7468
+ }
7469
+ if (this.isInitializedFor(mode)) {
7470
+ return;
7471
+ }
7472
+ const initialization = this.initializeUnlocked(mode, recoveredOwners, options).catch((error) => {
7473
+ this.resetLoadedIndexState();
7474
+ throw error;
7475
+ }).finally(() => {
7476
+ if (this.initializationPromise === initialization) {
7477
+ this.initializationPromise = null;
7478
+ }
7479
+ });
7480
+ this.initializationPromise = initialization;
7481
+ await initialization;
7482
+ }
7483
+ isInitializedFor(mode) {
7484
+ const hasState = Boolean(
7485
+ this.store && this.provider && this.invertedIndex && this.configuredProviderInfo && this.database
7486
+ );
7487
+ if (!hasState) {
7488
+ return false;
7489
+ }
7490
+ return mode === "reader" ? this.initializationMode !== "none" : this.initializationMode === "writer";
7491
+ }
7492
+ recordReadIssue(component, message, error) {
7493
+ this.readIssues.push(this.createReadIssue(component, message));
7494
+ this.readerArtifactRetryAfter.set(component, Date.now() + READER_ARTIFACT_RETRY_INTERVAL_MS);
7495
+ this.logger.warn(message, error === void 0 ? void 0 : { error: getErrorMessage2(error) });
7496
+ }
7497
+ createReadIssue(component, message) {
7498
+ return {
7499
+ component,
7500
+ message,
7501
+ blocking: component !== "keyword"
7502
+ };
7503
+ }
7504
+ getVectorReadIssueMessage() {
7505
+ if (this.config.scope === "global") {
7506
+ return "Shared vector index could not be read. Restore or repair the complete fingerprinted shared vector artifacts; automatic reset is disabled for global scope.";
7507
+ }
7508
+ if (!this.isLocalProjectIndexPath()) {
7509
+ 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.";
7510
+ }
7511
+ 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.";
7512
+ }
7513
+ getKeywordReadIssueMessage() {
7514
+ if (this.config.scope === "global") {
7515
+ 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.";
7516
+ }
7517
+ if (!this.isLocalProjectIndexPath()) {
7518
+ 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.";
7519
+ }
7520
+ 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.";
7521
+ }
7522
+ getDatabaseReadIssueMessage() {
7523
+ if (this.config.scope === "global") {
7524
+ return "Shared index database could not be read. Restore or repair the shared SQLite database; automatic reset is disabled for global scope.";
7525
+ }
7526
+ if (!this.isLocalProjectIndexPath()) {
7527
+ 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.";
7528
+ }
7529
+ return "Index database could not be read. Run index_codebase after the active writer finishes to repair or migrate it under the writer lease.";
7530
+ }
7531
+ getReaderFileFingerprint(filePath, identityOnly = false) {
7532
+ try {
7533
+ const stats = (0, import_fs8.statSync)(filePath);
7534
+ if (identityOnly) {
7535
+ return `${stats.dev}:${stats.ino}`;
7536
+ }
7537
+ return `${stats.dev}:${stats.ino}:${stats.size}:${stats.mtimeMs}:${stats.ctimeMs}`;
7538
+ } catch (error) {
7539
+ return `unavailable:${getErrorMessage2(error)}`;
7540
+ }
7541
+ }
7542
+ captureReaderArtifactFingerprint() {
7543
+ const storePath = path12.join(this.indexPath, "vectors");
7544
+ return {
7545
+ vectors: `${this.getReaderFileFingerprint(storePath)}|${this.getReaderFileFingerprint(`${storePath}.meta.json`)}`,
7546
+ keyword: this.getReaderFileFingerprint(path12.join(this.indexPath, "inverted-index.json")),
7547
+ database: this.getReaderFileFingerprint(path12.join(this.indexPath, "codebase.db")),
7548
+ databaseIdentity: this.getReaderFileFingerprint(path12.join(this.indexPath, "codebase.db"), true)
7549
+ };
7550
+ }
7551
+ refreshReaderArtifacts() {
7552
+ if (this.initializationMode !== "reader" || !this.configuredProviderInfo) {
7553
+ return;
7554
+ }
7555
+ const previousFingerprint = this.readerArtifactFingerprint;
7556
+ const currentFingerprint = this.captureReaderArtifactFingerprint();
7557
+ const issues = new Map(this.readIssues.map((issue) => [issue.component, issue]));
7558
+ const retryDue = (component) => issues.has(component) && Date.now() >= (this.readerArtifactRetryAfter.get(component) ?? 0);
7559
+ const vectorsChanged = !previousFingerprint || currentFingerprint.vectors !== previousFingerprint.vectors;
7560
+ const keywordChanged = !previousFingerprint || currentFingerprint.keyword !== previousFingerprint.keyword;
7561
+ const databaseChanged = !previousFingerprint || currentFingerprint.database !== previousFingerprint.database;
7562
+ const databaseReplaced = !previousFingerprint || currentFingerprint.databaseIdentity !== previousFingerprint.databaseIdentity;
7563
+ if (previousFingerprint && !vectorsChanged && !keywordChanged && !databaseChanged && !Array.from(issues.keys()).some(retryDue)) {
7564
+ return;
7565
+ }
7566
+ const setIssue = (component, message, error) => {
7567
+ if (!issues.has(component)) {
7568
+ this.logger.warn(message, error === void 0 ? void 0 : { error: getErrorMessage2(error) });
7569
+ }
7570
+ issues.set(component, this.createReadIssue(component, message));
7571
+ this.readerArtifactRetryAfter.set(component, Date.now() + READER_ARTIFACT_RETRY_INTERVAL_MS);
7572
+ };
7573
+ const storePath = path12.join(this.indexPath, "vectors");
7574
+ const vectorMetadataPath = `${storePath}.meta.json`;
7575
+ const invertedIndexPath = path12.join(this.indexPath, "inverted-index.json");
7576
+ const dbPath = path12.join(this.indexPath, "codebase.db");
7577
+ if (vectorsChanged || retryDue("vectors")) {
7578
+ const vectorStoreExists = (0, import_fs8.existsSync)(storePath);
7579
+ const vectorMetadataExists = (0, import_fs8.existsSync)(vectorMetadataPath);
7580
+ if (vectorStoreExists && vectorMetadataExists) {
7581
+ try {
7582
+ const store = new VectorStore(storePath, this.configuredProviderInfo.modelInfo.dimensions);
7583
+ store.loadStrict();
7584
+ this.store = store;
7585
+ issues.delete("vectors");
7586
+ this.readerArtifactRetryAfter.delete("vectors");
7587
+ } catch (error) {
7588
+ setIssue("vectors", this.getVectorReadIssueMessage(), error);
7589
+ }
7590
+ } else if (vectorStoreExists !== vectorMetadataExists || issues.has("vectors")) {
7591
+ setIssue("vectors", this.getVectorReadIssueMessage());
7592
+ }
7593
+ }
7594
+ if (keywordChanged || retryDue("keyword") || !(0, import_fs8.existsSync)(invertedIndexPath) && (this.store?.count() ?? 0) > 0) {
7595
+ if ((0, import_fs8.existsSync)(invertedIndexPath)) {
7596
+ try {
7597
+ const invertedIndex = new InvertedIndex(invertedIndexPath);
7598
+ invertedIndex.load();
7599
+ this.invertedIndex = invertedIndex;
7600
+ issues.delete("keyword");
7601
+ this.readerArtifactRetryAfter.delete("keyword");
7602
+ } catch (error) {
7603
+ setIssue("keyword", this.getKeywordReadIssueMessage(), error);
7604
+ }
7605
+ } else if ((this.store?.count() ?? 0) > 0 || issues.has("keyword")) {
7606
+ setIssue("keyword", this.getKeywordReadIssueMessage());
7607
+ }
7608
+ }
7609
+ if (databaseReplaced || databaseChanged && issues.has("database") || retryDue("database")) {
7610
+ if ((0, import_fs8.existsSync)(dbPath)) {
7611
+ try {
7612
+ const database = Database.openReadOnly(dbPath);
7613
+ if (this.database) {
7614
+ this.retiredDatabases.push(this.database);
7615
+ }
7616
+ this.database = database;
7617
+ issues.delete("database");
7618
+ this.readerArtifactRetryAfter.delete("database");
7619
+ } catch (error) {
7620
+ setIssue("database", this.getDatabaseReadIssueMessage(), error);
7621
+ }
7622
+ } else if ((this.store?.count() ?? 0) > 0 || issues.has("database")) {
7623
+ setIssue("database", this.getDatabaseReadIssueMessage());
7624
+ }
7625
+ }
7626
+ if (!issues.has("database")) {
7627
+ try {
7628
+ this.indexCompatibility = this.validateIndexCompatibility(this.configuredProviderInfo);
7629
+ } catch (error) {
7630
+ setIssue("database", this.getDatabaseReadIssueMessage(), error);
7631
+ }
7632
+ }
7633
+ this.readIssues = Array.from(issues.values());
7634
+ this.readerArtifactFingerprint = currentFingerprint;
7635
+ }
7636
+ refreshInactiveWriterArtifacts() {
7637
+ if (this.initializationMode !== "writer" || this.activeIndexLease) {
7638
+ return true;
7639
+ }
7640
+ const previousFingerprint = this.writerArtifactFingerprint;
7641
+ const currentFingerprint = this.captureReaderArtifactFingerprint();
7642
+ const retryDue = this.readIssues.some(
7643
+ (issue) => Date.now() >= (this.readerArtifactRetryAfter.get(issue.component) ?? 0)
7644
+ );
7645
+ const artifactsChanged = !previousFingerprint || currentFingerprint.vectors !== previousFingerprint.vectors || currentFingerprint.keyword !== previousFingerprint.keyword || currentFingerprint.database !== previousFingerprint.database || currentFingerprint.databaseIdentity !== previousFingerprint.databaseIdentity;
7646
+ if (!artifactsChanged && !retryDue) {
7647
+ return true;
7648
+ }
7649
+ if (!previousFingerprint || currentFingerprint.databaseIdentity !== previousFingerprint.databaseIdentity) {
7650
+ return false;
7651
+ }
7652
+ this.initializationMode = "reader";
7653
+ this.readerArtifactFingerprint = previousFingerprint;
7654
+ try {
7655
+ this.refreshReaderArtifacts();
7656
+ this.writerArtifactFingerprint = this.readerArtifactFingerprint ?? currentFingerprint;
7657
+ } finally {
7658
+ this.readerArtifactFingerprint = null;
7659
+ this.initializationMode = "writer";
7660
+ }
7661
+ return true;
7662
+ }
7663
+ async initializeUnlocked(mode, recoveredOwners = [], options = {}) {
7664
+ if (mode === "writer") {
7665
+ this.requireActiveLease();
7666
+ }
7667
+ this.readIssues = [];
7668
+ this.readerArtifactRetryAfter.clear();
6895
7669
  if (this.config.embeddingProvider === "custom") {
6896
7670
  if (!this.config.customProvider) {
6897
7671
  throw new Error("embeddingProvider is 'custom' but customProvider config is missing.");
@@ -6923,36 +7697,103 @@ var Indexer = class {
6923
7697
  });
6924
7698
  }
6925
7699
  }
6926
- await import_fs7.promises.mkdir(this.indexPath, { recursive: true });
6927
7700
  const dimensions = this.configuredProviderInfo.modelInfo.dimensions;
6928
- const storePath = path11.join(this.indexPath, "vectors");
6929
- this.store = new VectorStore(storePath, dimensions);
6930
- const indexFilePath = path11.join(this.indexPath, "vectors.usearch");
6931
- if ((0, import_fs7.existsSync)(indexFilePath)) {
6932
- this.store.load();
6933
- }
6934
- const invertedIndexPath = path11.join(this.indexPath, "inverted-index.json");
6935
- this.invertedIndex = new InvertedIndex(invertedIndexPath);
6936
- try {
6937
- this.invertedIndex.load();
6938
- } catch {
6939
- if ((0, import_fs7.existsSync)(invertedIndexPath)) {
6940
- await import_fs7.promises.unlink(invertedIndexPath);
7701
+ const storePath = path12.join(this.indexPath, "vectors");
7702
+ const vectorMetadataPath = `${storePath}.meta.json`;
7703
+ const invertedIndexPath = path12.join(this.indexPath, "inverted-index.json");
7704
+ const dbPath = path12.join(this.indexPath, "codebase.db");
7705
+ let dbIsNew = !(0, import_fs8.existsSync)(dbPath);
7706
+ const readerArtifactFingerprint = mode === "reader" ? this.captureReaderArtifactFingerprint() : null;
7707
+ if (mode === "writer") {
7708
+ await import_fs8.promises.mkdir(this.indexPath, { recursive: true });
7709
+ if (recoveredOwners.length > 0 && this.config.scope === "project" && !this.isLocalProjectIndexPath()) {
7710
+ throw new Error(
7711
+ "Interrupted indexing recovery is unsafe while using an inherited worktree index. Run index_codebase with force=true to create a local project index boundary."
7712
+ );
7713
+ }
7714
+ for (const recoveredOwner of recoveredOwners) {
7715
+ recoverLeaseArtifacts(this.indexPath, recoveredOwner, [
7716
+ storePath,
7717
+ `${storePath}.meta.json`
7718
+ ]);
7719
+ }
7720
+ if (recoveredOwners.length > 0 && this.config.scope === "project") {
7721
+ await this.resetLocalIndexArtifacts();
7722
+ }
7723
+ this.store = new VectorStore(storePath, dimensions);
7724
+ if ((0, import_fs8.existsSync)(storePath) || (0, import_fs8.existsSync)(vectorMetadataPath)) {
7725
+ this.store.load();
6941
7726
  }
6942
7727
  this.invertedIndex = new InvertedIndex(invertedIndexPath);
6943
- }
6944
- const dbPath = path11.join(this.indexPath, "codebase.db");
6945
- let dbIsNew = !(0, import_fs7.existsSync)(dbPath);
6946
- try {
6947
- this.database = new Database(dbPath);
6948
- } catch (error) {
6949
- if (!await this.tryResetCorruptedIndex("initializing index database", error)) {
6950
- throw error;
7728
+ try {
7729
+ this.invertedIndex.load();
7730
+ } catch {
7731
+ if ((0, import_fs8.existsSync)(invertedIndexPath)) {
7732
+ await import_fs8.promises.unlink(invertedIndexPath);
7733
+ }
7734
+ this.invertedIndex = new InvertedIndex(invertedIndexPath);
7735
+ }
7736
+ try {
7737
+ this.database = new Database(dbPath);
7738
+ } catch (error) {
7739
+ if (!await this.tryResetCorruptedIndex("initializing index database", error)) {
7740
+ throw error;
7741
+ }
7742
+ this.store = new VectorStore(storePath, dimensions);
7743
+ this.invertedIndex = new InvertedIndex(invertedIndexPath);
7744
+ this.database = new Database(dbPath);
7745
+ dbIsNew = true;
6951
7746
  }
7747
+ } else {
6952
7748
  this.store = new VectorStore(storePath, dimensions);
7749
+ const vectorStoreExists = (0, import_fs8.existsSync)(storePath);
7750
+ const vectorMetadataExists = (0, import_fs8.existsSync)(vectorMetadataPath);
7751
+ const vectorReadFailureMessage = this.getVectorReadIssueMessage();
7752
+ if (vectorStoreExists !== vectorMetadataExists) {
7753
+ this.recordReadIssue("vectors", vectorReadFailureMessage);
7754
+ } else if (vectorStoreExists) {
7755
+ try {
7756
+ this.store.loadStrict();
7757
+ } catch (error) {
7758
+ this.recordReadIssue("vectors", vectorReadFailureMessage, error);
7759
+ this.store = new VectorStore(storePath, dimensions);
7760
+ }
7761
+ }
6953
7762
  this.invertedIndex = new InvertedIndex(invertedIndexPath);
6954
- this.database = new Database(dbPath);
6955
- dbIsNew = true;
7763
+ if ((0, import_fs8.existsSync)(invertedIndexPath)) {
7764
+ try {
7765
+ this.invertedIndex.load();
7766
+ } catch (error) {
7767
+ this.recordReadIssue(
7768
+ "keyword",
7769
+ this.getKeywordReadIssueMessage(),
7770
+ error
7771
+ );
7772
+ this.invertedIndex = new InvertedIndex(invertedIndexPath);
7773
+ }
7774
+ } else if (this.store.count() > 0) {
7775
+ this.recordReadIssue("keyword", this.getKeywordReadIssueMessage());
7776
+ }
7777
+ if ((0, import_fs8.existsSync)(dbPath)) {
7778
+ try {
7779
+ this.database = Database.openReadOnly(dbPath);
7780
+ } catch (error) {
7781
+ this.recordReadIssue(
7782
+ "database",
7783
+ this.getDatabaseReadIssueMessage(),
7784
+ error
7785
+ );
7786
+ this.database = Database.createEmptyReadOnly();
7787
+ }
7788
+ } else {
7789
+ this.database = Database.createEmptyReadOnly();
7790
+ if (this.store.count() > 0) {
7791
+ this.recordReadIssue(
7792
+ "database",
7793
+ `Index database is missing for the published vectors. ${this.getDatabaseReadIssueMessage()}`
7794
+ );
7795
+ }
7796
+ }
6956
7797
  }
6957
7798
  if (isGitRepo(this.projectRoot)) {
6958
7799
  this.currentBranch = getBranchOrDefault(this.projectRoot);
@@ -6966,10 +7807,10 @@ var Indexer = class {
6966
7807
  this.baseBranch = "default";
6967
7808
  this.logger.branch("debug", "Not a git repository, using default branch");
6968
7809
  }
6969
- if (this.checkForInterruptedIndexing()) {
6970
- await this.recoverFromInterruptedIndexing();
7810
+ if (mode === "writer" && recoveredOwners.length > 0) {
7811
+ await this.recoverFromInterruptedIndexingUnlocked(recoveredOwners);
6971
7812
  }
6972
- if (dbIsNew && this.store.count() > 0) {
7813
+ if (mode === "writer" && dbIsNew && this.store.count() > 0) {
6973
7814
  this.migrateFromLegacyIndex();
6974
7815
  }
6975
7816
  this.loadFileHashCache();
@@ -6981,9 +7822,11 @@ var Indexer = class {
6981
7822
  configuredProviderInfo: this.configuredProviderInfo
6982
7823
  });
6983
7824
  }
6984
- if (this.config.indexing.autoGc) {
7825
+ if (mode === "writer" && this.config.indexing.autoGc && !options.skipAutoGc) {
6985
7826
  await this.maybeRunAutoGc();
6986
7827
  }
7828
+ this.initializationMode = mode;
7829
+ this.readerArtifactFingerprint = readerArtifactFingerprint;
6987
7830
  }
6988
7831
  async maybeRunAutoGc() {
6989
7832
  if (!this.database) return;
@@ -7000,7 +7843,7 @@ var Indexer = class {
7000
7843
  }
7001
7844
  }
7002
7845
  if (shouldRunGc) {
7003
- const result = await this.healthCheck();
7846
+ const result = await this.healthCheckUnlocked();
7004
7847
  if (result.warning) {
7005
7848
  this.database.setMetadata(STARTUP_WARNING_METADATA_KEY, result.warning);
7006
7849
  } else {
@@ -7022,7 +7865,7 @@ var Indexer = class {
7022
7865
  if (await this.tryResetCorruptedIndex("running automatic orphan garbage collection", error)) {
7023
7866
  return {
7024
7867
  resetCorruptedIndex: true,
7025
- warning: this.getCorruptedIndexWarning(path11.join(this.indexPath, "codebase.db"))
7868
+ warning: this.getCorruptedIndexWarning(path12.join(this.indexPath, "codebase.db"))
7026
7869
  };
7027
7870
  }
7028
7871
  throw error;
@@ -7037,28 +7880,29 @@ var Indexer = class {
7037
7880
  return;
7038
7881
  }
7039
7882
  const retainedEntries = store.getAllMetadata().filter(({ key }) => !excludedSet.has(key));
7040
- const storeBasePath = path11.join(this.indexPath, "vectors");
7041
- const storeIndexPath = `${storeBasePath}.usearch`;
7883
+ const storeBasePath = path12.join(this.indexPath, "vectors");
7884
+ const storeIndexPath = storeBasePath;
7042
7885
  const storeMetadataPath = `${storeBasePath}.meta.json`;
7043
- const backupIndexPath = `${storeIndexPath}.bak`;
7044
- const backupMetadataPath = `${storeMetadataPath}.bak`;
7886
+ const lease = this.requireActiveLease();
7887
+ const backupIndexPath = createLeaseTemporaryPath(storeIndexPath, lease.owner, "bak");
7888
+ const backupMetadataPath = createLeaseTemporaryPath(storeMetadataPath, lease.owner, "bak");
7045
7889
  let backedUpIndex = false;
7046
7890
  let backedUpMetadata = false;
7047
7891
  let rebuiltCount = 0;
7048
7892
  let skippedCount = 0;
7049
- if ((0, import_fs7.existsSync)(backupIndexPath)) {
7050
- (0, import_fs7.unlinkSync)(backupIndexPath);
7893
+ if ((0, import_fs8.existsSync)(backupIndexPath)) {
7894
+ (0, import_fs8.unlinkSync)(backupIndexPath);
7051
7895
  }
7052
- if ((0, import_fs7.existsSync)(backupMetadataPath)) {
7053
- (0, import_fs7.unlinkSync)(backupMetadataPath);
7896
+ if ((0, import_fs8.existsSync)(backupMetadataPath)) {
7897
+ (0, import_fs8.unlinkSync)(backupMetadataPath);
7054
7898
  }
7055
7899
  try {
7056
- if ((0, import_fs7.existsSync)(storeIndexPath)) {
7057
- (0, import_fs7.renameSync)(storeIndexPath, backupIndexPath);
7900
+ if ((0, import_fs8.existsSync)(storeIndexPath)) {
7901
+ (0, import_fs8.renameSync)(storeIndexPath, backupIndexPath);
7058
7902
  backedUpIndex = true;
7059
7903
  }
7060
- if ((0, import_fs7.existsSync)(storeMetadataPath)) {
7061
- (0, import_fs7.renameSync)(storeMetadataPath, backupMetadataPath);
7904
+ if ((0, import_fs8.existsSync)(storeMetadataPath)) {
7905
+ (0, import_fs8.renameSync)(storeMetadataPath, backupMetadataPath);
7062
7906
  backedUpMetadata = true;
7063
7907
  }
7064
7908
  store.clear();
@@ -7078,11 +7922,11 @@ var Indexer = class {
7078
7922
  rebuiltCount += 1;
7079
7923
  }
7080
7924
  store.save();
7081
- if (backedUpIndex && (0, import_fs7.existsSync)(backupIndexPath)) {
7082
- (0, import_fs7.unlinkSync)(backupIndexPath);
7925
+ if (backedUpIndex && (0, import_fs8.existsSync)(backupIndexPath)) {
7926
+ (0, import_fs8.unlinkSync)(backupIndexPath);
7083
7927
  }
7084
- if (backedUpMetadata && (0, import_fs7.existsSync)(backupMetadataPath)) {
7085
- (0, import_fs7.unlinkSync)(backupMetadataPath);
7928
+ if (backedUpMetadata && (0, import_fs8.existsSync)(backupMetadataPath)) {
7929
+ (0, import_fs8.unlinkSync)(backupMetadataPath);
7086
7930
  }
7087
7931
  this.logger.gc("info", "Rebuilt vector store to avoid native remove", {
7088
7932
  excludedChunks: excludedSet.size,
@@ -7094,17 +7938,17 @@ var Indexer = class {
7094
7938
  store.clear();
7095
7939
  } catch {
7096
7940
  }
7097
- if ((0, import_fs7.existsSync)(storeIndexPath)) {
7098
- (0, import_fs7.unlinkSync)(storeIndexPath);
7941
+ if ((0, import_fs8.existsSync)(storeIndexPath)) {
7942
+ (0, import_fs8.unlinkSync)(storeIndexPath);
7099
7943
  }
7100
- if ((0, import_fs7.existsSync)(storeMetadataPath)) {
7101
- (0, import_fs7.unlinkSync)(storeMetadataPath);
7944
+ if ((0, import_fs8.existsSync)(storeMetadataPath)) {
7945
+ (0, import_fs8.unlinkSync)(storeMetadataPath);
7102
7946
  }
7103
- if (backedUpIndex && (0, import_fs7.existsSync)(backupIndexPath)) {
7104
- (0, import_fs7.renameSync)(backupIndexPath, storeIndexPath);
7947
+ if (backedUpIndex && (0, import_fs8.existsSync)(backupIndexPath)) {
7948
+ (0, import_fs8.renameSync)(backupIndexPath, storeIndexPath);
7105
7949
  }
7106
- if (backedUpMetadata && (0, import_fs7.existsSync)(backupMetadataPath)) {
7107
- (0, import_fs7.renameSync)(backupMetadataPath, storeMetadataPath);
7950
+ if (backedUpMetadata && (0, import_fs8.existsSync)(backupMetadataPath)) {
7951
+ (0, import_fs8.renameSync)(backupMetadataPath, storeMetadataPath);
7108
7952
  }
7109
7953
  if (backedUpIndex || backedUpMetadata) {
7110
7954
  store.load();
@@ -7118,11 +7962,37 @@ var Indexer = class {
7118
7962
  }
7119
7963
  return `Detected a corrupted local SQLite index at ${dbPath} and reset the local index. Run index_codebase to rebuild search data.`;
7120
7964
  }
7965
+ async resetLocalIndexArtifacts() {
7966
+ this.store = null;
7967
+ this.invertedIndex = null;
7968
+ this.database?.close();
7969
+ this.database = null;
7970
+ this.indexCompatibility = null;
7971
+ this.initializationMode = "none";
7972
+ this.readIssues = [];
7973
+ this.readerArtifactFingerprint = null;
7974
+ this.writerArtifactFingerprint = null;
7975
+ this.readerArtifactRetryAfter.clear();
7976
+ this.fileHashCache.clear();
7977
+ const resetPaths = [
7978
+ path12.join(this.indexPath, "codebase.db"),
7979
+ path12.join(this.indexPath, "codebase.db-shm"),
7980
+ path12.join(this.indexPath, "codebase.db-wal"),
7981
+ path12.join(this.indexPath, "vectors"),
7982
+ path12.join(this.indexPath, "vectors.usearch"),
7983
+ path12.join(this.indexPath, "vectors.meta.json"),
7984
+ path12.join(this.indexPath, "inverted-index.json"),
7985
+ path12.join(this.indexPath, "file-hashes.json"),
7986
+ path12.join(this.indexPath, "failed-batches.json")
7987
+ ];
7988
+ await Promise.all(resetPaths.map((targetPath) => import_fs8.promises.rm(targetPath, { recursive: true, force: true })));
7989
+ await import_fs8.promises.mkdir(this.indexPath, { recursive: true });
7990
+ }
7121
7991
  async tryResetCorruptedIndex(stage, error) {
7122
7992
  if (!isSqliteCorruptionError(error)) {
7123
7993
  return false;
7124
7994
  }
7125
- const dbPath = path11.join(this.indexPath, "codebase.db");
7995
+ const dbPath = path12.join(this.indexPath, "codebase.db");
7126
7996
  const warning = this.getCorruptedIndexWarning(dbPath);
7127
7997
  const errorMessage = getErrorMessage2(error);
7128
7998
  if (this.config.scope === "global") {
@@ -7138,30 +8008,7 @@ var Indexer = class {
7138
8008
  dbPath,
7139
8009
  error: errorMessage
7140
8010
  });
7141
- this.store = null;
7142
- this.invertedIndex = null;
7143
- this.database?.close();
7144
- this.database = null;
7145
- this.indexCompatibility = null;
7146
- this.fileHashCache.clear();
7147
- const resetPaths = [
7148
- path11.join(this.indexPath, "codebase.db"),
7149
- path11.join(this.indexPath, "codebase.db-shm"),
7150
- path11.join(this.indexPath, "codebase.db-wal"),
7151
- path11.join(this.indexPath, "vectors.usearch"),
7152
- path11.join(this.indexPath, "inverted-index.json"),
7153
- path11.join(this.indexPath, "file-hashes.json"),
7154
- path11.join(this.indexPath, "failed-batches.json"),
7155
- path11.join(this.indexPath, "indexing.lock"),
7156
- path11.join(this.indexPath, "vectors")
7157
- ];
7158
- await Promise.all(resetPaths.map(async (targetPath) => {
7159
- try {
7160
- await import_fs7.promises.rm(targetPath, { recursive: true, force: true });
7161
- } catch {
7162
- }
7163
- }));
7164
- await import_fs7.promises.mkdir(this.indexPath, { recursive: true });
8011
+ await this.resetLocalIndexArtifacts();
7165
8012
  return true;
7166
8013
  }
7167
8014
  migrateFromLegacyIndex() {
@@ -7280,8 +8127,62 @@ var Indexer = class {
7280
8127
  return this.indexCompatibility;
7281
8128
  }
7282
8129
  async ensureInitialized() {
8130
+ let initializedReader = false;
8131
+ while (true) {
8132
+ if (this.initializationPromise) {
8133
+ await this.initializationPromise;
8134
+ }
8135
+ if (!this.isInitializedFor("reader")) {
8136
+ await this.initialize();
8137
+ initializedReader = true;
8138
+ continue;
8139
+ }
8140
+ if (this.initializationMode === "writer" && !this.activeIndexLease && !initializedReader) {
8141
+ if (!this.refreshInactiveWriterArtifacts()) {
8142
+ this.resetLoadedIndexState(true);
8143
+ await this.initialize();
8144
+ initializedReader = true;
8145
+ continue;
8146
+ }
8147
+ }
8148
+ if (this.initializationMode === "reader" && !initializedReader) {
8149
+ this.refreshReaderArtifacts();
8150
+ }
8151
+ const state = this.requireLoadedIndexState();
8152
+ return {
8153
+ ...state,
8154
+ readIssues: [...this.readIssues],
8155
+ compatibility: this.indexCompatibility ?? this.validateIndexCompatibility(state.configuredProviderInfo)
8156
+ };
8157
+ }
8158
+ }
8159
+ async ensureInitializedUnlocked(recoveredOwners = []) {
8160
+ this.requireActiveLease();
8161
+ if (this.initializationPromise) {
8162
+ await this.initializationPromise;
8163
+ }
8164
+ if (recoveredOwners.length > 0 || !this.isInitializedFor("writer")) {
8165
+ const retireReaderDatabase = this.initializationMode === "reader";
8166
+ this.resetLoadedIndexState(retireReaderDatabase);
8167
+ await this.initializeOnce("writer", recoveredOwners, { skipAutoGc: true });
8168
+ } else {
8169
+ this.refreshLoadedIndexState();
8170
+ }
8171
+ if (this.config.indexing.autoGc) {
8172
+ await this.maybeRunAutoGc();
8173
+ }
8174
+ return this.requireLoadedIndexState();
8175
+ }
8176
+ requireReadableComponents(readIssues, ...components) {
8177
+ const componentSet = new Set(components);
8178
+ const issues = readIssues.filter((issue) => issue.blocking && componentSet.has(issue.component));
8179
+ if (issues.length > 0) {
8180
+ throw new Error(issues.map((issue) => issue.message).join(" "));
8181
+ }
8182
+ }
8183
+ requireLoadedIndexState() {
7283
8184
  if (!this.store || !this.provider || !this.invertedIndex || !this.configuredProviderInfo || !this.database) {
7284
- await this.initialize();
8185
+ throw new Error("Index state is not initialized");
7285
8186
  }
7286
8187
  return {
7287
8188
  store: this.store,
@@ -7305,7 +8206,12 @@ var Indexer = class {
7305
8206
  return createCostEstimate(files, configuredProviderInfo);
7306
8207
  }
7307
8208
  async index(onProgress) {
7308
- const { store, provider, invertedIndex, database, configuredProviderInfo } = await this.ensureInitialized();
8209
+ return this.withIndexMutationLease("index", async (recoveredOwners) => {
8210
+ return this.indexUnlocked(onProgress, recoveredOwners);
8211
+ });
8212
+ }
8213
+ async indexUnlocked(onProgress, recoveredOwners = [], stateReady = false) {
8214
+ const { store, provider, invertedIndex, database, configuredProviderInfo } = stateReady ? this.requireLoadedIndexState() : await this.ensureInitializedUnlocked(recoveredOwners);
7309
8215
  const scopedRoots = this.config.scope === "global" ? this.getScopedRoots() : null;
7310
8216
  const branchCatalogKey = this.getBranchCatalogKey();
7311
8217
  const forceScopedReembed = scopedRoots !== null && database.getMetadata(this.getProjectForceReembedMetadataKey()) === "true";
@@ -7315,7 +8221,6 @@ var Indexer = class {
7315
8221
  `${this.indexCompatibility?.reason} Run index_codebase with force=true to rebuild the index.`
7316
8222
  );
7317
8223
  }
7318
- this.acquireIndexingLock();
7319
8224
  this.logger.recordIndexingStart();
7320
8225
  this.logger.info("Starting indexing", { projectRoot: this.projectRoot });
7321
8226
  const startTime = Date.now();
@@ -7366,7 +8271,7 @@ var Indexer = class {
7366
8271
  unchangedFilePaths.add(f.path);
7367
8272
  this.logger.recordCacheHit();
7368
8273
  } else {
7369
- const content = await import_fs7.promises.readFile(f.path, "utf-8");
8274
+ const content = await import_fs8.promises.readFile(f.path, "utf-8");
7370
8275
  changedFiles.push({ path: f.path, content, hash: currentHash });
7371
8276
  this.logger.recordCacheMiss();
7372
8277
  }
@@ -7464,7 +8369,7 @@ var Indexer = class {
7464
8369
  for (const parsed of parsedFiles) {
7465
8370
  currentFilePaths.add(parsed.path);
7466
8371
  if (parsed.chunks.length === 0) {
7467
- const relativePath = path11.relative(this.projectRoot, parsed.path);
8372
+ const relativePath = path12.relative(this.projectRoot, parsed.path);
7468
8373
  stats.parseFailures.push(relativePath);
7469
8374
  }
7470
8375
  let fileChunkCount = 0;
@@ -7664,7 +8569,9 @@ var Indexer = class {
7664
8569
  database.addChunksToBranchBatch(branchCatalogKey, Array.from(currentChunkIds));
7665
8570
  database.clearBranchSymbols(branchCatalogKey);
7666
8571
  database.addSymbolsToBranchBatch(branchCatalogKey, Array.from(allSymbolIds));
7667
- if (backfilledBlameMetadata) {
8572
+ const vectorPath = path12.join(this.indexPath, "vectors");
8573
+ const shouldFingerprintLegacyPair = !store.hasFingerprint() && (0, import_fs8.existsSync)(vectorPath) && (0, import_fs8.existsSync)(`${vectorPath}.meta.json`);
8574
+ if (backfilledBlameMetadata || shouldFingerprintLegacyPair) {
7668
8575
  store.save();
7669
8576
  }
7670
8577
  if (scopedRoots) {
@@ -7685,7 +8592,6 @@ var Indexer = class {
7685
8592
  chunksProcessed: 0,
7686
8593
  totalChunks: 0
7687
8594
  });
7688
- this.releaseIndexingLock();
7689
8595
  return stats;
7690
8596
  }
7691
8597
  if (pendingChunks.length === 0) {
@@ -7694,7 +8600,7 @@ var Indexer = class {
7694
8600
  database.clearBranchSymbols(branchCatalogKey);
7695
8601
  database.addSymbolsToBranchBatch(branchCatalogKey, Array.from(allSymbolIds));
7696
8602
  store.save();
7697
- invertedIndex.save();
8603
+ this.saveInvertedIndex(invertedIndex);
7698
8604
  if (scopedRoots) {
7699
8605
  this.replaceScopedFileHashCache(currentFileHashes, scopedRoots);
7700
8606
  this.clearScopedFailedBatches(scopedRoots);
@@ -7713,7 +8619,6 @@ var Indexer = class {
7713
8619
  chunksProcessed: 0,
7714
8620
  totalChunks: 0
7715
8621
  });
7716
- this.releaseIndexingLock();
7717
8622
  return stats;
7718
8623
  }
7719
8624
  onProgress?.({
@@ -7944,7 +8849,7 @@ var Indexer = class {
7944
8849
  database.clearBranchSymbols(branchCatalogKey);
7945
8850
  database.addSymbolsToBranchBatch(branchCatalogKey, Array.from(allSymbolIds));
7946
8851
  store.save();
7947
- invertedIndex.save();
8852
+ this.saveInvertedIndex(invertedIndex);
7948
8853
  if (scopedRoots) {
7949
8854
  this.replaceScopedFileHashCache(currentFileHashes, scopedRoots);
7950
8855
  } else {
@@ -7996,7 +8901,6 @@ var Indexer = class {
7996
8901
  chunksProcessed: stats.indexedChunks,
7997
8902
  totalChunks: pendingChunks.length
7998
8903
  });
7999
- this.releaseIndexingLock();
8000
8904
  return stats;
8001
8905
  }
8002
8906
  async getQueryEmbedding(query, provider) {
@@ -8062,8 +8966,8 @@ var Indexer = class {
8062
8966
  return intersection / union;
8063
8967
  }
8064
8968
  async search(query, limit, options) {
8065
- const { store, provider, database } = await this.ensureInitialized();
8066
- const compatibility = this.checkCompatibility();
8969
+ const { store, provider, invertedIndex, database, readIssues, compatibility } = await this.ensureInitialized();
8970
+ this.requireReadableComponents(readIssues, "vectors", "database");
8067
8971
  if (!compatibility.compatible) {
8068
8972
  throw new Error(
8069
8973
  `${compatibility.reason ?? "Index is incompatible with current embedding provider."} A possible solution is to run index_codebase with force=true to rebuild the index.`
@@ -8077,6 +8981,7 @@ var Indexer = class {
8077
8981
  const maxResults = limit ?? this.config.search.maxResults;
8078
8982
  const hybridWeight = options?.hybridWeight ?? this.config.search.hybridWeight;
8079
8983
  const fusionStrategy = this.config.search.fusionStrategy;
8984
+ const effectiveHybridWeight = fusionStrategy === "weighted" && readIssues.some((issue) => issue.component === "keyword") ? 0 : hybridWeight;
8080
8985
  const rrfK = this.config.search.rrfK;
8081
8986
  const rerankTopN = this.config.search.rerankTopN;
8082
8987
  const filterByBranch = options?.filterByBranch ?? true;
@@ -8085,7 +8990,7 @@ var Indexer = class {
8085
8990
  this.logger.search("debug", "Starting search", {
8086
8991
  query,
8087
8992
  maxResults,
8088
- hybridWeight,
8993
+ hybridWeight: effectiveHybridWeight,
8089
8994
  fusionStrategy,
8090
8995
  rrfK,
8091
8996
  rerankTopN,
@@ -8093,13 +8998,22 @@ var Indexer = class {
8093
8998
  });
8094
8999
  const embeddingStartTime = import_perf_hooks.performance.now();
8095
9000
  const embeddingQuery = stripFilePathHint(query);
8096
- const embedding = await this.getQueryEmbedding(embeddingQuery, provider);
9001
+ let embedding;
9002
+ try {
9003
+ embedding = await this.getQueryEmbedding(embeddingQuery, provider);
9004
+ } catch (error) {
9005
+ this.logger.warn("Query embedding failed; falling back to keyword-only search", {
9006
+ query,
9007
+ error: getErrorMessage2(error),
9008
+ action: "Check the embedding provider configuration and retry search after restoring provider health."
9009
+ });
9010
+ }
8097
9011
  const embeddingMs = import_perf_hooks.performance.now() - embeddingStartTime;
8098
9012
  const vectorStartTime = import_perf_hooks.performance.now();
8099
- const semanticResults = store.search(embedding, maxResults * 4);
9013
+ const semanticResults = embedding ? store.search(embedding, maxResults * 4) : [];
8100
9014
  const vectorMs = import_perf_hooks.performance.now() - vectorStartTime;
8101
9015
  const keywordStartTime = import_perf_hooks.performance.now();
8102
- const keywordResults = await this.keywordSearch(query, maxResults * 4);
9016
+ const keywordResults = await this.keywordSearch(query, maxResults * 4, store, invertedIndex);
8103
9017
  const keywordMs = import_perf_hooks.performance.now() - keywordStartTime;
8104
9018
  let branchChunkIds = null;
8105
9019
  if (filterByBranch && (this.config.scope === "global" || this.currentBranch !== "default")) {
@@ -8131,12 +9045,13 @@ var Indexer = class {
8131
9045
  });
8132
9046
  }
8133
9047
  const fusionStartTime = import_perf_hooks.performance.now();
9048
+ const rankingHybridWeight = embedding === void 0 && fusionStrategy === "weighted" ? 1 : effectiveHybridWeight;
8134
9049
  const combined = rankHybridResults(query, semanticCandidates, keywordCandidates, {
8135
9050
  fusionStrategy,
8136
9051
  rrfK,
8137
9052
  rerankTopN,
8138
9053
  limit: maxResults,
8139
- hybridWeight,
9054
+ hybridWeight: rankingHybridWeight,
8140
9055
  prioritizeSourcePaths: sourceIntent
8141
9056
  });
8142
9057
  const rerankedCombined = await this.rerankCandidatesWithApi(query, combined, {
@@ -8210,7 +9125,7 @@ var Indexer = class {
8210
9125
  let contextEndLine = r.metadata.endLine;
8211
9126
  if (!metadataOnly && this.config.search.includeContext) {
8212
9127
  try {
8213
- const fileContent = await import_fs7.promises.readFile(
9128
+ const fileContent = await import_fs8.promises.readFile(
8214
9129
  r.metadata.filePath,
8215
9130
  "utf-8"
8216
9131
  );
@@ -8236,8 +9151,7 @@ var Indexer = class {
8236
9151
  })
8237
9152
  );
8238
9153
  }
8239
- async keywordSearch(query, limit) {
8240
- const { store, invertedIndex } = await this.ensureInitialized();
9154
+ async keywordSearch(query, limit, store, invertedIndex) {
8241
9155
  const scores = invertedIndex.search(query);
8242
9156
  if (scores.size === 0) {
8243
9157
  return [];
@@ -8255,24 +9169,54 @@ var Indexer = class {
8255
9169
  return results.slice(0, limit);
8256
9170
  }
8257
9171
  async getStatus() {
8258
- const { store, configuredProviderInfo, database } = await this.ensureInitialized();
9172
+ const { store, configuredProviderInfo, database, readIssues, compatibility } = await this.ensureInitialized();
8259
9173
  const failedBatchesCount = this.getFailedBatchesCount();
9174
+ const vectorCount = store.count();
9175
+ const statusReadIssues = [...readIssues];
9176
+ let startupWarning = "";
9177
+ if (!statusReadIssues.some((issue) => issue.component === "database")) {
9178
+ try {
9179
+ startupWarning = database.getMetadata(STARTUP_WARNING_METADATA_KEY) ?? "";
9180
+ } catch (error) {
9181
+ const message = this.getDatabaseReadIssueMessage();
9182
+ statusReadIssues.push(this.createReadIssue("database", message));
9183
+ if (!this.readIssues.some((issue) => issue.component === "database")) {
9184
+ this.recordReadIssue("database", message, error);
9185
+ }
9186
+ }
9187
+ }
9188
+ const readWarning = statusReadIssues.map((issue) => issue.message).join(" ");
9189
+ const warning = [readWarning, startupWarning].filter((message) => message.length > 0).join(" ");
9190
+ const hasBlockingReadIssue = statusReadIssues.some((issue) => issue.blocking);
8260
9191
  return {
8261
- indexed: store.count() > 0,
8262
- vectorCount: store.count(),
9192
+ indexed: vectorCount > 0 && !hasBlockingReadIssue,
9193
+ vectorCount,
8263
9194
  provider: configuredProviderInfo.provider,
8264
9195
  model: configuredProviderInfo.modelInfo.model,
8265
9196
  indexPath: this.indexPath,
8266
9197
  currentBranch: this.currentBranch,
8267
9198
  baseBranch: this.baseBranch,
8268
- compatibility: this.indexCompatibility,
9199
+ compatibility,
8269
9200
  failedBatchesCount,
8270
9201
  failedBatchesPath: failedBatchesCount > 0 ? this.failedBatchesPath : void 0,
8271
- warning: database.getMetadata(STARTUP_WARNING_METADATA_KEY) ?? void 0
9202
+ warning: warning || void 0
8272
9203
  };
8273
9204
  }
9205
+ async forceIndex(onProgress) {
9206
+ return this.withIndexMutationLease("force-index", async (recoveredOwners) => {
9207
+ await this.ensureInitializedUnlocked(recoveredOwners);
9208
+ await this.clearIndexUnlocked();
9209
+ return this.indexUnlocked(onProgress, [], true);
9210
+ });
9211
+ }
8274
9212
  async clearIndex() {
8275
- const { store, invertedIndex, database } = await this.ensureInitialized();
9213
+ await this.withIndexMutationLease("clear", async (recoveredOwners) => {
9214
+ await this.ensureInitializedUnlocked(recoveredOwners);
9215
+ await this.clearIndexUnlocked();
9216
+ });
9217
+ }
9218
+ async clearIndexUnlocked() {
9219
+ const { store, invertedIndex, database } = this.requireLoadedIndexState();
8276
9220
  if (this.config.scope === "global") {
8277
9221
  store.load();
8278
9222
  invertedIndex.load();
@@ -8299,7 +9243,7 @@ var Indexer = class {
8299
9243
  store.clear();
8300
9244
  store.save();
8301
9245
  invertedIndex.clear();
8302
- invertedIndex.save();
9246
+ this.saveInvertedIndex(invertedIndex);
8303
9247
  this.fileHashCache.clear();
8304
9248
  this.saveFileHashCache();
8305
9249
  database.clearAllIndexedData();
@@ -8323,14 +9267,7 @@ var Indexer = class {
8323
9267
  this.indexCompatibility = compatibility;
8324
9268
  return;
8325
9269
  }
8326
- const localProjectIndexPaths = [path11.join(this.projectRoot, getHostProjectIndexRelativePath(this.host))];
8327
- if (this.host !== "opencode") {
8328
- localProjectIndexPaths.push(path11.join(this.projectRoot, getHostProjectIndexRelativePath("opencode")));
8329
- }
8330
- const isLocalProjectIndex = localProjectIndexPaths.some(
8331
- (localPath) => path11.resolve(this.indexPath) === path11.resolve(localPath)
8332
- );
8333
- if (!isLocalProjectIndex) {
9270
+ if (!this.isLocalProjectIndexPath()) {
8334
9271
  throw new Error(
8335
9272
  "Project-scoped force rebuild is unsafe while using an inherited worktree index. Create a local project config boundary before clearing the index."
8336
9273
  );
@@ -8338,7 +9275,7 @@ var Indexer = class {
8338
9275
  store.clear();
8339
9276
  store.save();
8340
9277
  invertedIndex.clear();
8341
- invertedIndex.save();
9278
+ this.saveInvertedIndex(invertedIndex);
8342
9279
  this.fileHashCache.clear();
8343
9280
  this.saveFileHashCache();
8344
9281
  database.clearAllIndexedData();
@@ -8356,7 +9293,13 @@ var Indexer = class {
8356
9293
  this.indexCompatibility = this.validateIndexCompatibility(this.configuredProviderInfo);
8357
9294
  }
8358
9295
  async healthCheck() {
8359
- const { store, invertedIndex, database } = await this.ensureInitialized();
9296
+ return this.withIndexMutationLease("health-check", async (recoveredOwners) => {
9297
+ await this.ensureInitializedUnlocked(recoveredOwners);
9298
+ return this.healthCheckUnlocked();
9299
+ });
9300
+ }
9301
+ async healthCheckUnlocked() {
9302
+ const { store, invertedIndex, database } = this.requireLoadedIndexState();
8360
9303
  this.logger.gc("info", "Starting health check");
8361
9304
  const allMetadata = store.getAllMetadata();
8362
9305
  const filePathsToChunkKeys = /* @__PURE__ */ new Map();
@@ -8369,7 +9312,7 @@ var Indexer = class {
8369
9312
  const removedChunkKeys = [];
8370
9313
  const chunkKeysByRemovedFile = /* @__PURE__ */ new Map();
8371
9314
  for (const [filePath, chunkKeys] of filePathsToChunkKeys) {
8372
- if (!(0, import_fs7.existsSync)(filePath)) {
9315
+ if (!(0, import_fs8.existsSync)(filePath)) {
8373
9316
  chunkKeysByRemovedFile.set(filePath, chunkKeys);
8374
9317
  for (const key of chunkKeys) {
8375
9318
  removedChunkKeys.push(key);
@@ -8394,7 +9337,7 @@ var Indexer = class {
8394
9337
  const removedCount = removedChunkKeys.length;
8395
9338
  if (removedCount > 0) {
8396
9339
  store.save();
8397
- invertedIndex.save();
9340
+ this.saveInvertedIndex(invertedIndex);
8398
9341
  }
8399
9342
  let gcOrphanEmbeddings;
8400
9343
  let gcOrphanChunks;
@@ -8409,7 +9352,7 @@ var Indexer = class {
8409
9352
  if (!await this.tryResetCorruptedIndex("running index health check", error)) {
8410
9353
  throw error;
8411
9354
  }
8412
- await this.ensureInitialized();
9355
+ await this.initializeUnlocked("writer", [], { skipAutoGc: true });
8413
9356
  return {
8414
9357
  removed: 0,
8415
9358
  filePaths: [],
@@ -8418,7 +9361,7 @@ var Indexer = class {
8418
9361
  gcOrphanSymbols: 0,
8419
9362
  gcOrphanCallEdges: 0,
8420
9363
  resetCorruptedIndex: true,
8421
- warning: this.getCorruptedIndexWarning(path11.join(this.indexPath, "codebase.db"))
9364
+ warning: this.getCorruptedIndexWarning(path12.join(this.indexPath, "codebase.db"))
8422
9365
  };
8423
9366
  }
8424
9367
  this.logger.recordGc(removedCount, gcOrphanChunks, gcOrphanEmbeddings);
@@ -8431,7 +9374,13 @@ var Indexer = class {
8431
9374
  return { removed: removedCount, filePaths: removedFilePaths, gcOrphanEmbeddings, gcOrphanChunks, gcOrphanSymbols, gcOrphanCallEdges };
8432
9375
  }
8433
9376
  async retryFailedBatches() {
8434
- const { store, provider, invertedIndex, database, configuredProviderInfo } = await this.ensureInitialized();
9377
+ return this.withIndexMutationLease("retry-failed-batches", async (recoveredOwners) => {
9378
+ await this.ensureInitializedUnlocked(recoveredOwners);
9379
+ return this.retryFailedBatchesUnlocked();
9380
+ });
9381
+ }
9382
+ async retryFailedBatchesUnlocked() {
9383
+ const { store, provider, invertedIndex, database, configuredProviderInfo } = this.requireLoadedIndexState();
8435
9384
  const maxChunkTokens = getSafeEmbeddingChunkTokenLimit(configuredProviderInfo);
8436
9385
  const providerRateLimits = this.getProviderRateLimits(configuredProviderInfo.provider);
8437
9386
  const roots = this.config.scope === "global" ? this.getScopedRoots() : null;
@@ -8595,7 +9544,7 @@ var Indexer = class {
8595
9544
  }
8596
9545
  if (succeeded > 0) {
8597
9546
  store.save();
8598
- invertedIndex.save();
9547
+ this.saveInvertedIndex(invertedIndex);
8599
9548
  }
8600
9549
  if (roots && succeeded > 0 && persistedStillFailing.length === 0 && this.hasProjectForceReembedPending()) {
8601
9550
  database.deleteMetadata(this.getProjectForceReembedMetadataKey());
@@ -8623,15 +9572,16 @@ var Indexer = class {
8623
9572
  }
8624
9573
  }
8625
9574
  async getDatabaseStats() {
8626
- const { database } = await this.ensureInitialized();
9575
+ const { database, readIssues } = await this.ensureInitialized();
9576
+ this.requireReadableComponents(readIssues, "database");
8627
9577
  return database.getStats();
8628
9578
  }
8629
9579
  getLogger() {
8630
9580
  return this.logger;
8631
9581
  }
8632
9582
  async findSimilar(code, limit = this.config.search.maxResults, options) {
8633
- const { store, provider, database } = await this.ensureInitialized();
8634
- const compatibility = this.checkCompatibility();
9583
+ const { store, provider, database, readIssues, compatibility } = await this.ensureInitialized();
9584
+ this.requireReadableComponents(readIssues, "vectors", "database");
8635
9585
  if (!compatibility.compatible) {
8636
9586
  throw new Error(
8637
9587
  `${compatibility.reason ?? "Index is incompatible with current embedding provider."} Run index_codebase with force=true to rebuild the index.`
@@ -8721,7 +9671,7 @@ var Indexer = class {
8721
9671
  let content = "";
8722
9672
  if (this.config.search.includeContext) {
8723
9673
  try {
8724
- const fileContent = await import_fs7.promises.readFile(
9674
+ const fileContent = await import_fs8.promises.readFile(
8725
9675
  r.metadata.filePath,
8726
9676
  "utf-8"
8727
9677
  );
@@ -8745,7 +9695,8 @@ var Indexer = class {
8745
9695
  );
8746
9696
  }
8747
9697
  async getCallers(targetName, callTypeFilter) {
8748
- const { database } = await this.ensureInitialized();
9698
+ const { database, readIssues } = await this.ensureInitialized();
9699
+ this.requireReadableComponents(readIssues, "database");
8749
9700
  const seen = /* @__PURE__ */ new Set();
8750
9701
  const results = [];
8751
9702
  for (const branchKey of this.getBranchCatalogKeys()) {
@@ -8759,7 +9710,8 @@ var Indexer = class {
8759
9710
  return results;
8760
9711
  }
8761
9712
  async getCallees(symbolId, callTypeFilter) {
8762
- const { database } = await this.ensureInitialized();
9713
+ const { database, readIssues } = await this.ensureInitialized();
9714
+ this.requireReadableComponents(readIssues, "database");
8763
9715
  const seen = /* @__PURE__ */ new Set();
8764
9716
  const results = [];
8765
9717
  for (const branchKey of this.getBranchCatalogKeys()) {
@@ -8773,43 +9725,50 @@ var Indexer = class {
8773
9725
  return results;
8774
9726
  }
8775
9727
  async findCallPath(fromName, toName, maxDepth) {
8776
- const { database } = await this.ensureInitialized();
9728
+ const { database, readIssues } = await this.ensureInitialized();
9729
+ this.requireReadableComponents(readIssues, "database");
8777
9730
  let shortest = [];
8778
9731
  for (const branchKey of this.getBranchCatalogKeys()) {
8779
- const path26 = database.findShortestPath(fromName, toName, branchKey, maxDepth);
8780
- if (path26.length > 0 && (shortest.length === 0 || path26.length < shortest.length)) {
8781
- shortest = path26;
9732
+ const path27 = database.findShortestPath(fromName, toName, branchKey, maxDepth);
9733
+ if (path27.length > 0 && (shortest.length === 0 || path27.length < shortest.length)) {
9734
+ shortest = path27;
8782
9735
  }
8783
9736
  }
8784
9737
  return shortest;
8785
9738
  }
8786
9739
  async getSymbolsForBranch(branch) {
8787
- const { database } = await this.ensureInitialized();
9740
+ const { database, readIssues } = await this.ensureInitialized();
9741
+ this.requireReadableComponents(readIssues, "database");
8788
9742
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8789
9743
  return database.getSymbolsForBranch(resolvedBranch);
8790
9744
  }
8791
9745
  async getSymbolsForFiles(filePaths, branch) {
8792
- const { database } = await this.ensureInitialized();
9746
+ const { database, readIssues } = await this.ensureInitialized();
9747
+ this.requireReadableComponents(readIssues, "database");
8793
9748
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8794
9749
  return database.getSymbolsForFiles(filePaths, resolvedBranch);
8795
9750
  }
8796
9751
  async getTransitiveReachability(rootSymbolIds, direction, maxDepth) {
8797
- const { database } = await this.ensureInitialized();
9752
+ const { database, readIssues } = await this.ensureInitialized();
9753
+ this.requireReadableComponents(readIssues, "database");
8798
9754
  const branch = this.getBranchCatalogKey();
8799
9755
  return database.getTransitiveReachability(rootSymbolIds, branch, direction, maxDepth);
8800
9756
  }
8801
9757
  async detectCommunities(branch, symbolIds) {
8802
- const { database } = await this.ensureInitialized();
9758
+ const { database, readIssues } = await this.ensureInitialized();
9759
+ this.requireReadableComponents(readIssues, "database");
8803
9760
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8804
9761
  return database.detectCommunities(resolvedBranch, symbolIds);
8805
9762
  }
8806
9763
  async computeCentrality(branch) {
8807
- const { database } = await this.ensureInitialized();
9764
+ const { database, readIssues } = await this.ensureInitialized();
9765
+ this.requireReadableComponents(readIssues, "database");
8808
9766
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8809
9767
  return database.computeCentrality(resolvedBranch);
8810
9768
  }
8811
9769
  async getPrImpact(opts) {
8812
- const { database } = await this.ensureInitialized();
9770
+ const { database, readIssues } = await this.ensureInitialized();
9771
+ this.requireReadableComponents(readIssues, "database");
8813
9772
  const execFileAsync3 = (0, import_util3.promisify)(import_child_process3.execFile);
8814
9773
  const changedFilesResult = await getChangedFiles({
8815
9774
  pr: opts.pr,
@@ -8832,7 +9791,7 @@ var Indexer = class {
8832
9791
  "Run index_codebase first to build the call graph and symbol index for this branch."
8833
9792
  );
8834
9793
  }
8835
- const absoluteChangedFiles = changedFiles.map((f) => path11.resolve(this.projectRoot, f));
9794
+ const absoluteChangedFiles = changedFiles.map((f) => path12.resolve(this.projectRoot, f));
8836
9795
  const directSymbols = database.getSymbolsForFiles(absoluteChangedFiles, branchKey);
8837
9796
  const directIds = directSymbols.map((s) => s.id);
8838
9797
  const direction = opts.direction ?? "both";
@@ -8915,7 +9874,7 @@ var Indexer = class {
8915
9874
  projectRoot: this.projectRoot,
8916
9875
  baseBranch: this.baseBranch
8917
9876
  });
8918
- const otherAbsolute = otherChanged.files.map((f) => path11.resolve(this.projectRoot, f));
9877
+ const otherAbsolute = otherChanged.files.map((f) => path12.resolve(this.projectRoot, f));
8919
9878
  const otherBranchKey = this.getBranchCatalogKeyFor(openPr.headRefName);
8920
9879
  const otherSymbols = database.getSymbolsForFiles(otherAbsolute, otherBranchKey);
8921
9880
  const otherLabels = /* @__PURE__ */ new Set();
@@ -8965,7 +9924,8 @@ var Indexer = class {
8965
9924
  };
8966
9925
  }
8967
9926
  async getVisualizationData(options) {
8968
- const { database, store } = await this.ensureInitialized();
9927
+ const { database, store, readIssues } = await this.ensureInitialized();
9928
+ this.requireReadableComponents(readIssues, "vectors", "database");
8969
9929
  const seenSymbols = /* @__PURE__ */ new Map();
8970
9930
  const seenEdges = /* @__PURE__ */ new Map();
8971
9931
  for (const branchKey of this.getBranchCatalogKeys()) {
@@ -8978,12 +9938,12 @@ var Indexer = class {
8978
9938
  if (meta.filePath) filePaths.add(meta.filePath);
8979
9939
  }
8980
9940
  const directory = options?.directory?.replace(/\/$/, "");
8981
- const absoluteDirectoryFilter = directory ? path11.resolve(this.projectRoot, directory) : void 0;
9941
+ const absoluteDirectoryFilter = directory ? path12.resolve(this.projectRoot, directory) : void 0;
8982
9942
  for (const filePath of filePaths) {
8983
9943
  if (directory) {
8984
- const absoluteFilePath = path11.resolve(filePath);
9944
+ const absoluteFilePath = path12.resolve(filePath);
8985
9945
  const matchesRelative = filePath === directory || filePath.startsWith(directory + "/");
8986
- const matchesProjectRelative = absoluteDirectoryFilter !== void 0 && (absoluteFilePath === absoluteDirectoryFilter || absoluteFilePath.startsWith(absoluteDirectoryFilter + path11.sep));
9946
+ const matchesProjectRelative = absoluteDirectoryFilter !== void 0 && (absoluteFilePath === absoluteDirectoryFilter || absoluteFilePath.startsWith(absoluteDirectoryFilter + path12.sep));
8987
9947
  if (!matchesRelative && !matchesProjectRelative) {
8988
9948
  continue;
8989
9949
  }
@@ -9005,12 +9965,23 @@ var Indexer = class {
9005
9965
  return { symbols: [...seenSymbols.values()], edges: [...seenEdges.values()] };
9006
9966
  }
9007
9967
  async close() {
9008
- await this.database?.close();
9968
+ this.database?.close();
9969
+ for (const database of this.retiredDatabases) {
9970
+ database.close();
9971
+ }
9972
+ this.retiredDatabases = [];
9009
9973
  this.database = null;
9010
9974
  this.store = null;
9011
9975
  this.invertedIndex = null;
9012
9976
  this.provider = null;
9013
9977
  this.reranker = null;
9978
+ this.configuredProviderInfo = null;
9979
+ this.indexCompatibility = null;
9980
+ this.initializationMode = "none";
9981
+ this.readIssues = [];
9982
+ this.readerArtifactFingerprint = null;
9983
+ this.writerArtifactFingerprint = null;
9984
+ this.readerArtifactRetryAfter.clear();
9014
9985
  }
9015
9986
  };
9016
9987
 
@@ -9261,15 +10232,15 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
9261
10232
  }
9262
10233
 
9263
10234
  // src/eval/runner-config.ts
9264
- var import_fs8 = require("fs");
9265
- var os4 = __toESM(require("os"), 1);
9266
- var path13 = __toESM(require("path"), 1);
10235
+ var import_fs9 = require("fs");
10236
+ var os5 = __toESM(require("os"), 1);
10237
+ var path14 = __toESM(require("path"), 1);
9267
10238
 
9268
10239
  // src/config/rebase.ts
9269
- var path12 = __toESM(require("path"), 1);
10240
+ var path13 = __toESM(require("path"), 1);
9270
10241
  function isWithinRoot(rootDir, targetPath) {
9271
- const relativePath = path12.relative(rootDir, targetPath);
9272
- return relativePath === "" || !relativePath.startsWith("..") && !path12.isAbsolute(relativePath);
10242
+ const relativePath = path13.relative(rootDir, targetPath);
10243
+ return relativePath === "" || !relativePath.startsWith("..") && !path13.isAbsolute(relativePath);
9273
10244
  }
9274
10245
  function rebasePathEntries(values, fromDir, toDir) {
9275
10246
  if (!Array.isArray(values)) {
@@ -9277,10 +10248,10 @@ function rebasePathEntries(values, fromDir, toDir) {
9277
10248
  }
9278
10249
  return values.filter((value) => typeof value === "string").map((value) => {
9279
10250
  const trimmed = value.trim();
9280
- if (!trimmed || path12.isAbsolute(trimmed)) {
10251
+ if (!trimmed || path13.isAbsolute(trimmed)) {
9281
10252
  return trimmed;
9282
10253
  }
9283
- return normalizePathSeparators(path12.normalize(path12.relative(toDir, path12.resolve(fromDir, trimmed))));
10254
+ return normalizePathSeparators(path13.normalize(path13.relative(toDir, path13.resolve(fromDir, trimmed))));
9284
10255
  }).filter(Boolean);
9285
10256
  }
9286
10257
  function resolveInheritedKnowledgeBaseEntries(values, sourceRoot, targetRoot) {
@@ -9292,17 +10263,17 @@ function resolveInheritedKnowledgeBaseEntries(values, sourceRoot, targetRoot) {
9292
10263
  if (!trimmed) {
9293
10264
  return trimmed;
9294
10265
  }
9295
- if (path12.isAbsolute(trimmed)) {
10266
+ if (path13.isAbsolute(trimmed)) {
9296
10267
  if (isWithinRoot(sourceRoot, trimmed)) {
9297
- return normalizePathSeparators(path12.normalize(path12.relative(sourceRoot, trimmed) || "."));
10268
+ return normalizePathSeparators(path13.normalize(path13.relative(sourceRoot, trimmed) || "."));
9298
10269
  }
9299
- return path12.normalize(trimmed);
10270
+ return path13.normalize(trimmed);
9300
10271
  }
9301
- const resolvedFromSource = path12.resolve(sourceRoot, trimmed);
10272
+ const resolvedFromSource = path13.resolve(sourceRoot, trimmed);
9302
10273
  if (isWithinRoot(sourceRoot, resolvedFromSource)) {
9303
- return normalizePathSeparators(path12.normalize(trimmed));
10274
+ return normalizePathSeparators(path13.normalize(trimmed));
9304
10275
  }
9305
- return normalizePathSeparators(path12.normalize(path12.relative(targetRoot, resolvedFromSource)));
10276
+ return normalizePathSeparators(path13.normalize(path13.relative(targetRoot, resolvedFromSource)));
9306
10277
  }).filter(Boolean);
9307
10278
  }
9308
10279
 
@@ -9340,7 +10311,7 @@ function validateEvalConfigShape(rawConfig, filePath) {
9340
10311
  }
9341
10312
  function parseJsonConfigFile(filePath) {
9342
10313
  try {
9343
- return validateEvalConfigShape(JSON.parse((0, import_fs8.readFileSync)(filePath, "utf-8")), filePath);
10314
+ return validateEvalConfigShape(JSON.parse((0, import_fs9.readFileSync)(filePath, "utf-8")), filePath);
9344
10315
  } catch (error) {
9345
10316
  if (error instanceof Error && error.message.startsWith("Eval config at ")) {
9346
10317
  throw error;
@@ -9350,20 +10321,20 @@ function parseJsonConfigFile(filePath) {
9350
10321
  }
9351
10322
  }
9352
10323
  function toAbsolute(projectRoot, maybeRelative) {
9353
- return path13.isAbsolute(maybeRelative) ? maybeRelative : path13.join(projectRoot, maybeRelative);
10324
+ return path14.isAbsolute(maybeRelative) ? maybeRelative : path14.join(projectRoot, maybeRelative);
9354
10325
  }
9355
10326
  function isProjectScopedConfigPath(configPath) {
9356
- return path13.basename(configPath) === "codebase-index.json" && path13.basename(path13.dirname(configPath)) === ".opencode";
10327
+ return path14.basename(configPath) === "codebase-index.json" && path14.basename(path14.dirname(configPath)) === ".opencode";
9357
10328
  }
9358
10329
  function normalizeEvalConfigKnowledgeBases(rawConfig, projectRoot, resolvedConfigPath) {
9359
10330
  const config = rawConfig && typeof rawConfig === "object" ? { ...rawConfig } : {};
9360
10331
  const rebaseEntries = (values) => isProjectScopedConfigPath(resolvedConfigPath) ? resolveInheritedKnowledgeBaseEntries(
9361
10332
  values,
9362
- path13.dirname(path13.dirname(resolvedConfigPath)),
10333
+ path14.dirname(path14.dirname(resolvedConfigPath)),
9363
10334
  projectRoot
9364
10335
  ) : rebasePathEntries(
9365
10336
  values,
9366
- path13.dirname(resolvedConfigPath),
10337
+ path14.dirname(resolvedConfigPath),
9367
10338
  projectRoot
9368
10339
  );
9369
10340
  if (Array.isArray(config.knowledgeBases)) {
@@ -9376,7 +10347,7 @@ function normalizeEvalConfigKnowledgeBases(rawConfig, projectRoot, resolvedConfi
9376
10347
  }
9377
10348
  function loadRawConfig(projectRoot, configPath) {
9378
10349
  const fromPath = configPath ? toAbsolute(projectRoot, configPath) : null;
9379
- if (fromPath && (0, import_fs8.existsSync)(fromPath)) {
10350
+ if (fromPath && (0, import_fs9.existsSync)(fromPath)) {
9380
10351
  return normalizeEvalConfigKnowledgeBases(
9381
10352
  parseJsonConfigFile(fromPath),
9382
10353
  projectRoot,
@@ -9384,15 +10355,15 @@ function loadRawConfig(projectRoot, configPath) {
9384
10355
  );
9385
10356
  }
9386
10357
  const projectConfig = resolveProjectConfigPath(projectRoot);
9387
- if ((0, import_fs8.existsSync)(projectConfig)) {
10358
+ if ((0, import_fs9.existsSync)(projectConfig)) {
9388
10359
  return normalizeEvalConfigKnowledgeBases(
9389
10360
  parseJsonConfigFile(projectConfig),
9390
10361
  projectRoot,
9391
10362
  projectConfig
9392
10363
  );
9393
10364
  }
9394
- const globalConfig = path13.join(os4.homedir(), ".config", "opencode", "codebase-index.json");
9395
- if ((0, import_fs8.existsSync)(globalConfig)) {
10365
+ const globalConfig = path14.join(os5.homedir(), ".config", "opencode", "codebase-index.json");
10366
+ if ((0, import_fs9.existsSync)(globalConfig)) {
9396
10367
  return parseJsonConfigFile(globalConfig);
9397
10368
  }
9398
10369
  return {};
@@ -9401,24 +10372,24 @@ function getIndexRootPath(projectRoot, scope) {
9401
10372
  return scope === "global" ? getGlobalIndexPath() : resolveProjectIndexPath(projectRoot, scope);
9402
10373
  }
9403
10374
  function getLocalProjectIndexRoot(projectRoot) {
9404
- return path13.join(projectRoot, ".opencode", "index");
10375
+ return path14.join(projectRoot, ".opencode", "index");
9405
10376
  }
9406
10377
  function getLocalProjectConfigPath(projectRoot) {
9407
- return path13.join(projectRoot, ".opencode", "codebase-index.json");
10378
+ return path14.join(projectRoot, ".opencode", "codebase-index.json");
9408
10379
  }
9409
10380
  function clearIndexRoot(projectRoot, scope) {
9410
10381
  const indexRoot = scope === "global" ? getIndexRootPath(projectRoot, scope) : getLocalProjectIndexRoot(projectRoot);
9411
- if ((0, import_fs8.existsSync)(indexRoot)) {
9412
- (0, import_fs8.rmSync)(indexRoot, { recursive: true, force: true });
10382
+ if ((0, import_fs9.existsSync)(indexRoot)) {
10383
+ (0, import_fs9.rmSync)(indexRoot, { recursive: true, force: true });
9413
10384
  }
9414
10385
  }
9415
10386
  function ensureLocalEvalProjectConfig(projectRoot, configPath) {
9416
10387
  const localConfigPath = getLocalProjectConfigPath(projectRoot);
9417
10388
  const resolvedConfigPath = configPath ? toAbsolute(projectRoot, configPath) : resolveProjectConfigPath(projectRoot);
9418
- if (!configPath && (0, import_fs8.existsSync)(localConfigPath)) {
10389
+ if (!configPath && (0, import_fs9.existsSync)(localConfigPath)) {
9419
10390
  return localConfigPath;
9420
10391
  }
9421
- if (!(0, import_fs8.existsSync)(resolvedConfigPath) || resolvedConfigPath === localConfigPath) {
10392
+ if (!(0, import_fs9.existsSync)(resolvedConfigPath) || resolvedConfigPath === localConfigPath) {
9422
10393
  return resolvedConfigPath;
9423
10394
  }
9424
10395
  const sourceConfig = normalizeEvalConfigKnowledgeBases(
@@ -9426,8 +10397,8 @@ function ensureLocalEvalProjectConfig(projectRoot, configPath) {
9426
10397
  projectRoot,
9427
10398
  resolvedConfigPath
9428
10399
  );
9429
- (0, import_fs8.mkdirSync)(path13.dirname(localConfigPath), { recursive: true });
9430
- (0, import_fs8.writeFileSync)(localConfigPath, JSON.stringify(sourceConfig, null, 2), "utf-8");
10400
+ (0, import_fs9.mkdirSync)(path14.dirname(localConfigPath), { recursive: true });
10401
+ (0, import_fs9.writeFileSync)(localConfigPath, JSON.stringify(sourceConfig, null, 2), "utf-8");
9431
10402
  return localConfigPath;
9432
10403
  }
9433
10404
  function loadParsedConfig(projectRoot, configPath) {
@@ -9460,9 +10431,9 @@ function getEmbeddingCostPer1MTokens(embeddingProvider) {
9460
10431
  }
9461
10432
 
9462
10433
  // src/eval/schema.ts
9463
- var import_fs9 = require("fs");
10434
+ var import_fs10 = require("fs");
9464
10435
  function parseJsonFile(filePath) {
9465
- const content = (0, import_fs9.readFileSync)(filePath, "utf-8");
10436
+ const content = (0, import_fs10.readFileSync)(filePath, "utf-8");
9466
10437
  try {
9467
10438
  return JSON.parse(content);
9468
10439
  } catch (error) {
@@ -9476,23 +10447,23 @@ function isRecord2(value) {
9476
10447
  function isStringArray3(value) {
9477
10448
  return Array.isArray(value) && value.every((item) => typeof item === "string");
9478
10449
  }
9479
- function asPositiveNumber(value, path26) {
10450
+ function asPositiveNumber(value, path27) {
9480
10451
  if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
9481
- throw new Error(`${path26} must be a non-negative number`);
10452
+ throw new Error(`${path27} must be a non-negative number`);
9482
10453
  }
9483
10454
  return value;
9484
10455
  }
9485
- function parseQueryType(value, path26) {
10456
+ function parseQueryType(value, path27) {
9486
10457
  if (value === "definition" || value === "implementation-intent" || value === "similarity" || value === "keyword-heavy") {
9487
10458
  return value;
9488
10459
  }
9489
10460
  throw new Error(
9490
- `${path26} must be one of: definition, implementation-intent, similarity, keyword-heavy`
10461
+ `${path27} must be one of: definition, implementation-intent, similarity, keyword-heavy`
9491
10462
  );
9492
10463
  }
9493
- function parseExpected(input, path26) {
10464
+ function parseExpected(input, path27) {
9494
10465
  if (!isRecord2(input)) {
9495
- throw new Error(`${path26} must be an object`);
10466
+ throw new Error(`${path27} must be an object`);
9496
10467
  }
9497
10468
  const filePathRaw = input.filePath;
9498
10469
  const acceptableFilesRaw = input.acceptableFiles;
@@ -9501,16 +10472,16 @@ function parseExpected(input, path26) {
9501
10472
  const filePath = typeof filePathRaw === "string" ? filePathRaw : void 0;
9502
10473
  const acceptableFiles = isStringArray3(acceptableFilesRaw) ? acceptableFilesRaw : void 0;
9503
10474
  if (!filePath && (!acceptableFiles || acceptableFiles.length === 0)) {
9504
- throw new Error(`${path26} must include either expected.filePath or expected.acceptableFiles`);
10475
+ throw new Error(`${path27} must include either expected.filePath or expected.acceptableFiles`);
9505
10476
  }
9506
10477
  if (acceptableFilesRaw !== void 0 && !isStringArray3(acceptableFilesRaw)) {
9507
- throw new Error(`${path26}.acceptableFiles must be an array of strings`);
10478
+ throw new Error(`${path27}.acceptableFiles must be an array of strings`);
9508
10479
  }
9509
10480
  if (symbolRaw !== void 0 && typeof symbolRaw !== "string") {
9510
- throw new Error(`${path26}.symbol must be a string when provided`);
10481
+ throw new Error(`${path27}.symbol must be a string when provided`);
9511
10482
  }
9512
10483
  if (branchRaw !== void 0 && typeof branchRaw !== "string") {
9513
- throw new Error(`${path26}.branch must be a string when provided`);
10484
+ throw new Error(`${path27}.branch must be a string when provided`);
9514
10485
  }
9515
10486
  return {
9516
10487
  filePath,
@@ -9520,25 +10491,25 @@ function parseExpected(input, path26) {
9520
10491
  };
9521
10492
  }
9522
10493
  function parseQuery(input, index) {
9523
- const path26 = `queries[${index}]`;
10494
+ const path27 = `queries[${index}]`;
9524
10495
  if (!isRecord2(input)) {
9525
- throw new Error(`${path26} must be an object`);
10496
+ throw new Error(`${path27} must be an object`);
9526
10497
  }
9527
10498
  const id = input.id;
9528
10499
  const query = input.query;
9529
10500
  const queryType = input.queryType;
9530
10501
  const expected = input.expected;
9531
10502
  if (typeof id !== "string" || id.trim().length === 0) {
9532
- throw new Error(`${path26}.id must be a non-empty string`);
10503
+ throw new Error(`${path27}.id must be a non-empty string`);
9533
10504
  }
9534
10505
  if (typeof query !== "string" || query.trim().length === 0) {
9535
- throw new Error(`${path26}.query must be a non-empty string`);
10506
+ throw new Error(`${path27}.query must be a non-empty string`);
9536
10507
  }
9537
10508
  return {
9538
10509
  id,
9539
10510
  query,
9540
- queryType: parseQueryType(queryType, `${path26}.queryType`),
9541
- expected: parseExpected(expected, `${path26}.expected`)
10511
+ queryType: parseQueryType(queryType, `${path27}.queryType`),
10512
+ expected: parseExpected(expected, `${path27}.expected`)
9542
10513
  };
9543
10514
  }
9544
10515
  function parseGoldenDataset(raw, sourceLabel) {
@@ -9721,13 +10692,13 @@ async function runEvaluation(options) {
9721
10692
  };
9722
10693
  const outputDir = createRunDirectory(toAbsolute(options.projectRoot, options.outputRoot));
9723
10694
  const perQueryArtifact = buildPerQueryArtifact(perQuery);
9724
- writeJson(path14.join(outputDir, "summary.json"), summary);
9725
- writeJson(path14.join(outputDir, "per-query.json"), perQueryArtifact);
10695
+ writeJson(path15.join(outputDir, "summary.json"), summary);
10696
+ writeJson(path15.join(outputDir, "per-query.json"), perQueryArtifact);
9726
10697
  let comparison;
9727
10698
  if (againstPath) {
9728
10699
  const baseline = loadSummary(againstPath);
9729
10700
  comparison = compareSummaries(summary, baseline, againstPath);
9730
- writeJson(path14.join(outputDir, "compare.json"), comparison);
10701
+ writeJson(path15.join(outputDir, "compare.json"), comparison);
9731
10702
  }
9732
10703
  let gate;
9733
10704
  if (options.ciMode) {
@@ -9737,10 +10708,10 @@ async function runEvaluation(options) {
9737
10708
  const budget = loadBudget(budgetPath);
9738
10709
  if (!comparison && budget.baselinePath) {
9739
10710
  const resolvedBaseline = toAbsolute(options.projectRoot, budget.baselinePath);
9740
- if ((0, import_fs10.existsSync)(resolvedBaseline)) {
10711
+ if ((0, import_fs11.existsSync)(resolvedBaseline)) {
9741
10712
  const baselineSummary = loadSummary(resolvedBaseline);
9742
10713
  comparison = compareSummaries(summary, baselineSummary, resolvedBaseline);
9743
- writeJson(path14.join(outputDir, "compare.json"), comparison);
10714
+ writeJson(path15.join(outputDir, "compare.json"), comparison);
9744
10715
  } else if (budget.failOnMissingBaseline) {
9745
10716
  throw new Error(
9746
10717
  `Budget baseline is missing: ${resolvedBaseline}. Set failOnMissingBaseline=false to allow CI run without baseline.`
@@ -9750,7 +10721,7 @@ async function runEvaluation(options) {
9750
10721
  gate = evaluateBudgetGate(budget, summary, comparison);
9751
10722
  }
9752
10723
  const markdown = createSummaryMarkdown(summary, comparison, gate);
9753
- writeText(path14.join(outputDir, "summary.md"), markdown);
10724
+ writeText(path15.join(outputDir, "summary.md"), markdown);
9754
10725
  return { outputDir, summary, perQuery, comparison, gate };
9755
10726
  } finally {
9756
10727
  await indexer.close();
@@ -9808,23 +10779,23 @@ async function runSweep(options, sweep) {
9808
10779
  bestByMrrAt10,
9809
10780
  bestByP95Latency
9810
10781
  };
9811
- writeJson(path14.join(outputDir, "compare.json"), aggregate);
10782
+ writeJson(path15.join(outputDir, "compare.json"), aggregate);
9812
10783
  const md = createSummaryMarkdown(
9813
10784
  bestByHitAt5?.summary ?? runs[0].summary,
9814
10785
  bestByHitAt5?.comparison,
9815
10786
  void 0,
9816
10787
  aggregate
9817
10788
  );
9818
- writeText(path14.join(outputDir, "summary.md"), md);
9819
- writeJson(path14.join(outputDir, "summary.json"), bestByHitAt5?.summary ?? runs[0].summary);
10789
+ writeText(path15.join(outputDir, "summary.md"), md);
10790
+ writeJson(path15.join(outputDir, "summary.json"), bestByHitAt5?.summary ?? runs[0].summary);
9820
10791
  return { outputDir, aggregate };
9821
10792
  }
9822
10793
 
9823
10794
  // src/eval/cli.ts
9824
- var path16 = __toESM(require("path"), 1);
10795
+ var path17 = __toESM(require("path"), 1);
9825
10796
 
9826
10797
  // src/eval/cli-parser.ts
9827
- var path15 = __toESM(require("path"), 1);
10798
+ var path16 = __toESM(require("path"), 1);
9828
10799
  function printUsage() {
9829
10800
  console.log(`
9830
10801
  Usage:
@@ -9896,12 +10867,12 @@ function parseEvalArgs(argv, cwd) {
9896
10867
  const arg = argv[i];
9897
10868
  const next = argv[i + 1];
9898
10869
  if (arg === "--project" && next) {
9899
- parsed.projectRoot = path15.resolve(cwd, next);
10870
+ parsed.projectRoot = path16.resolve(cwd, next);
9900
10871
  i += 1;
9901
10872
  continue;
9902
10873
  }
9903
10874
  if (arg === "--config" && next) {
9904
- parsed.configPath = path15.resolve(cwd, next);
10875
+ parsed.configPath = path16.resolve(cwd, next);
9905
10876
  i += 1;
9906
10877
  continue;
9907
10878
  }
@@ -10097,22 +11068,22 @@ async function handleEvalCommand(args, cwd) {
10097
11068
  if (!parsed.againstPath.endsWith(".json")) {
10098
11069
  throw new Error("eval diff --against must point to a summary JSON file");
10099
11070
  }
10100
- const currentSummary = loadSummary(path16.resolve(parsed.projectRoot, currentPath), {
11071
+ const currentSummary = loadSummary(path17.resolve(parsed.projectRoot, currentPath), {
10101
11072
  allowLegacyDiversityMetrics: true
10102
11073
  });
10103
- const baselineSummary = loadSummary(path16.resolve(parsed.projectRoot, parsed.againstPath), {
11074
+ const baselineSummary = loadSummary(path17.resolve(parsed.projectRoot, parsed.againstPath), {
10104
11075
  allowLegacyDiversityMetrics: true
10105
11076
  });
10106
11077
  const comparison = compareSummaries(
10107
11078
  currentSummary,
10108
11079
  baselineSummary,
10109
- path16.resolve(parsed.projectRoot, parsed.againstPath)
11080
+ path17.resolve(parsed.projectRoot, parsed.againstPath)
10110
11081
  );
10111
- const outputDir = createRunDirectory(path16.resolve(parsed.projectRoot, parsed.outputRoot));
11082
+ const outputDir = createRunDirectory(path17.resolve(parsed.projectRoot, parsed.outputRoot));
10112
11083
  const summaryMd = createSummaryMarkdown(currentSummary, comparison);
10113
- writeJson(path16.join(outputDir, "compare.json"), comparison);
10114
- writeText(path16.join(outputDir, "summary.md"), summaryMd);
10115
- writeJson(path16.join(outputDir, "summary.json"), currentSummary);
11084
+ writeJson(path17.join(outputDir, "compare.json"), comparison);
11085
+ writeText(path17.join(outputDir, "summary.md"), summaryMd);
11086
+ writeJson(path17.join(outputDir, "summary.json"), currentSummary);
10116
11087
  console.log(`Eval diff complete. Artifacts: ${outputDir}`);
10117
11088
  return 0;
10118
11089
  }
@@ -10121,7 +11092,7 @@ async function handleEvalCommand(args, cwd) {
10121
11092
 
10122
11093
  // src/mcp-server.ts
10123
11094
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
10124
- var import_fs14 = require("fs");
11095
+ var import_fs15 = require("fs");
10125
11096
 
10126
11097
  // src/mcp-server/register-prompts.ts
10127
11098
  var import_zod = require("zod");
@@ -10312,6 +11283,10 @@ function formatStatus(status) {
10312
11283
  lines.push(`Failed batches: ${status.failedBatchesPath}`);
10313
11284
  }
10314
11285
  }
11286
+ if (status.warning) {
11287
+ lines.push("");
11288
+ lines.push(`INDEX WARNING: ${status.warning}`);
11289
+ }
10315
11290
  if (status.compatibility && !status.compatibility.compatible) {
10316
11291
  lines.push("");
10317
11292
  lines.push(`COMPATIBILITY WARNING: ${status.compatibility.reason}`);
@@ -10417,16 +11392,16 @@ function formatCallGraphCallees(symbolId, callees, relationshipType) {
10417
11392
  return `[${index + 1}] \u2192 ${edge.targetName} (${edge.callType})${confidence} at line ${edge.line}${edge.isResolved ? ` [resolved: ${edge.toSymbolId}]` : " [unresolved]"}`;
10418
11393
  }).join("\n");
10419
11394
  }
10420
- function formatCallGraphPath(from, to, path26) {
10421
- if (path26.length === 0) {
11395
+ function formatCallGraphPath(from, to, path27) {
11396
+ if (path27.length === 0) {
10422
11397
  return `No path found between "${from}" and "${to}". They may be in disconnected components, or the call graph index needs updating.`;
10423
11398
  }
10424
- const formatted = path26.map((hop, index) => {
11399
+ const formatted = path27.map((hop, index) => {
10425
11400
  const prefix = index === 0 ? "[start]" : `--${hop.callType}-->`;
10426
11401
  const location = hop.filePath ? ` (${hop.filePath}:${hop.line})` : "";
10427
11402
  return `${prefix} ${hop.symbolName}${location}`;
10428
11403
  });
10429
- return `Path (${path26.length} hops):
11404
+ return `Path (${path27.length} hops):
10430
11405
  ${formatted.join("\n")}`;
10431
11406
  }
10432
11407
  function formatResultHeader(result, index) {
@@ -10517,12 +11492,12 @@ function formatPrImpact(result) {
10517
11492
  }
10518
11493
 
10519
11494
  // src/tools/operations.ts
10520
- var import_fs13 = require("fs");
10521
- var path20 = __toESM(require("path"), 1);
11495
+ var import_fs14 = require("fs");
11496
+ var path21 = __toESM(require("path"), 1);
10522
11497
 
10523
11498
  // src/config/merger.ts
10524
- var import_fs11 = require("fs");
10525
- var path17 = __toESM(require("path"), 1);
11499
+ var import_fs12 = require("fs");
11500
+ var path18 = __toESM(require("path"), 1);
10526
11501
  var PROJECT_OVERRIDE_KEYS = [
10527
11502
  "embeddingProvider",
10528
11503
  "customProvider",
@@ -10555,8 +11530,8 @@ function mergeUniqueStringArray(values) {
10555
11530
  return [...new Set(values.map((value) => String(value).trim()))];
10556
11531
  }
10557
11532
  function normalizeKnowledgeBasePath(value) {
10558
- let normalized = path17.normalize(String(value).trim());
10559
- const root = path17.parse(normalized).root;
11533
+ let normalized = path18.normalize(String(value).trim());
11534
+ const root = path18.parse(normalized).root;
10560
11535
  while (normalized.length > root.length && /[\\/]$/.test(normalized)) {
10561
11536
  normalized = normalized.slice(0, -1);
10562
11537
  }
@@ -10590,11 +11565,11 @@ function validateConfigLayerShape(rawConfig, filePath) {
10590
11565
  return rawConfig;
10591
11566
  }
10592
11567
  function loadJsonFile(filePath) {
10593
- if (!(0, import_fs11.existsSync)(filePath)) {
11568
+ if (!(0, import_fs12.existsSync)(filePath)) {
10594
11569
  return null;
10595
11570
  }
10596
11571
  try {
10597
- const content = (0, import_fs11.readFileSync)(filePath, "utf-8");
11572
+ const content = (0, import_fs12.readFileSync)(filePath, "utf-8");
10598
11573
  return validateConfigLayerShape(JSON.parse(content), filePath);
10599
11574
  } catch (error) {
10600
11575
  if (error instanceof Error && error.message.startsWith("Config file ")) {
@@ -10609,8 +11584,8 @@ function loadConfigFile(filePath) {
10609
11584
  }
10610
11585
  function materializeLocalProjectConfig(projectRoot, config, host = "opencode") {
10611
11586
  const localConfigPath = resolveWritableProjectConfigPath(projectRoot, host);
10612
- (0, import_fs11.mkdirSync)(path17.dirname(localConfigPath), { recursive: true });
10613
- (0, import_fs11.writeFileSync)(localConfigPath, JSON.stringify(config, null, 2), "utf-8");
11587
+ (0, import_fs12.mkdirSync)(path18.dirname(localConfigPath), { recursive: true });
11588
+ (0, import_fs12.writeFileSync)(localConfigPath, JSON.stringify(config, null, 2), "utf-8");
10614
11589
  return localConfigPath;
10615
11590
  }
10616
11591
  function loadProjectConfigLayer(projectRoot, host = "opencode") {
@@ -10620,7 +11595,7 @@ function loadProjectConfigLayer(projectRoot, host = "opencode") {
10620
11595
  return {};
10621
11596
  }
10622
11597
  const normalizedConfig = { ...projectConfig };
10623
- const projectConfigBaseDir = path17.dirname(path17.dirname(projectConfigPath));
11598
+ const projectConfigBaseDir = path18.dirname(path18.dirname(projectConfigPath));
10624
11599
  if (Array.isArray(normalizedConfig.knowledgeBases)) {
10625
11600
  normalizedConfig.knowledgeBases = resolveInheritedKnowledgeBaseEntries(
10626
11601
  normalizedConfig.knowledgeBases,
@@ -10684,19 +11659,19 @@ function loadMergedConfig(projectRoot, host = "opencode") {
10684
11659
  }
10685
11660
 
10686
11661
  // src/tools/knowledge-base-paths.ts
10687
- var path18 = __toESM(require("path"), 1);
11662
+ var path19 = __toESM(require("path"), 1);
10688
11663
  function resolveConfigPathValue(value, baseDir) {
10689
11664
  const trimmed = value.trim();
10690
11665
  if (!trimmed) {
10691
11666
  return trimmed;
10692
11667
  }
10693
- const absolutePath = path18.isAbsolute(trimmed) ? trimmed : path18.resolve(baseDir, trimmed);
10694
- return path18.normalize(absolutePath);
11668
+ const absolutePath = path19.isAbsolute(trimmed) ? trimmed : path19.resolve(baseDir, trimmed);
11669
+ return path19.normalize(absolutePath);
10695
11670
  }
10696
11671
 
10697
11672
  // src/tools/config-state.ts
10698
- var import_fs12 = require("fs");
10699
- var path19 = __toESM(require("path"), 1);
11673
+ var import_fs13 = require("fs");
11674
+ var path20 = __toESM(require("path"), 1);
10700
11675
  function normalizeKnowledgeBasePaths(config, projectRoot) {
10701
11676
  const normalized = { ...config };
10702
11677
  if (Array.isArray(normalized.knowledgeBases)) {
@@ -10720,6 +11695,24 @@ function loadRuntimeConfig(projectRoot, host = "opencode") {
10720
11695
  var indexerCache = /* @__PURE__ */ new Map();
10721
11696
  var configCache = /* @__PURE__ */ new Map();
10722
11697
  var defaultProjectRoots = /* @__PURE__ */ new Map();
11698
+ function getIndexBusyResult(error) {
11699
+ if (!isIndexLockContentionError(error)) return null;
11700
+ const owner = error.owner;
11701
+ const ownerText = owner ? `PID ${owner.pid}, operation ${owner.operation}, since ${owner.startedAt}` : "unreadable owner";
11702
+ if (error.reason === "legacy-lock") {
11703
+ return {
11704
+ kind: "busy",
11705
+ text: `INDEX_BUSY: legacy lock format detected (${ownerText}). Verify the PID and remove this lock manually only if it is stale.`
11706
+ };
11707
+ }
11708
+ if (error.reason === "unknown-owner") {
11709
+ return {
11710
+ kind: "busy",
11711
+ text: `INDEX_BUSY: unreadable or remote lock owner (${ownerText}). Automatic recovery was refused; manual verification is required.`
11712
+ };
11713
+ }
11714
+ return { kind: "busy", text: `INDEX_BUSY: another index operation is already in progress (${ownerText}).` };
11715
+ }
10723
11716
  function getProjectRoot(projectRoot, host) {
10724
11717
  if (projectRoot) {
10725
11718
  return projectRoot;
@@ -10764,8 +11757,8 @@ function refreshIndexerForDirectory(projectRoot, host = "opencode", config = par
10764
11757
  }
10765
11758
  function shouldForceLocalizeProjectIndex(projectRoot, host = "opencode") {
10766
11759
  const root = getProjectRoot(projectRoot, host);
10767
- const localIndexPath = path20.join(root, getHostProjectIndexRelativePath(host));
10768
- if ((0, import_fs13.existsSync)(localIndexPath)) {
11760
+ const localIndexPath = path21.join(root, getHostProjectIndexRelativePath(host));
11761
+ if ((0, import_fs14.existsSync)(localIndexPath)) {
10769
11762
  return false;
10770
11763
  }
10771
11764
  const inheritedIndexPath = resolveWorktreeFallbackProjectIndexPath(root, host);
@@ -10821,35 +11814,46 @@ async function getCallGraphPath(projectRoot, host, from, to, maxDepth) {
10821
11814
  async function runIndexCodebase(projectRoot, host, args, onProgress) {
10822
11815
  const root = getProjectRoot(projectRoot, host);
10823
11816
  const key = getIndexerCacheKey(root, host);
10824
- const cachedConfig = configCache.get(key);
10825
11817
  let indexer = getIndexerForProject(root, host);
10826
- if (args.estimateOnly) {
10827
- return { kind: "estimate", estimate: await indexer.estimateCost() };
10828
- }
10829
- if (args.force) {
10830
- if (shouldForceLocalizeProjectIndex(root, host)) {
10831
- materializeLocalProjectConfig(root, loadProjectConfigLayer(root, host), host);
10832
- refreshIndexerForDirectory(root, host, cachedConfig);
10833
- indexer = getIndexerForProject(root, host);
10834
- }
10835
- await indexer.clearIndex();
10836
- refreshIndexerForDirectory(root, host, cachedConfig);
10837
- indexer = getIndexerForProject(root, host);
10838
- }
10839
- const stats = await indexer.index((progress) => {
10840
- if (onProgress) {
10841
- return onProgress(formatProgressTitle(progress), {
10842
- phase: progress.phase,
10843
- filesProcessed: progress.filesProcessed,
10844
- totalFiles: progress.totalFiles,
10845
- chunksProcessed: progress.chunksProcessed,
10846
- totalChunks: progress.totalChunks,
10847
- percentage: calculatePercentage(progress)
11818
+ const runtimeConfig = configCache.get(key);
11819
+ try {
11820
+ if (args.estimateOnly) {
11821
+ return { kind: "estimate", estimate: await indexer.estimateCost() };
11822
+ }
11823
+ const runIndex = async (target) => {
11824
+ const operation = args.force ? target.forceIndex.bind(target) : target.index.bind(target);
11825
+ return operation((progress) => {
11826
+ if (onProgress) {
11827
+ return onProgress(formatProgressTitle(progress), {
11828
+ phase: progress.phase,
11829
+ filesProcessed: progress.filesProcessed,
11830
+ totalFiles: progress.totalFiles,
11831
+ chunksProcessed: progress.chunksProcessed,
11832
+ totalChunks: progress.totalChunks,
11833
+ percentage: calculatePercentage(progress)
11834
+ });
11835
+ }
11836
+ return Promise.resolve();
10848
11837
  });
11838
+ };
11839
+ let stats;
11840
+ if (args.force && shouldForceLocalizeProjectIndex(root, host)) {
11841
+ const inheritedIndexPath = resolveProjectIndexPath(root, runtimeConfig.scope, host);
11842
+ stats = await withIndexLock(inheritedIndexPath, "force-index", async () => {
11843
+ materializeLocalProjectConfig(root, loadProjectConfigLayer(root, host), host);
11844
+ refreshIndexerForDirectory(root, host, runtimeConfig);
11845
+ indexer = getIndexerForProject(root, host);
11846
+ return runIndex(indexer);
11847
+ }, { completeRecoveries: false });
11848
+ } else {
11849
+ stats = await runIndex(indexer);
10849
11850
  }
10850
- return Promise.resolve();
10851
- });
10852
- return { kind: "stats", stats };
11851
+ return { kind: "stats", stats };
11852
+ } catch (error) {
11853
+ const busyResult = getIndexBusyResult(error);
11854
+ if (!busyResult) throw error;
11855
+ return busyResult;
11856
+ }
10853
11857
  }
10854
11858
  async function getIndexStatus(projectRoot, host) {
10855
11859
  const indexer = getIndexerForProject(projectRoot, host);
@@ -10859,6 +11863,15 @@ async function getIndexHealthCheck(projectRoot, host) {
10859
11863
  const indexer = getIndexerForProject(projectRoot, host);
10860
11864
  return indexer.healthCheck();
10861
11865
  }
11866
+ async function runIndexHealthCheck(projectRoot, host) {
11867
+ try {
11868
+ return { kind: "health", health: await getIndexHealthCheck(projectRoot, host) };
11869
+ } catch (error) {
11870
+ const busyResult = getIndexBusyResult(error);
11871
+ if (!busyResult) throw error;
11872
+ return busyResult;
11873
+ }
11874
+ }
10862
11875
  async function getPrImpact(projectRoot, host, params) {
10863
11876
  const indexer = getIndexerForProject(projectRoot, host);
10864
11877
  return indexer.getPrImpact({
@@ -10939,20 +11952,87 @@ var CHUNK_TYPE_ENUM = [
10939
11952
  ];
10940
11953
 
10941
11954
  // src/mcp-server/register-tools.ts
11955
+ function allowNullAsUndefined(schema) {
11956
+ return import_zod2.z.preprocess((value) => value === null ? void 0 : value, schema);
11957
+ }
10942
11958
  function registerMcpTools(server, runtime) {
11959
+ server.tool(
11960
+ "codebase_context",
11961
+ "PREFERRED FIRST TOOL for any question about this repository. Use before built-in code search, grep, shell search, or broad file reads. Provide from+to for a dependency path, symbol for a definition, or only query for low-token conceptual discovery. Use call_graph directly for callers or callees.",
11962
+ {
11963
+ query: import_zod2.z.string().describe("The codebase question or behavior to locate. Always provide the user's repository question here."),
11964
+ from: allowNullAsUndefined(import_zod2.z.string().optional()).describe("Source symbol. For dependency-path questions, extract the first endpoint and provide it here."),
11965
+ to: allowNullAsUndefined(import_zod2.z.string().optional()).describe("Target symbol. For dependency-path questions, extract the second endpoint and provide it here."),
11966
+ symbol: allowNullAsUndefined(import_zod2.z.string().optional()).describe("Exact symbol for an authoritative definition lookup. Omit when from and to are supplied."),
11967
+ limit: allowNullAsUndefined(import_zod2.z.number().optional().default(10)).describe("Maximum number of search or definition results"),
11968
+ maxDepth: allowNullAsUndefined(import_zod2.z.number().optional().default(10)).describe("Maximum call-graph traversal depth for from/to path lookup"),
11969
+ fileType: allowNullAsUndefined(import_zod2.z.string().optional()).describe("Filter by file extension (e.g., 'ts', 'py', 'rs')"),
11970
+ directory: allowNullAsUndefined(import_zod2.z.string().optional()).describe("Filter by directory path (e.g., 'src/utils', 'lib')")
11971
+ },
11972
+ async (args) => {
11973
+ if (args.from && args.to) {
11974
+ const path27 = await getCallGraphPath(runtime.projectRoot, runtime.host, args.from, args.to, args.maxDepth);
11975
+ if (path27.length > 0) {
11976
+ return { content: [{ type: "text", text: formatCallGraphPath(args.from, args.to, path27) }] };
11977
+ }
11978
+ const { callers } = await getCallGraphData(runtime.projectRoot, runtime.host, {
11979
+ name: args.to,
11980
+ direction: "callers"
11981
+ });
11982
+ const directEdge = callers.find((edge) => edge.fromSymbolName === args.from);
11983
+ if (directEdge) {
11984
+ const location = directEdge.fromSymbolFilePath ? ` at ${directEdge.fromSymbolFilePath}:${directEdge.line}` : "";
11985
+ return {
11986
+ content: [{
11987
+ type: "text",
11988
+ text: `Direct path: ${args.from} --${directEdge.callType}--> ${args.to}${location} (edge is ${directEdge.isResolved ? "resolved" : "unresolved"}).`
11989
+ }]
11990
+ };
11991
+ }
11992
+ return { content: [{ type: "text", text: formatCallGraphPath(args.from, args.to, path27) }] };
11993
+ }
11994
+ if (args.symbol) {
11995
+ const results2 = await implementationLookup(runtime.projectRoot, runtime.host, args.symbol, {
11996
+ limit: args.limit ?? 10,
11997
+ fileType: args.fileType,
11998
+ directory: args.directory
11999
+ });
12000
+ return { content: [{ type: "text", text: formatDefinitionLookup(results2, args.symbol) }] };
12001
+ }
12002
+ const results = await searchCodebase(runtime.projectRoot, runtime.host, args.query, {
12003
+ limit: args.limit ?? 10,
12004
+ fileType: args.fileType,
12005
+ directory: args.directory,
12006
+ metadataOnly: true
12007
+ });
12008
+ if (results.length === 0) {
12009
+ return { content: [{ type: "text", text: "No matching code found. Try a different query or run index_codebase first." }] };
12010
+ }
12011
+ return {
12012
+ content: [{
12013
+ type: "text",
12014
+ text: `Found ${results.length} locations for "${args.query}":
12015
+
12016
+ ${formatCodebasePeek(results)}
12017
+
12018
+ Use implementation_lookup for an authoritative definition, or call call_graph/call_graph_path once you have a symbol.`
12019
+ }]
12020
+ };
12021
+ }
12022
+ );
10943
12023
  server.tool(
10944
12024
  "codebase_search",
10945
- "Search codebase by MEANING, not keywords. Returns full code content. For just finding WHERE code is (saves ~90% tokens), use codebase_peek instead.",
12025
+ "FULL-CONTENT semantic retrieval. Use after codebase_peek when you need implementation text, not as the default first step. For exact identifiers or exhaustive matches use grep instead.",
10946
12026
  {
10947
12027
  query: import_zod2.z.string().describe("Natural language description of what code you're looking for. Describe behavior, not syntax."),
10948
- limit: import_zod2.z.number().optional().default(5).describe("Maximum number of results to return"),
10949
- fileType: import_zod2.z.string().optional().describe("Filter by file extension (e.g., 'ts', 'py', 'rs')"),
10950
- directory: import_zod2.z.string().optional().describe("Filter by directory path (e.g., 'src/utils', 'lib')"),
10951
- chunkType: import_zod2.z.enum(CHUNK_TYPE_ENUM).optional().describe("Filter by code chunk type"),
10952
- contextLines: import_zod2.z.number().optional().describe("Number of extra lines to include before/after each match (default: 0)"),
10953
- blameAuthor: import_zod2.z.string().optional().describe("Filter by git blame author name or email"),
10954
- blameSha: import_zod2.z.string().optional().describe("Filter by git blame commit SHA or prefix"),
10955
- blameSince: import_zod2.z.string().optional().describe("Filter to chunks last changed on or after this date")
12028
+ limit: allowNullAsUndefined(import_zod2.z.number().optional().default(5)).describe("Maximum number of results to return"),
12029
+ fileType: allowNullAsUndefined(import_zod2.z.string().optional()).describe("Filter by file extension (e.g., 'ts', 'py', 'rs')"),
12030
+ directory: allowNullAsUndefined(import_zod2.z.string().optional()).describe("Filter by directory path (e.g., 'src/utils', 'lib')"),
12031
+ chunkType: allowNullAsUndefined(import_zod2.z.enum(CHUNK_TYPE_ENUM).optional()).describe("Filter by code chunk type"),
12032
+ contextLines: allowNullAsUndefined(import_zod2.z.number().optional()).describe("Number of extra lines to include before/after each match (default: 0)"),
12033
+ blameAuthor: allowNullAsUndefined(import_zod2.z.string().optional()).describe("Filter by git blame author name or email"),
12034
+ blameSha: allowNullAsUndefined(import_zod2.z.string().optional()).describe("Filter by git blame commit SHA or prefix"),
12035
+ blameSince: allowNullAsUndefined(import_zod2.z.string().optional()).describe("Filter to chunks last changed on or after this date")
10956
12036
  },
10957
12037
  async (args) => {
10958
12038
  const results = await searchCodebase(runtime.projectRoot, runtime.host, args.query, {
@@ -10975,16 +12055,16 @@ ${formatSearchResults(results, "score")}` }] };
10975
12055
  );
10976
12056
  server.tool(
10977
12057
  "codebase_peek",
10978
- "Quick lookup of code locations by meaning. Returns only metadata (file, line, name, type) WITHOUT code content. Saves ~90% tokens vs codebase_search.",
12058
+ "DIRECT LOW-TOKEN semantic location lookup for unfamiliar-code discovery. Prefer codebase_context when the request may involve definitions or graph navigation; use this specialized tool when you only need conceptual locations.",
10979
12059
  {
10980
12060
  query: import_zod2.z.string().describe("Natural language description of what code you're looking for."),
10981
- limit: import_zod2.z.number().optional().default(10).describe("Maximum number of results to return"),
10982
- fileType: import_zod2.z.string().optional().describe("Filter by file extension (e.g., 'ts', 'py', 'rs')"),
10983
- directory: import_zod2.z.string().optional().describe("Filter by directory path (e.g., 'src/utils', 'lib')"),
10984
- chunkType: import_zod2.z.enum(CHUNK_TYPE_ENUM).optional().describe("Filter by code chunk type"),
10985
- blameAuthor: import_zod2.z.string().optional().describe("Filter by git blame author name or email"),
10986
- blameSha: import_zod2.z.string().optional().describe("Filter by git blame commit SHA or prefix"),
10987
- blameSince: import_zod2.z.string().optional().describe("Filter to chunks last changed on or after this date")
12061
+ limit: allowNullAsUndefined(import_zod2.z.number().optional().default(10)).describe("Maximum number of results to return"),
12062
+ fileType: allowNullAsUndefined(import_zod2.z.string().optional()).describe("Filter by file extension (e.g., 'ts', 'py', 'rs')"),
12063
+ directory: allowNullAsUndefined(import_zod2.z.string().optional()).describe("Filter by directory path (e.g., 'src/utils', 'lib')"),
12064
+ chunkType: allowNullAsUndefined(import_zod2.z.enum(CHUNK_TYPE_ENUM).optional()).describe("Filter by code chunk type"),
12065
+ blameAuthor: allowNullAsUndefined(import_zod2.z.string().optional()).describe("Filter by git blame author name or email"),
12066
+ blameSha: allowNullAsUndefined(import_zod2.z.string().optional()).describe("Filter by git blame commit SHA or prefix"),
12067
+ blameSince: allowNullAsUndefined(import_zod2.z.string().optional()).describe("Filter to chunks last changed on or after this date")
10988
12068
  },
10989
12069
  async (args) => {
10990
12070
  const results = await searchCodebase(runtime.projectRoot, runtime.host, args.query, {
@@ -11009,21 +12089,26 @@ Use Read tool to examine specific files.` }] };
11009
12089
  );
11010
12090
  server.tool(
11011
12091
  "index_codebase",
11012
- "Index the codebase for semantic search. Creates vector embeddings of code chunks. Incremental - only re-indexes changed files. Run before first codebase_search.",
12092
+ "Create or refresh the semantic index. Call index_status first when readiness is unknown, then use this tool only if the index is missing, stale, or incompatible. Incremental by default; force=true rebuilds everything.",
11013
12093
  {
11014
- force: import_zod2.z.boolean().optional().default(false).describe("Force reindex even if already indexed"),
11015
- estimateOnly: import_zod2.z.boolean().optional().default(false).describe("Only show cost estimate without indexing"),
11016
- verbose: import_zod2.z.boolean().optional().default(false).describe("Show detailed info about skipped files and parsing failures")
12094
+ force: allowNullAsUndefined(import_zod2.z.boolean().optional().default(false)).describe("Force reindex even if already indexed"),
12095
+ estimateOnly: allowNullAsUndefined(import_zod2.z.boolean().optional().default(false)).describe("Only show cost estimate without indexing"),
12096
+ verbose: allowNullAsUndefined(import_zod2.z.boolean().optional().default(false)).describe("Show detailed info about skipped files and parsing failures")
11017
12097
  },
11018
12098
  async (args) => {
11019
12099
  const result = await runIndexCodebase(runtime.projectRoot, runtime.host, args);
11020
- const text = result.kind === "estimate" ? formatCostEstimate(result.estimate) : formatIndexStats(result.stats, args.verbose ?? false);
11021
- return { content: [{ type: "text", text }] };
12100
+ if (result.kind === "estimate") {
12101
+ return { content: [{ type: "text", text: formatCostEstimate(result.estimate) }] };
12102
+ }
12103
+ if (result.kind === "busy") {
12104
+ return { content: [{ type: "text", text: result.text }], isError: true };
12105
+ }
12106
+ return { content: [{ type: "text", text: formatIndexStats(result.stats, args.verbose ?? false) }] };
11022
12107
  }
11023
12108
  );
11024
12109
  server.tool(
11025
12110
  "index_status",
11026
- "Check the status of the codebase index. Shows whether the codebase is indexed, how many chunks are stored, and the embedding provider being used.",
12111
+ "START HERE once per repository task when index readiness or freshness is unknown. Reports whether semantic retrieval is ready, chunk counts, compatibility, and the embedding provider. If ready, continue with codebase_peek or implementation_lookup; otherwise run index_codebase.",
11027
12112
  {},
11028
12113
  async () => {
11029
12114
  const status = await getIndexStatus(runtime.projectRoot, runtime.host);
@@ -11035,8 +12120,11 @@ Use Read tool to examine specific files.` }] };
11035
12120
  "Check index health and remove stale entries from deleted files. Run this to clean up the index after files have been deleted.",
11036
12121
  {},
11037
12122
  async () => {
11038
- const result = await getIndexHealthCheck(runtime.projectRoot, runtime.host);
11039
- return { content: [{ type: "text", text: formatHealthCheck(result) }] };
12123
+ const result = await runIndexHealthCheck(runtime.projectRoot, runtime.host);
12124
+ if (result.kind === "busy") {
12125
+ return { content: [{ type: "text", text: result.text }], isError: true };
12126
+ }
12127
+ return { content: [{ type: "text", text: formatHealthCheck(result.health) }] };
11040
12128
  }
11041
12129
  );
11042
12130
  server.tool(
@@ -11052,9 +12140,13 @@ Use Read tool to examine specific files.` }] };
11052
12140
  "index_logs",
11053
12141
  "Get recent debug logs from the codebase indexer. Requires debug.enabled=true in config.",
11054
12142
  {
11055
- limit: import_zod2.z.number().optional().default(20).describe("Maximum number of log entries to return"),
11056
- category: import_zod2.z.enum(["search", "embedding", "cache", "gc", "branch", "general"]).optional().describe("Filter by log category"),
11057
- level: import_zod2.z.enum(["error", "warn", "info", "debug"]).optional().describe("Filter by minimum log level")
12143
+ limit: allowNullAsUndefined(import_zod2.z.number().optional().default(20)).describe("Maximum number of log entries to return"),
12144
+ category: allowNullAsUndefined(
12145
+ import_zod2.z.enum(["search", "embedding", "cache", "gc", "branch", "general"]).optional()
12146
+ ).describe("Filter by log category"),
12147
+ level: allowNullAsUndefined(
12148
+ import_zod2.z.enum(["error", "warn", "info", "debug"]).optional()
12149
+ ).describe("Filter by minimum log level")
11058
12150
  },
11059
12151
  async (args) => {
11060
12152
  const result = await getIndexLogs(runtime.projectRoot, runtime.host, args);
@@ -11063,14 +12155,14 @@ Use Read tool to examine specific files.` }] };
11063
12155
  );
11064
12156
  server.tool(
11065
12157
  "find_similar",
11066
- "Find code similar to a given snippet. Use for duplicate detection, pattern discovery, or refactoring prep.",
12158
+ "Use when you already have a code snippet and need analogous implementations, duplicates, patterns, or refactoring candidates. For a natural-language concept without example code, start with codebase_peek instead.",
11067
12159
  {
11068
12160
  code: import_zod2.z.string().describe("The code snippet to find similar code for"),
11069
- limit: import_zod2.z.number().optional().default(10).describe("Maximum number of results to return"),
11070
- fileType: import_zod2.z.string().optional().describe("Filter by file extension (e.g., 'ts', 'py', 'rs')"),
11071
- directory: import_zod2.z.string().optional().describe("Filter by directory path (e.g., 'src/utils', 'lib')"),
11072
- chunkType: import_zod2.z.enum(CHUNK_TYPE_ENUM).optional().describe("Filter by code chunk type"),
11073
- excludeFile: import_zod2.z.string().optional().describe("Exclude results from this file path")
12161
+ limit: allowNullAsUndefined(import_zod2.z.number().optional().default(10)).describe("Maximum number of results to return"),
12162
+ fileType: allowNullAsUndefined(import_zod2.z.string().optional()).describe("Filter by file extension (e.g., 'ts', 'py', 'rs')"),
12163
+ directory: allowNullAsUndefined(import_zod2.z.string().optional()).describe("Filter by directory path (e.g., 'src/utils', 'lib')"),
12164
+ chunkType: allowNullAsUndefined(import_zod2.z.enum(CHUNK_TYPE_ENUM).optional()).describe("Filter by code chunk type"),
12165
+ excludeFile: allowNullAsUndefined(import_zod2.z.string().optional()).describe("Exclude results from this file path")
11074
12166
  },
11075
12167
  async (args) => {
11076
12168
  const results = await findSimilarCode(runtime.projectRoot, runtime.host, args.code, {
@@ -11090,12 +12182,12 @@ ${formatSearchResults(results)}` }] };
11090
12182
  );
11091
12183
  server.tool(
11092
12184
  "implementation_lookup",
11093
- "Jump to symbol definition. Find WHERE something is defined. Returns the authoritative source location(s). Prefers real implementation files over tests, docs, examples, and fixtures.",
12185
+ "FIRST TOOL only for known-symbol definition questions. Returns authoritative source locations and prefers implementations over tests, docs, examples, and fixtures. Do not use for callers, callees, dependency paths, or code flow; use codebase_context with direction or from/to for those questions.",
11094
12186
  {
11095
12187
  query: import_zod2.z.string().describe("Symbol name or natural language description (e.g., 'validateToken', 'where is the payment handler defined')"),
11096
- limit: import_zod2.z.number().optional().default(5).describe("Maximum number of results"),
11097
- fileType: import_zod2.z.string().optional().describe("Filter by file extension (e.g., 'ts', 'py')"),
11098
- directory: import_zod2.z.string().optional().describe("Filter by directory path (e.g., 'src/utils')")
12188
+ limit: allowNullAsUndefined(import_zod2.z.number().optional().default(5)).describe("Maximum number of results"),
12189
+ fileType: allowNullAsUndefined(import_zod2.z.string().optional()).describe("Filter by file extension (e.g., 'ts', 'py')"),
12190
+ directory: allowNullAsUndefined(import_zod2.z.string().optional()).describe("Filter by directory path (e.g., 'src/utils')")
11099
12191
  },
11100
12192
  async (args) => {
11101
12193
  const results = await implementationLookup(runtime.projectRoot, runtime.host, args.query, {
@@ -11108,12 +12200,16 @@ ${formatSearchResults(results)}` }] };
11108
12200
  );
11109
12201
  server.tool(
11110
12202
  "call_graph",
11111
- "Query the call graph to find callers or callees of a function/method. Use to understand code flow and dependencies. Supports relationship types: Call, MethodCall, Constructor, Import, Inherits, Implements.",
12203
+ "Use after identifying a symbol to find its direct callers or callees and understand code flow. Use implementation_lookup first if the symbol or definition is still ambiguous. Supports relationship types: Call, MethodCall, Constructor, Import, Inherits, Implements.",
11112
12204
  {
11113
12205
  name: import_zod2.z.string().describe("Function or method name to query"),
11114
- direction: import_zod2.z.enum(["callers", "callees"]).default("callers").describe("Direction: 'callers' finds who calls this function, 'callees' finds what this function calls"),
11115
- symbolId: import_zod2.z.string().optional().describe("Symbol ID (required for 'callees' direction)"),
11116
- relationshipType: import_zod2.z.enum(["Call", "MethodCall", "Constructor", "Import", "Inherits", "Implements"]).optional().describe("Filter by relationship type. Omit to show all.")
12206
+ direction: allowNullAsUndefined(
12207
+ import_zod2.z.enum(["callers", "callees"]).default("callers")
12208
+ ).describe("Direction: 'callers' finds who calls this function, 'callees' finds what this function calls"),
12209
+ symbolId: allowNullAsUndefined(import_zod2.z.string().optional()).describe("Symbol ID (required for 'callees' direction)"),
12210
+ relationshipType: allowNullAsUndefined(
12211
+ import_zod2.z.enum(["Call", "MethodCall", "Constructor", "Import", "Inherits", "Implements"]).optional()
12212
+ ).describe("Filter by relationship type. Omit to show all.")
11117
12213
  },
11118
12214
  async (args) => {
11119
12215
  if (args.direction === "callees") {
@@ -11129,27 +12225,29 @@ ${formatSearchResults(results)}` }] };
11129
12225
  );
11130
12226
  server.tool(
11131
12227
  "call_graph_path",
11132
- "Find the shortest connection path between two symbols in the call graph. Returns the chain of calls connecting them.",
12228
+ "Use after identifying both endpoint symbols to find the shortest known call path between them. Use codebase_peek or implementation_lookup first when either endpoint is unknown.",
11133
12229
  {
11134
12230
  from: import_zod2.z.string().describe("Source function/method name (starting point)"),
11135
12231
  to: import_zod2.z.string().describe("Target function/method name (destination)"),
11136
- maxDepth: import_zod2.z.number().optional().default(10).describe("Maximum traversal depth (default: 10)")
12232
+ maxDepth: allowNullAsUndefined(import_zod2.z.number().optional().default(10)).describe("Maximum traversal depth (default: 10)")
11137
12233
  },
11138
12234
  async (args) => {
11139
- const path26 = await getCallGraphPath(runtime.projectRoot, runtime.host, args.from, args.to, args.maxDepth);
11140
- return { content: [{ type: "text", text: formatCallGraphPath(args.from, args.to, path26) }] };
12235
+ const path27 = await getCallGraphPath(runtime.projectRoot, runtime.host, args.from, args.to, args.maxDepth);
12236
+ return { content: [{ type: "text", text: formatCallGraphPath(args.from, args.to, path27) }] };
11141
12237
  }
11142
12238
  );
11143
12239
  server.tool(
11144
12240
  "pr_impact",
11145
- "Analyze the impact of a pull request or branch by examining changed files, affected symbols, transitive callers, communities touched, hub nodes, and risk level. Use to understand blast radius before merging.",
12241
+ "FIRST TOOL for pull-request or branch blast-radius questions. Analyzes changed files, affected symbols, transitive dependencies, communities, hub nodes, conflicts, and risk before merging.",
11146
12242
  {
11147
- pr: import_zod2.z.number().optional().describe("Pull request number to analyze"),
11148
- branch: import_zod2.z.string().optional().describe("Branch name to analyze (defaults to current branch)"),
11149
- maxDepth: import_zod2.z.number().optional().default(5).describe("Maximum traversal depth for transitive callers (default: 5)"),
11150
- hubThreshold: import_zod2.z.number().optional().default(10).describe("Minimum caller count to flag a symbol as a hub node (default: 10)"),
11151
- checkConflicts: import_zod2.z.boolean().optional().default(false).describe("Check for conflicting open PRs touching the same communities (default: false)"),
11152
- direction: import_zod2.z.enum(["callers", "callees", "both"]).optional().default("both").describe("Call-graph traversal direction: 'callers' for upstream, 'callees' for downstream, 'both' for union (default: both)")
12243
+ pr: allowNullAsUndefined(import_zod2.z.number().optional()).describe("Pull request number to analyze"),
12244
+ branch: allowNullAsUndefined(import_zod2.z.string().optional()).describe("Branch name to analyze (defaults to current branch)"),
12245
+ maxDepth: allowNullAsUndefined(import_zod2.z.number().optional().default(5)).describe("Maximum traversal depth for transitive callers (default: 5)"),
12246
+ hubThreshold: allowNullAsUndefined(import_zod2.z.number().optional().default(10)).describe("Minimum caller count to flag a symbol as a hub node (default: 10)"),
12247
+ checkConflicts: allowNullAsUndefined(import_zod2.z.boolean().optional().default(false)).describe("Check for conflicting open PRs touching the same communities (default: false)"),
12248
+ direction: allowNullAsUndefined(
12249
+ import_zod2.z.enum(["callers", "callees", "both"]).optional().default("both")
12250
+ ).describe("Call-graph traversal direction: 'callers' for upstream, 'callees' for downstream, 'both' for union (default: both)")
11153
12251
  },
11154
12252
  async (args) => {
11155
12253
  try {
@@ -11172,8 +12270,12 @@ ${formatSearchResults(results)}` }] };
11172
12270
 
11173
12271
  // src/mcp-server.ts
11174
12272
  var import_meta2 = {};
12273
+ function getServerInstructions(host) {
12274
+ const hostText = `host ${host}`;
12275
+ return `This MCP server is the preferred codebase-understanding path for ${hostText}. Start a repository task with index_status when index readiness or freshness is unknown. Use codebase_context as the preferred first entry point because it returns low-token locations first and routes to definitions or call-graph helpers when symbol intent is present. Use codebase_peek for direct conceptual location lookup, implementation_lookup for known-symbol definition questions, and codebase_search only when full semantic code content is needed. For exact identifiers or exhaustive matches, use grep. After identifying symbols, use call_graph or call_graph_path to trace dependencies. If the index is unavailable, run index_codebase, then retry the retrieval tool.`;
12276
+ }
11175
12277
  function getPackageVersion() {
11176
- const raw = JSON.parse((0, import_fs14.readFileSync)(new URL("../package.json", import_meta2.url), "utf-8"));
12278
+ const raw = JSON.parse((0, import_fs15.readFileSync)(new URL("../package.json", import_meta2.url), "utf-8"));
11177
12279
  if (raw && typeof raw === "object" && "version" in raw && typeof raw.version === "string") {
11178
12280
  return raw.version;
11179
12281
  }
@@ -11183,6 +12285,8 @@ function createMcpServer(projectRoot, config, host = "opencode") {
11183
12285
  const server = new import_mcp.McpServer({
11184
12286
  name: "opencode-codebase-index",
11185
12287
  version: getPackageVersion()
12288
+ }, {
12289
+ instructions: getServerInstructions(host)
11186
12290
  });
11187
12291
  initializeTools(projectRoot, config, host);
11188
12292
  registerMcpTools(server, {
@@ -11194,18 +12298,37 @@ function createMcpServer(projectRoot, config, host = "opencode") {
11194
12298
  }
11195
12299
 
11196
12300
  // src/utils/auto-index.ts
12301
+ var INITIAL_RETRY_DELAY_MS = 50;
12302
+ var MAX_RETRY_DELAY_MS = 500;
12303
+ var autoIndexStates = /* @__PURE__ */ new WeakMap();
11197
12304
  function getErrorMessage3(error) {
11198
12305
  return error instanceof Error ? error.message : String(error);
11199
12306
  }
11200
- function startAutoIndex(indexer, projectRoot) {
11201
- indexer.initialize().then(() => {
11202
- indexer.index().catch((error) => {
11203
- console.error(`[codebase-index] Auto-index failed for "${projectRoot}": ${getErrorMessage3(error)}`);
11204
- });
12307
+ function runAutoIndex(indexer, projectRoot, state) {
12308
+ void indexer.index().then(() => {
12309
+ autoIndexStates.delete(indexer);
11205
12310
  }).catch((error) => {
11206
- console.error(`[codebase-index] Auto-index initialization failed for "${projectRoot}": ${getErrorMessage3(error)}`);
12311
+ if (!isTransientIndexLockContention(error)) {
12312
+ autoIndexStates.delete(indexer);
12313
+ console.error(`[codebase-index] Auto-index failed for "${projectRoot}": ${getErrorMessage3(error)}`);
12314
+ return;
12315
+ }
12316
+ const retryDelayMs = state.retryDelayMs;
12317
+ state.retryDelayMs = Math.min(retryDelayMs * 2, MAX_RETRY_DELAY_MS);
12318
+ const retryTimer = setTimeout(() => {
12319
+ runAutoIndex(indexer, projectRoot, state);
12320
+ }, retryDelayMs);
12321
+ retryTimer.unref?.();
11207
12322
  });
11208
12323
  }
12324
+ function startAutoIndex(indexer, projectRoot) {
12325
+ if (autoIndexStates.has(indexer)) return;
12326
+ const state = {
12327
+ retryDelayMs: INITIAL_RETRY_DELAY_MS
12328
+ };
12329
+ autoIndexStates.set(indexer, state);
12330
+ runAutoIndex(indexer, projectRoot, state);
12331
+ }
11209
12332
 
11210
12333
  // node_modules/chokidar/index.js
11211
12334
  var import_node_events = require("events");
@@ -11297,7 +12420,7 @@ var ReaddirpStream = class extends import_node_stream.Readable {
11297
12420
  this._directoryFilter = normalizeFilter(opts.directoryFilter);
11298
12421
  const statMethod = opts.lstat ? import_promises.lstat : import_promises.stat;
11299
12422
  if (wantBigintFsStats) {
11300
- this._stat = (path26) => statMethod(path26, { bigint: true });
12423
+ this._stat = (path27) => statMethod(path27, { bigint: true });
11301
12424
  } else {
11302
12425
  this._stat = statMethod;
11303
12426
  }
@@ -11322,8 +12445,8 @@ var ReaddirpStream = class extends import_node_stream.Readable {
11322
12445
  const par = this.parent;
11323
12446
  const fil = par && par.files;
11324
12447
  if (fil && fil.length > 0) {
11325
- const { path: path26, depth } = par;
11326
- const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path26));
12448
+ const { path: path27, depth } = par;
12449
+ const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path27));
11327
12450
  const awaited = await Promise.all(slice);
11328
12451
  for (const entry of awaited) {
11329
12452
  if (!entry)
@@ -11363,20 +12486,20 @@ var ReaddirpStream = class extends import_node_stream.Readable {
11363
12486
  this.reading = false;
11364
12487
  }
11365
12488
  }
11366
- async _exploreDir(path26, depth) {
12489
+ async _exploreDir(path27, depth) {
11367
12490
  let files;
11368
12491
  try {
11369
- files = await (0, import_promises.readdir)(path26, this._rdOptions);
12492
+ files = await (0, import_promises.readdir)(path27, this._rdOptions);
11370
12493
  } catch (error) {
11371
12494
  this._onError(error);
11372
12495
  }
11373
- return { files, depth, path: path26 };
12496
+ return { files, depth, path: path27 };
11374
12497
  }
11375
- async _formatEntry(dirent, path26) {
12498
+ async _formatEntry(dirent, path27) {
11376
12499
  let entry;
11377
12500
  const basename5 = this._isDirent ? dirent.name : dirent;
11378
12501
  try {
11379
- const fullPath = (0, import_node_path.resolve)((0, import_node_path.join)(path26, basename5));
12502
+ const fullPath = (0, import_node_path.resolve)((0, import_node_path.join)(path27, basename5));
11380
12503
  entry = { path: (0, import_node_path.relative)(this._root, fullPath), fullPath, basename: basename5 };
11381
12504
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
11382
12505
  } catch (err) {
@@ -11776,16 +12899,16 @@ var delFromSet = (main2, prop, item) => {
11776
12899
  };
11777
12900
  var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
11778
12901
  var FsWatchInstances = /* @__PURE__ */ new Map();
11779
- function createFsWatchInstance(path26, options, listener, errHandler, emitRaw) {
12902
+ function createFsWatchInstance(path27, options, listener, errHandler, emitRaw) {
11780
12903
  const handleEvent = (rawEvent, evPath) => {
11781
- listener(path26);
11782
- emitRaw(rawEvent, evPath, { watchedPath: path26 });
11783
- if (evPath && path26 !== evPath) {
11784
- fsWatchBroadcast(sp.resolve(path26, evPath), KEY_LISTENERS, sp.join(path26, evPath));
12904
+ listener(path27);
12905
+ emitRaw(rawEvent, evPath, { watchedPath: path27 });
12906
+ if (evPath && path27 !== evPath) {
12907
+ fsWatchBroadcast(sp.resolve(path27, evPath), KEY_LISTENERS, sp.join(path27, evPath));
11785
12908
  }
11786
12909
  };
11787
12910
  try {
11788
- return (0, import_node_fs.watch)(path26, {
12911
+ return (0, import_node_fs.watch)(path27, {
11789
12912
  persistent: options.persistent
11790
12913
  }, handleEvent);
11791
12914
  } catch (error) {
@@ -11801,12 +12924,12 @@ var fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
11801
12924
  listener(val1, val2, val3);
11802
12925
  });
11803
12926
  };
11804
- var setFsWatchListener = (path26, fullPath, options, handlers) => {
12927
+ var setFsWatchListener = (path27, fullPath, options, handlers) => {
11805
12928
  const { listener, errHandler, rawEmitter } = handlers;
11806
12929
  let cont = FsWatchInstances.get(fullPath);
11807
12930
  let watcher;
11808
12931
  if (!options.persistent) {
11809
- watcher = createFsWatchInstance(path26, options, listener, errHandler, rawEmitter);
12932
+ watcher = createFsWatchInstance(path27, options, listener, errHandler, rawEmitter);
11810
12933
  if (!watcher)
11811
12934
  return;
11812
12935
  return watcher.close.bind(watcher);
@@ -11817,7 +12940,7 @@ var setFsWatchListener = (path26, fullPath, options, handlers) => {
11817
12940
  addAndConvert(cont, KEY_RAW, rawEmitter);
11818
12941
  } else {
11819
12942
  watcher = createFsWatchInstance(
11820
- path26,
12943
+ path27,
11821
12944
  options,
11822
12945
  fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS),
11823
12946
  errHandler,
@@ -11832,7 +12955,7 @@ var setFsWatchListener = (path26, fullPath, options, handlers) => {
11832
12955
  cont.watcherUnusable = true;
11833
12956
  if (isWindows && error.code === "EPERM") {
11834
12957
  try {
11835
- const fd = await (0, import_promises2.open)(path26, "r");
12958
+ const fd = await (0, import_promises2.open)(path27, "r");
11836
12959
  await fd.close();
11837
12960
  broadcastErr(error);
11838
12961
  } catch (err) {
@@ -11863,7 +12986,7 @@ var setFsWatchListener = (path26, fullPath, options, handlers) => {
11863
12986
  };
11864
12987
  };
11865
12988
  var FsWatchFileInstances = /* @__PURE__ */ new Map();
11866
- var setFsWatchFileListener = (path26, fullPath, options, handlers) => {
12989
+ var setFsWatchFileListener = (path27, fullPath, options, handlers) => {
11867
12990
  const { listener, rawEmitter } = handlers;
11868
12991
  let cont = FsWatchFileInstances.get(fullPath);
11869
12992
  const copts = cont && cont.options;
@@ -11885,7 +13008,7 @@ var setFsWatchFileListener = (path26, fullPath, options, handlers) => {
11885
13008
  });
11886
13009
  const currmtime = curr.mtimeMs;
11887
13010
  if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
11888
- foreach(cont.listeners, (listener2) => listener2(path26, curr));
13011
+ foreach(cont.listeners, (listener2) => listener2(path27, curr));
11889
13012
  }
11890
13013
  })
11891
13014
  };
@@ -11915,13 +13038,13 @@ var NodeFsHandler = class {
11915
13038
  * @param listener on fs change
11916
13039
  * @returns closer for the watcher instance
11917
13040
  */
11918
- _watchWithNodeFs(path26, listener) {
13041
+ _watchWithNodeFs(path27, listener) {
11919
13042
  const opts = this.fsw.options;
11920
- const directory = sp.dirname(path26);
11921
- const basename5 = sp.basename(path26);
13043
+ const directory = sp.dirname(path27);
13044
+ const basename5 = sp.basename(path27);
11922
13045
  const parent = this.fsw._getWatchedDir(directory);
11923
13046
  parent.add(basename5);
11924
- const absolutePath = sp.resolve(path26);
13047
+ const absolutePath = sp.resolve(path27);
11925
13048
  const options = {
11926
13049
  persistent: opts.persistent
11927
13050
  };
@@ -11931,12 +13054,12 @@ var NodeFsHandler = class {
11931
13054
  if (opts.usePolling) {
11932
13055
  const enableBin = opts.interval !== opts.binaryInterval;
11933
13056
  options.interval = enableBin && isBinaryPath(basename5) ? opts.binaryInterval : opts.interval;
11934
- closer = setFsWatchFileListener(path26, absolutePath, options, {
13057
+ closer = setFsWatchFileListener(path27, absolutePath, options, {
11935
13058
  listener,
11936
13059
  rawEmitter: this.fsw._emitRaw
11937
13060
  });
11938
13061
  } else {
11939
- closer = setFsWatchListener(path26, absolutePath, options, {
13062
+ closer = setFsWatchListener(path27, absolutePath, options, {
11940
13063
  listener,
11941
13064
  errHandler: this._boundHandleError,
11942
13065
  rawEmitter: this.fsw._emitRaw
@@ -11958,7 +13081,7 @@ var NodeFsHandler = class {
11958
13081
  let prevStats = stats;
11959
13082
  if (parent.has(basename5))
11960
13083
  return;
11961
- const listener = async (path26, newStats) => {
13084
+ const listener = async (path27, newStats) => {
11962
13085
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
11963
13086
  return;
11964
13087
  if (!newStats || newStats.mtimeMs === 0) {
@@ -11972,11 +13095,11 @@ var NodeFsHandler = class {
11972
13095
  this.fsw._emit(EV.CHANGE, file, newStats2);
11973
13096
  }
11974
13097
  if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats2.ino) {
11975
- this.fsw._closeFile(path26);
13098
+ this.fsw._closeFile(path27);
11976
13099
  prevStats = newStats2;
11977
13100
  const closer2 = this._watchWithNodeFs(file, listener);
11978
13101
  if (closer2)
11979
- this.fsw._addPathCloser(path26, closer2);
13102
+ this.fsw._addPathCloser(path27, closer2);
11980
13103
  } else {
11981
13104
  prevStats = newStats2;
11982
13105
  }
@@ -12008,7 +13131,7 @@ var NodeFsHandler = class {
12008
13131
  * @param item basename of this item
12009
13132
  * @returns true if no more processing is needed for this entry.
12010
13133
  */
12011
- async _handleSymlink(entry, directory, path26, item) {
13134
+ async _handleSymlink(entry, directory, path27, item) {
12012
13135
  if (this.fsw.closed) {
12013
13136
  return;
12014
13137
  }
@@ -12018,7 +13141,7 @@ var NodeFsHandler = class {
12018
13141
  this.fsw._incrReadyCount();
12019
13142
  let linkPath;
12020
13143
  try {
12021
- linkPath = await (0, import_promises2.realpath)(path26);
13144
+ linkPath = await (0, import_promises2.realpath)(path27);
12022
13145
  } catch (e) {
12023
13146
  this.fsw._emitReady();
12024
13147
  return true;
@@ -12028,12 +13151,12 @@ var NodeFsHandler = class {
12028
13151
  if (dir.has(item)) {
12029
13152
  if (this.fsw._symlinkPaths.get(full) !== linkPath) {
12030
13153
  this.fsw._symlinkPaths.set(full, linkPath);
12031
- this.fsw._emit(EV.CHANGE, path26, entry.stats);
13154
+ this.fsw._emit(EV.CHANGE, path27, entry.stats);
12032
13155
  }
12033
13156
  } else {
12034
13157
  dir.add(item);
12035
13158
  this.fsw._symlinkPaths.set(full, linkPath);
12036
- this.fsw._emit(EV.ADD, path26, entry.stats);
13159
+ this.fsw._emit(EV.ADD, path27, entry.stats);
12037
13160
  }
12038
13161
  this.fsw._emitReady();
12039
13162
  return true;
@@ -12063,9 +13186,9 @@ var NodeFsHandler = class {
12063
13186
  return;
12064
13187
  }
12065
13188
  const item = entry.path;
12066
- let path26 = sp.join(directory, item);
13189
+ let path27 = sp.join(directory, item);
12067
13190
  current.add(item);
12068
- if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path26, item)) {
13191
+ if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path27, item)) {
12069
13192
  return;
12070
13193
  }
12071
13194
  if (this.fsw.closed) {
@@ -12074,8 +13197,8 @@ var NodeFsHandler = class {
12074
13197
  }
12075
13198
  if (item === target || !target && !previous.has(item)) {
12076
13199
  this.fsw._incrReadyCount();
12077
- path26 = sp.join(dir, sp.relative(dir, path26));
12078
- this._addToNodeFs(path26, initialAdd, wh, depth + 1);
13200
+ path27 = sp.join(dir, sp.relative(dir, path27));
13201
+ this._addToNodeFs(path27, initialAdd, wh, depth + 1);
12079
13202
  }
12080
13203
  }).on(EV.ERROR, this._boundHandleError);
12081
13204
  return new Promise((resolve15, reject) => {
@@ -12144,13 +13267,13 @@ var NodeFsHandler = class {
12144
13267
  * @param depth Child path actually targeted for watch
12145
13268
  * @param target Child path actually targeted for watch
12146
13269
  */
12147
- async _addToNodeFs(path26, initialAdd, priorWh, depth, target) {
13270
+ async _addToNodeFs(path27, initialAdd, priorWh, depth, target) {
12148
13271
  const ready = this.fsw._emitReady;
12149
- if (this.fsw._isIgnored(path26) || this.fsw.closed) {
13272
+ if (this.fsw._isIgnored(path27) || this.fsw.closed) {
12150
13273
  ready();
12151
13274
  return false;
12152
13275
  }
12153
- const wh = this.fsw._getWatchHelpers(path26);
13276
+ const wh = this.fsw._getWatchHelpers(path27);
12154
13277
  if (priorWh) {
12155
13278
  wh.filterPath = (entry) => priorWh.filterPath(entry);
12156
13279
  wh.filterDir = (entry) => priorWh.filterDir(entry);
@@ -12166,8 +13289,8 @@ var NodeFsHandler = class {
12166
13289
  const follow = this.fsw.options.followSymlinks;
12167
13290
  let closer;
12168
13291
  if (stats.isDirectory()) {
12169
- const absPath = sp.resolve(path26);
12170
- const targetPath = follow ? await (0, import_promises2.realpath)(path26) : path26;
13292
+ const absPath = sp.resolve(path27);
13293
+ const targetPath = follow ? await (0, import_promises2.realpath)(path27) : path27;
12171
13294
  if (this.fsw.closed)
12172
13295
  return;
12173
13296
  closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
@@ -12177,29 +13300,29 @@ var NodeFsHandler = class {
12177
13300
  this.fsw._symlinkPaths.set(absPath, targetPath);
12178
13301
  }
12179
13302
  } else if (stats.isSymbolicLink()) {
12180
- const targetPath = follow ? await (0, import_promises2.realpath)(path26) : path26;
13303
+ const targetPath = follow ? await (0, import_promises2.realpath)(path27) : path27;
12181
13304
  if (this.fsw.closed)
12182
13305
  return;
12183
13306
  const parent = sp.dirname(wh.watchPath);
12184
13307
  this.fsw._getWatchedDir(parent).add(wh.watchPath);
12185
13308
  this.fsw._emit(EV.ADD, wh.watchPath, stats);
12186
- closer = await this._handleDir(parent, stats, initialAdd, depth, path26, wh, targetPath);
13309
+ closer = await this._handleDir(parent, stats, initialAdd, depth, path27, wh, targetPath);
12187
13310
  if (this.fsw.closed)
12188
13311
  return;
12189
13312
  if (targetPath !== void 0) {
12190
- this.fsw._symlinkPaths.set(sp.resolve(path26), targetPath);
13313
+ this.fsw._symlinkPaths.set(sp.resolve(path27), targetPath);
12191
13314
  }
12192
13315
  } else {
12193
13316
  closer = this._handleFile(wh.watchPath, stats, initialAdd);
12194
13317
  }
12195
13318
  ready();
12196
13319
  if (closer)
12197
- this.fsw._addPathCloser(path26, closer);
13320
+ this.fsw._addPathCloser(path27, closer);
12198
13321
  return false;
12199
13322
  } catch (error) {
12200
13323
  if (this.fsw._handleError(error)) {
12201
13324
  ready();
12202
- return path26;
13325
+ return path27;
12203
13326
  }
12204
13327
  }
12205
13328
  }
@@ -12242,24 +13365,24 @@ function createPattern(matcher) {
12242
13365
  }
12243
13366
  return () => false;
12244
13367
  }
12245
- function normalizePath2(path26) {
12246
- if (typeof path26 !== "string")
13368
+ function normalizePath2(path27) {
13369
+ if (typeof path27 !== "string")
12247
13370
  throw new Error("string expected");
12248
- path26 = sp2.normalize(path26);
12249
- path26 = path26.replace(/\\/g, "/");
13371
+ path27 = sp2.normalize(path27);
13372
+ path27 = path27.replace(/\\/g, "/");
12250
13373
  let prepend = false;
12251
- if (path26.startsWith("//"))
13374
+ if (path27.startsWith("//"))
12252
13375
  prepend = true;
12253
- path26 = path26.replace(DOUBLE_SLASH_RE, "/");
13376
+ path27 = path27.replace(DOUBLE_SLASH_RE, "/");
12254
13377
  if (prepend)
12255
- path26 = "/" + path26;
12256
- return path26;
13378
+ path27 = "/" + path27;
13379
+ return path27;
12257
13380
  }
12258
13381
  function matchPatterns(patterns, testString, stats) {
12259
- const path26 = normalizePath2(testString);
13382
+ const path27 = normalizePath2(testString);
12260
13383
  for (let index = 0; index < patterns.length; index++) {
12261
13384
  const pattern = patterns[index];
12262
- if (pattern(path26, stats)) {
13385
+ if (pattern(path27, stats)) {
12263
13386
  return true;
12264
13387
  }
12265
13388
  }
@@ -12297,19 +13420,19 @@ var toUnix = (string) => {
12297
13420
  }
12298
13421
  return str;
12299
13422
  };
12300
- var normalizePathToUnix = (path26) => toUnix(sp2.normalize(toUnix(path26)));
12301
- var normalizeIgnored = (cwd = "") => (path26) => {
12302
- if (typeof path26 === "string") {
12303
- return normalizePathToUnix(sp2.isAbsolute(path26) ? path26 : sp2.join(cwd, path26));
13423
+ var normalizePathToUnix = (path27) => toUnix(sp2.normalize(toUnix(path27)));
13424
+ var normalizeIgnored = (cwd = "") => (path27) => {
13425
+ if (typeof path27 === "string") {
13426
+ return normalizePathToUnix(sp2.isAbsolute(path27) ? path27 : sp2.join(cwd, path27));
12304
13427
  } else {
12305
- return path26;
13428
+ return path27;
12306
13429
  }
12307
13430
  };
12308
- var getAbsolutePath = (path26, cwd) => {
12309
- if (sp2.isAbsolute(path26)) {
12310
- return path26;
13431
+ var getAbsolutePath = (path27, cwd) => {
13432
+ if (sp2.isAbsolute(path27)) {
13433
+ return path27;
12311
13434
  }
12312
- return sp2.join(cwd, path26);
13435
+ return sp2.join(cwd, path27);
12313
13436
  };
12314
13437
  var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
12315
13438
  var DirEntry = class {
@@ -12374,10 +13497,10 @@ var WatchHelper = class {
12374
13497
  dirParts;
12375
13498
  followSymlinks;
12376
13499
  statMethod;
12377
- constructor(path26, follow, fsw) {
13500
+ constructor(path27, follow, fsw) {
12378
13501
  this.fsw = fsw;
12379
- const watchPath = path26;
12380
- this.path = path26 = path26.replace(REPLACER_RE, "");
13502
+ const watchPath = path27;
13503
+ this.path = path27 = path27.replace(REPLACER_RE, "");
12381
13504
  this.watchPath = watchPath;
12382
13505
  this.fullWatchPath = sp2.resolve(watchPath);
12383
13506
  this.dirParts = [];
@@ -12517,20 +13640,20 @@ var FSWatcher = class extends import_node_events.EventEmitter {
12517
13640
  this._closePromise = void 0;
12518
13641
  let paths = unifyPaths(paths_);
12519
13642
  if (cwd) {
12520
- paths = paths.map((path26) => {
12521
- const absPath = getAbsolutePath(path26, cwd);
13643
+ paths = paths.map((path27) => {
13644
+ const absPath = getAbsolutePath(path27, cwd);
12522
13645
  return absPath;
12523
13646
  });
12524
13647
  }
12525
- paths.forEach((path26) => {
12526
- this._removeIgnoredPath(path26);
13648
+ paths.forEach((path27) => {
13649
+ this._removeIgnoredPath(path27);
12527
13650
  });
12528
13651
  this._userIgnored = void 0;
12529
13652
  if (!this._readyCount)
12530
13653
  this._readyCount = 0;
12531
13654
  this._readyCount += paths.length;
12532
- Promise.all(paths.map(async (path26) => {
12533
- const res = await this._nodeFsHandler._addToNodeFs(path26, !_internal, void 0, 0, _origAdd);
13655
+ Promise.all(paths.map(async (path27) => {
13656
+ const res = await this._nodeFsHandler._addToNodeFs(path27, !_internal, void 0, 0, _origAdd);
12534
13657
  if (res)
12535
13658
  this._emitReady();
12536
13659
  return res;
@@ -12552,17 +13675,17 @@ var FSWatcher = class extends import_node_events.EventEmitter {
12552
13675
  return this;
12553
13676
  const paths = unifyPaths(paths_);
12554
13677
  const { cwd } = this.options;
12555
- paths.forEach((path26) => {
12556
- if (!sp2.isAbsolute(path26) && !this._closers.has(path26)) {
13678
+ paths.forEach((path27) => {
13679
+ if (!sp2.isAbsolute(path27) && !this._closers.has(path27)) {
12557
13680
  if (cwd)
12558
- path26 = sp2.join(cwd, path26);
12559
- path26 = sp2.resolve(path26);
13681
+ path27 = sp2.join(cwd, path27);
13682
+ path27 = sp2.resolve(path27);
12560
13683
  }
12561
- this._closePath(path26);
12562
- this._addIgnoredPath(path26);
12563
- if (this._watched.has(path26)) {
13684
+ this._closePath(path27);
13685
+ this._addIgnoredPath(path27);
13686
+ if (this._watched.has(path27)) {
12564
13687
  this._addIgnoredPath({
12565
- path: path26,
13688
+ path: path27,
12566
13689
  recursive: true
12567
13690
  });
12568
13691
  }
@@ -12626,38 +13749,38 @@ var FSWatcher = class extends import_node_events.EventEmitter {
12626
13749
  * @param stats arguments to be passed with event
12627
13750
  * @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
12628
13751
  */
12629
- async _emit(event, path26, stats) {
13752
+ async _emit(event, path27, stats) {
12630
13753
  if (this.closed)
12631
13754
  return;
12632
13755
  const opts = this.options;
12633
13756
  if (isWindows)
12634
- path26 = sp2.normalize(path26);
13757
+ path27 = sp2.normalize(path27);
12635
13758
  if (opts.cwd)
12636
- path26 = sp2.relative(opts.cwd, path26);
12637
- const args = [path26];
13759
+ path27 = sp2.relative(opts.cwd, path27);
13760
+ const args = [path27];
12638
13761
  if (stats != null)
12639
13762
  args.push(stats);
12640
13763
  const awf = opts.awaitWriteFinish;
12641
13764
  let pw;
12642
- if (awf && (pw = this._pendingWrites.get(path26))) {
13765
+ if (awf && (pw = this._pendingWrites.get(path27))) {
12643
13766
  pw.lastChange = /* @__PURE__ */ new Date();
12644
13767
  return this;
12645
13768
  }
12646
13769
  if (opts.atomic) {
12647
13770
  if (event === EVENTS.UNLINK) {
12648
- this._pendingUnlinks.set(path26, [event, ...args]);
13771
+ this._pendingUnlinks.set(path27, [event, ...args]);
12649
13772
  setTimeout(() => {
12650
- this._pendingUnlinks.forEach((entry, path27) => {
13773
+ this._pendingUnlinks.forEach((entry, path28) => {
12651
13774
  this.emit(...entry);
12652
13775
  this.emit(EVENTS.ALL, ...entry);
12653
- this._pendingUnlinks.delete(path27);
13776
+ this._pendingUnlinks.delete(path28);
12654
13777
  });
12655
13778
  }, typeof opts.atomic === "number" ? opts.atomic : 100);
12656
13779
  return this;
12657
13780
  }
12658
- if (event === EVENTS.ADD && this._pendingUnlinks.has(path26)) {
13781
+ if (event === EVENTS.ADD && this._pendingUnlinks.has(path27)) {
12659
13782
  event = EVENTS.CHANGE;
12660
- this._pendingUnlinks.delete(path26);
13783
+ this._pendingUnlinks.delete(path27);
12661
13784
  }
12662
13785
  }
12663
13786
  if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) {
@@ -12675,16 +13798,16 @@ var FSWatcher = class extends import_node_events.EventEmitter {
12675
13798
  this.emitWithAll(event, args);
12676
13799
  }
12677
13800
  };
12678
- this._awaitWriteFinish(path26, awf.stabilityThreshold, event, awfEmit);
13801
+ this._awaitWriteFinish(path27, awf.stabilityThreshold, event, awfEmit);
12679
13802
  return this;
12680
13803
  }
12681
13804
  if (event === EVENTS.CHANGE) {
12682
- const isThrottled = !this._throttle(EVENTS.CHANGE, path26, 50);
13805
+ const isThrottled = !this._throttle(EVENTS.CHANGE, path27, 50);
12683
13806
  if (isThrottled)
12684
13807
  return this;
12685
13808
  }
12686
13809
  if (opts.alwaysStat && stats === void 0 && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) {
12687
- const fullPath = opts.cwd ? sp2.join(opts.cwd, path26) : path26;
13810
+ const fullPath = opts.cwd ? sp2.join(opts.cwd, path27) : path27;
12688
13811
  let stats2;
12689
13812
  try {
12690
13813
  stats2 = await (0, import_promises3.stat)(fullPath);
@@ -12715,23 +13838,23 @@ var FSWatcher = class extends import_node_events.EventEmitter {
12715
13838
  * @param timeout duration of time to suppress duplicate actions
12716
13839
  * @returns tracking object or false if action should be suppressed
12717
13840
  */
12718
- _throttle(actionType, path26, timeout) {
13841
+ _throttle(actionType, path27, timeout) {
12719
13842
  if (!this._throttled.has(actionType)) {
12720
13843
  this._throttled.set(actionType, /* @__PURE__ */ new Map());
12721
13844
  }
12722
13845
  const action = this._throttled.get(actionType);
12723
13846
  if (!action)
12724
13847
  throw new Error("invalid throttle");
12725
- const actionPath = action.get(path26);
13848
+ const actionPath = action.get(path27);
12726
13849
  if (actionPath) {
12727
13850
  actionPath.count++;
12728
13851
  return false;
12729
13852
  }
12730
13853
  let timeoutObject;
12731
13854
  const clear = () => {
12732
- const item = action.get(path26);
13855
+ const item = action.get(path27);
12733
13856
  const count = item ? item.count : 0;
12734
- action.delete(path26);
13857
+ action.delete(path27);
12735
13858
  clearTimeout(timeoutObject);
12736
13859
  if (item)
12737
13860
  clearTimeout(item.timeoutObject);
@@ -12739,7 +13862,7 @@ var FSWatcher = class extends import_node_events.EventEmitter {
12739
13862
  };
12740
13863
  timeoutObject = setTimeout(clear, timeout);
12741
13864
  const thr = { timeoutObject, clear, count: 0 };
12742
- action.set(path26, thr);
13865
+ action.set(path27, thr);
12743
13866
  return thr;
12744
13867
  }
12745
13868
  _incrReadyCount() {
@@ -12753,44 +13876,44 @@ var FSWatcher = class extends import_node_events.EventEmitter {
12753
13876
  * @param event
12754
13877
  * @param awfEmit Callback to be called when ready for event to be emitted.
12755
13878
  */
12756
- _awaitWriteFinish(path26, threshold, event, awfEmit) {
13879
+ _awaitWriteFinish(path27, threshold, event, awfEmit) {
12757
13880
  const awf = this.options.awaitWriteFinish;
12758
13881
  if (typeof awf !== "object")
12759
13882
  return;
12760
13883
  const pollInterval = awf.pollInterval;
12761
13884
  let timeoutHandler;
12762
- let fullPath = path26;
12763
- if (this.options.cwd && !sp2.isAbsolute(path26)) {
12764
- fullPath = sp2.join(this.options.cwd, path26);
13885
+ let fullPath = path27;
13886
+ if (this.options.cwd && !sp2.isAbsolute(path27)) {
13887
+ fullPath = sp2.join(this.options.cwd, path27);
12765
13888
  }
12766
13889
  const now = /* @__PURE__ */ new Date();
12767
13890
  const writes = this._pendingWrites;
12768
13891
  function awaitWriteFinishFn(prevStat) {
12769
13892
  (0, import_node_fs2.stat)(fullPath, (err, curStat) => {
12770
- if (err || !writes.has(path26)) {
13893
+ if (err || !writes.has(path27)) {
12771
13894
  if (err && err.code !== "ENOENT")
12772
13895
  awfEmit(err);
12773
13896
  return;
12774
13897
  }
12775
13898
  const now2 = Number(/* @__PURE__ */ new Date());
12776
13899
  if (prevStat && curStat.size !== prevStat.size) {
12777
- writes.get(path26).lastChange = now2;
13900
+ writes.get(path27).lastChange = now2;
12778
13901
  }
12779
- const pw = writes.get(path26);
13902
+ const pw = writes.get(path27);
12780
13903
  const df = now2 - pw.lastChange;
12781
13904
  if (df >= threshold) {
12782
- writes.delete(path26);
13905
+ writes.delete(path27);
12783
13906
  awfEmit(void 0, curStat);
12784
13907
  } else {
12785
13908
  timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
12786
13909
  }
12787
13910
  });
12788
13911
  }
12789
- if (!writes.has(path26)) {
12790
- writes.set(path26, {
13912
+ if (!writes.has(path27)) {
13913
+ writes.set(path27, {
12791
13914
  lastChange: now,
12792
13915
  cancelWait: () => {
12793
- writes.delete(path26);
13916
+ writes.delete(path27);
12794
13917
  clearTimeout(timeoutHandler);
12795
13918
  return event;
12796
13919
  }
@@ -12801,8 +13924,8 @@ var FSWatcher = class extends import_node_events.EventEmitter {
12801
13924
  /**
12802
13925
  * Determines whether user has asked to ignore this path.
12803
13926
  */
12804
- _isIgnored(path26, stats) {
12805
- if (this.options.atomic && DOT_RE.test(path26))
13927
+ _isIgnored(path27, stats) {
13928
+ if (this.options.atomic && DOT_RE.test(path27))
12806
13929
  return true;
12807
13930
  if (!this._userIgnored) {
12808
13931
  const { cwd } = this.options;
@@ -12812,17 +13935,17 @@ var FSWatcher = class extends import_node_events.EventEmitter {
12812
13935
  const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
12813
13936
  this._userIgnored = anymatch(list, void 0);
12814
13937
  }
12815
- return this._userIgnored(path26, stats);
13938
+ return this._userIgnored(path27, stats);
12816
13939
  }
12817
- _isntIgnored(path26, stat4) {
12818
- return !this._isIgnored(path26, stat4);
13940
+ _isntIgnored(path27, stat4) {
13941
+ return !this._isIgnored(path27, stat4);
12819
13942
  }
12820
13943
  /**
12821
13944
  * Provides a set of common helpers and properties relating to symlink handling.
12822
13945
  * @param path file or directory pattern being watched
12823
13946
  */
12824
- _getWatchHelpers(path26) {
12825
- return new WatchHelper(path26, this.options.followSymlinks, this);
13947
+ _getWatchHelpers(path27) {
13948
+ return new WatchHelper(path27, this.options.followSymlinks, this);
12826
13949
  }
12827
13950
  // Directory helpers
12828
13951
  // -----------------
@@ -12854,63 +13977,63 @@ var FSWatcher = class extends import_node_events.EventEmitter {
12854
13977
  * @param item base path of item/directory
12855
13978
  */
12856
13979
  _remove(directory, item, isDirectory) {
12857
- const path26 = sp2.join(directory, item);
12858
- const fullPath = sp2.resolve(path26);
12859
- isDirectory = isDirectory != null ? isDirectory : this._watched.has(path26) || this._watched.has(fullPath);
12860
- if (!this._throttle("remove", path26, 100))
13980
+ const path27 = sp2.join(directory, item);
13981
+ const fullPath = sp2.resolve(path27);
13982
+ isDirectory = isDirectory != null ? isDirectory : this._watched.has(path27) || this._watched.has(fullPath);
13983
+ if (!this._throttle("remove", path27, 100))
12861
13984
  return;
12862
13985
  if (!isDirectory && this._watched.size === 1) {
12863
13986
  this.add(directory, item, true);
12864
13987
  }
12865
- const wp = this._getWatchedDir(path26);
13988
+ const wp = this._getWatchedDir(path27);
12866
13989
  const nestedDirectoryChildren = wp.getChildren();
12867
- nestedDirectoryChildren.forEach((nested) => this._remove(path26, nested));
13990
+ nestedDirectoryChildren.forEach((nested) => this._remove(path27, nested));
12868
13991
  const parent = this._getWatchedDir(directory);
12869
13992
  const wasTracked = parent.has(item);
12870
13993
  parent.remove(item);
12871
13994
  if (this._symlinkPaths.has(fullPath)) {
12872
13995
  this._symlinkPaths.delete(fullPath);
12873
13996
  }
12874
- let relPath = path26;
13997
+ let relPath = path27;
12875
13998
  if (this.options.cwd)
12876
- relPath = sp2.relative(this.options.cwd, path26);
13999
+ relPath = sp2.relative(this.options.cwd, path27);
12877
14000
  if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
12878
14001
  const event = this._pendingWrites.get(relPath).cancelWait();
12879
14002
  if (event === EVENTS.ADD)
12880
14003
  return;
12881
14004
  }
12882
- this._watched.delete(path26);
14005
+ this._watched.delete(path27);
12883
14006
  this._watched.delete(fullPath);
12884
14007
  const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK;
12885
- if (wasTracked && !this._isIgnored(path26))
12886
- this._emit(eventName, path26);
12887
- this._closePath(path26);
14008
+ if (wasTracked && !this._isIgnored(path27))
14009
+ this._emit(eventName, path27);
14010
+ this._closePath(path27);
12888
14011
  }
12889
14012
  /**
12890
14013
  * Closes all watchers for a path
12891
14014
  */
12892
- _closePath(path26) {
12893
- this._closeFile(path26);
12894
- const dir = sp2.dirname(path26);
12895
- this._getWatchedDir(dir).remove(sp2.basename(path26));
14015
+ _closePath(path27) {
14016
+ this._closeFile(path27);
14017
+ const dir = sp2.dirname(path27);
14018
+ this._getWatchedDir(dir).remove(sp2.basename(path27));
12896
14019
  }
12897
14020
  /**
12898
14021
  * Closes only file-specific watchers
12899
14022
  */
12900
- _closeFile(path26) {
12901
- const closers = this._closers.get(path26);
14023
+ _closeFile(path27) {
14024
+ const closers = this._closers.get(path27);
12902
14025
  if (!closers)
12903
14026
  return;
12904
14027
  closers.forEach((closer) => closer());
12905
- this._closers.delete(path26);
14028
+ this._closers.delete(path27);
12906
14029
  }
12907
- _addPathCloser(path26, closer) {
14030
+ _addPathCloser(path27, closer) {
12908
14031
  if (!closer)
12909
14032
  return;
12910
- let list = this._closers.get(path26);
14033
+ let list = this._closers.get(path27);
12911
14034
  if (!list) {
12912
14035
  list = [];
12913
- this._closers.set(path26, list);
14036
+ this._closers.set(path27, list);
12914
14037
  }
12915
14038
  list.push(closer);
12916
14039
  }
@@ -12940,7 +14063,7 @@ function watch(paths, options = {}) {
12940
14063
  var chokidar_default = { watch, FSWatcher };
12941
14064
 
12942
14065
  // src/watcher/file-watcher.ts
12943
- var path21 = __toESM(require("path"), 1);
14066
+ var path22 = __toESM(require("path"), 1);
12944
14067
  var FileWatcher = class {
12945
14068
  watcher = null;
12946
14069
  projectRoot;
@@ -12951,6 +14074,8 @@ var FileWatcher = class {
12951
14074
  debounceTimer = null;
12952
14075
  debounceMs = 1e3;
12953
14076
  onChanges = null;
14077
+ readyPromise = null;
14078
+ resolveReady = null;
12954
14079
  constructor(projectRoot, config, host = "opencode", options = {}) {
12955
14080
  this.projectRoot = projectRoot;
12956
14081
  this.config = config;
@@ -12966,15 +14091,15 @@ var FileWatcher = class {
12966
14091
  const watchTargets = this.configPath ? [this.projectRoot, this.configPath] : this.projectRoot;
12967
14092
  this.watcher = chokidar_default.watch(watchTargets, {
12968
14093
  ignored: (filePath) => {
12969
- const relativePath = path21.relative(this.projectRoot, filePath);
14094
+ const relativePath = path22.relative(this.projectRoot, filePath);
12970
14095
  if (!relativePath) return false;
12971
14096
  if (this.isProjectConfigPathOrAncestor(relativePath)) {
12972
14097
  return false;
12973
14098
  }
12974
- if (hasFilteredPathSegment(relativePath, path21.sep)) {
14099
+ if (hasFilteredPathSegment(relativePath, path22.sep)) {
12975
14100
  return true;
12976
14101
  }
12977
- if (isRestrictedDirectory(relativePath, path21.sep)) {
14102
+ if (isRestrictedDirectory(relativePath, path22.sep)) {
12978
14103
  return true;
12979
14104
  }
12980
14105
  if (ignoreFilter.ignores(relativePath)) {
@@ -12989,6 +14114,13 @@ var FileWatcher = class {
12989
14114
  pollInterval: 100
12990
14115
  }
12991
14116
  });
14117
+ this.readyPromise = new Promise((resolve15) => {
14118
+ this.resolveReady = resolve15;
14119
+ });
14120
+ this.watcher.once("ready", () => {
14121
+ this.resolveReady?.();
14122
+ this.resolveReady = null;
14123
+ });
12992
14124
  this.watcher.on("error", (error) => {
12993
14125
  const err = error instanceof Error ? error : null;
12994
14126
  if (err?.code === "EPERM" || err?.code === "EACCES") {
@@ -13020,24 +14152,24 @@ var FileWatcher = class {
13020
14152
  this.scheduleFlush();
13021
14153
  }
13022
14154
  isProjectConfigPath(filePath) {
13023
- const relativePath = path21.relative(this.projectRoot, filePath);
13024
- const normalizedRelativePath = path21.normalize(relativePath);
14155
+ const relativePath = path22.relative(this.projectRoot, filePath);
14156
+ const normalizedRelativePath = path22.normalize(relativePath);
13025
14157
  return this.getProjectConfigRelativePaths().some((configPath) => configPath === normalizedRelativePath);
13026
14158
  }
13027
14159
  isProjectConfigPathOrAncestor(relativePath) {
13028
- const normalizedRelativePath = path21.normalize(relativePath);
14160
+ const normalizedRelativePath = path22.normalize(relativePath);
13029
14161
  return this.getProjectConfigRelativePaths().some(
13030
- (configPath) => configPath === normalizedRelativePath || configPath.startsWith(`${normalizedRelativePath}${path21.sep}`)
14162
+ (configPath) => configPath === normalizedRelativePath || configPath.startsWith(`${normalizedRelativePath}${path22.sep}`)
13031
14163
  );
13032
14164
  }
13033
14165
  getProjectConfigRelativePaths() {
13034
14166
  if (this.configPath) {
13035
- return [path21.normalize(path21.relative(this.projectRoot, this.configPath))];
14167
+ return [path22.normalize(path22.relative(this.projectRoot, this.configPath))];
13036
14168
  }
13037
14169
  return [
13038
14170
  resolveProjectConfigPath(this.projectRoot, this.host),
13039
14171
  resolveWritableProjectConfigPath(this.projectRoot, this.host)
13040
- ].map((configPath) => path21.normalize(path21.relative(this.projectRoot, configPath)));
14172
+ ].map((configPath) => path22.normalize(path22.relative(this.projectRoot, configPath)));
13041
14173
  }
13042
14174
  scheduleFlush() {
13043
14175
  if (this.debounceTimer) {
@@ -13052,7 +14184,7 @@ var FileWatcher = class {
13052
14184
  return;
13053
14185
  }
13054
14186
  const changes = Array.from(this.pendingChanges.entries()).map(
13055
- ([path26, type]) => ({ path: path26, type })
14187
+ ([path27, type]) => ({ path: path27, type })
13056
14188
  );
13057
14189
  this.pendingChanges.clear();
13058
14190
  try {
@@ -13061,25 +14193,32 @@ var FileWatcher = class {
13061
14193
  console.error("Error handling file changes:", error);
13062
14194
  }
13063
14195
  }
13064
- stop() {
14196
+ async stop() {
13065
14197
  if (this.debounceTimer) {
13066
14198
  clearTimeout(this.debounceTimer);
13067
14199
  this.debounceTimer = null;
13068
14200
  }
13069
14201
  if (this.watcher) {
13070
- this.watcher.close();
14202
+ const watcher = this.watcher;
13071
14203
  this.watcher = null;
14204
+ await watcher.close();
13072
14205
  }
14206
+ this.resolveReady?.();
14207
+ this.resolveReady = null;
14208
+ this.readyPromise = null;
13073
14209
  this.pendingChanges.clear();
13074
14210
  this.onChanges = null;
13075
14211
  }
13076
14212
  isRunning() {
13077
14213
  return this.watcher !== null;
13078
14214
  }
14215
+ async waitUntilReady() {
14216
+ await (this.readyPromise ?? Promise.resolve());
14217
+ }
13079
14218
  };
13080
14219
 
13081
14220
  // src/watcher/git-head-watcher.ts
13082
- var path22 = __toESM(require("path"), 1);
14221
+ var path23 = __toESM(require("path"), 1);
13083
14222
  var GitHeadWatcher = class {
13084
14223
  watcher = null;
13085
14224
  projectRoot;
@@ -13101,7 +14240,7 @@ var GitHeadWatcher = class {
13101
14240
  this.onBranchChange = handler;
13102
14241
  this.currentBranch = getCurrentBranch(this.projectRoot);
13103
14242
  const headPath = getHeadPath(this.projectRoot);
13104
- const refsPath = path22.join(this.projectRoot, ".git", "refs", "heads");
14243
+ const refsPath = path23.join(this.projectRoot, ".git", "refs", "heads");
13105
14244
  this.watcher = chokidar_default.watch([headPath, refsPath], {
13106
14245
  persistent: true,
13107
14246
  ignoreInitial: true,
@@ -13138,14 +14277,15 @@ var GitHeadWatcher = class {
13138
14277
  getCurrentBranch() {
13139
14278
  return this.currentBranch;
13140
14279
  }
13141
- stop() {
14280
+ async stop() {
13142
14281
  if (this.debounceTimer) {
13143
14282
  clearTimeout(this.debounceTimer);
13144
14283
  this.debounceTimer = null;
13145
14284
  }
13146
14285
  if (this.watcher) {
13147
- this.watcher.close();
14286
+ const watcher = this.watcher;
13148
14287
  this.watcher = null;
14288
+ await watcher.close();
13149
14289
  }
13150
14290
  this.onBranchChange = null;
13151
14291
  }
@@ -13163,6 +14303,8 @@ var BackgroundReindexer = class {
13163
14303
  running = false;
13164
14304
  pending = false;
13165
14305
  stopped = false;
14306
+ retryTimer = null;
14307
+ retryDelayMs = 50;
13166
14308
  request() {
13167
14309
  if (this.stopped) {
13168
14310
  return;
@@ -13173,9 +14315,13 @@ var BackgroundReindexer = class {
13173
14315
  stop() {
13174
14316
  this.stopped = true;
13175
14317
  this.pending = false;
14318
+ if (this.retryTimer) {
14319
+ clearTimeout(this.retryTimer);
14320
+ this.retryTimer = null;
14321
+ }
13176
14322
  }
13177
14323
  drain() {
13178
- if (this.stopped || this.running || !this.pending) {
14324
+ if (this.stopped || this.running || this.retryTimer || !this.pending) {
13179
14325
  return;
13180
14326
  }
13181
14327
  this.pending = false;
@@ -13183,13 +14329,29 @@ var BackgroundReindexer = class {
13183
14329
  void this.run();
13184
14330
  }
13185
14331
  async run() {
14332
+ let retryAfterContention = false;
13186
14333
  try {
13187
14334
  await this.runIndex();
14335
+ this.retryDelayMs = 50;
13188
14336
  } catch (error) {
13189
- console.error("[codebase-index] Background reindex failed:", error);
14337
+ if (isTransientIndexLockContention(error)) {
14338
+ this.pending = true;
14339
+ retryAfterContention = true;
14340
+ } else {
14341
+ console.error("[codebase-index] Background reindex failed:", error);
14342
+ }
13190
14343
  } finally {
13191
14344
  this.running = false;
13192
- this.drain();
14345
+ if (retryAfterContention && !this.stopped) {
14346
+ const delay = this.retryDelayMs;
14347
+ this.retryDelayMs = Math.min(this.retryDelayMs * 2, 500);
14348
+ this.retryTimer = setTimeout(() => {
14349
+ this.retryTimer = null;
14350
+ this.drain();
14351
+ }, delay);
14352
+ } else {
14353
+ this.drain();
14354
+ }
13193
14355
  }
13194
14356
  }
13195
14357
  };
@@ -13223,10 +14385,12 @@ function createWatcherWithIndexer(getIndexer, projectRoot, config, host = "openc
13223
14385
  return {
13224
14386
  fileWatcher,
13225
14387
  gitWatcher,
13226
- stop() {
14388
+ whenReady() {
14389
+ return fileWatcher.waitUntilReady();
14390
+ },
14391
+ async stop() {
13227
14392
  backgroundReindexer.stop();
13228
- fileWatcher.stop();
13229
- gitWatcher?.stop();
14393
+ await Promise.all([fileWatcher.stop(), gitWatcher?.stop()]);
13230
14394
  }
13231
14395
  };
13232
14396
  }
@@ -13245,7 +14409,7 @@ function getConfigPaths(projectRoot, host, options) {
13245
14409
 
13246
14410
  // src/tools/visualize/activity.ts
13247
14411
  var import_child_process4 = require("child_process");
13248
- var path23 = __toESM(require("path"), 1);
14412
+ var path24 = __toESM(require("path"), 1);
13249
14413
  function attachRecentActivity(data, projectRoot) {
13250
14414
  const activity = readGitActivity(projectRoot);
13251
14415
  const changes = activity.size > 0 ? buildGitChanges(data, activity, projectRoot) : buildGraphChanges(data);
@@ -13407,7 +14571,7 @@ function normalizePath3(filePath) {
13407
14571
  return filePath.replace(/\\/g, "/");
13408
14572
  }
13409
14573
  function toGitRelativePath(projectRoot, filePath) {
13410
- const relativePath = path23.isAbsolute(filePath) ? path23.relative(projectRoot, filePath) : filePath;
14574
+ const relativePath = path24.isAbsolute(filePath) ? path24.relative(projectRoot, filePath) : filePath;
13411
14575
  return normalizePath3(relativePath);
13412
14576
  }
13413
14577
 
@@ -13665,7 +14829,7 @@ render();
13665
14829
  }
13666
14830
 
13667
14831
  // src/tools/visualize/transform.ts
13668
- var path24 = __toESM(require("path"), 1);
14832
+ var path25 = __toESM(require("path"), 1);
13669
14833
 
13670
14834
  // src/tools/visualize/modules.ts
13671
14835
  var MAX_MODULES = 18;
@@ -13925,7 +15089,7 @@ function transformForVisualization(symbols, edges, options = {}) {
13925
15089
  filePath: s.filePath,
13926
15090
  kind: s.kind,
13927
15091
  line: s.startLine,
13928
- directory: path24.dirname(s.filePath),
15092
+ directory: path25.dirname(s.filePath),
13929
15093
  moduleId: "",
13930
15094
  moduleLabel: ""
13931
15095
  }));
@@ -13954,9 +15118,9 @@ function parseArgs(argv) {
13954
15118
  let host = "opencode";
13955
15119
  for (let i = 2; i < argv.length; i++) {
13956
15120
  if (argv[i] === "--project" && argv[i + 1]) {
13957
- project = path25.resolve(argv[++i]);
15121
+ project = path26.resolve(argv[++i]);
13958
15122
  } else if (argv[i] === "--config" && argv[i + 1]) {
13959
- config = path25.resolve(argv[++i]);
15123
+ config = path26.resolve(argv[++i]);
13960
15124
  } else if (argv[i] === "--host" && argv[i + 1]) {
13961
15125
  host = parseHostMode(argv[++i]);
13962
15126
  } else if (argv[i] === "--host") {
@@ -13969,7 +15133,7 @@ function loadCliRawConfig(args) {
13969
15133
  return args.config ? loadConfigFile(args.config) : loadMergedConfig(args.project, args.host);
13970
15134
  }
13971
15135
  function isCliEntrypoint(moduleUrl, argvPath) {
13972
- return argvPath !== void 0 && (0, import_fs15.realpathSync)((0, import_url2.fileURLToPath)(moduleUrl)) === (0, import_fs15.realpathSync)(argvPath);
15136
+ return argvPath !== void 0 && (0, import_fs16.realpathSync)((0, import_url2.fileURLToPath)(moduleUrl)) === (0, import_fs16.realpathSync)(argvPath);
13973
15137
  }
13974
15138
  function parseVisualizeArgs(argv, cwd) {
13975
15139
  let project = cwd;
@@ -13979,7 +15143,7 @@ function parseVisualizeArgs(argv, cwd) {
13979
15143
  for (let i = 0; i < argv.length; i++) {
13980
15144
  const arg = argv[i];
13981
15145
  if (arg === "--project" && argv[i + 1]) {
13982
- project = path25.resolve(argv[++i]);
15146
+ project = path26.resolve(argv[++i]);
13983
15147
  } else if (arg === "--max" && argv[i + 1]) {
13984
15148
  maxNodes = Number(argv[++i]);
13985
15149
  } else if (arg.startsWith("--max=") || arg.startsWith("max=")) {
@@ -14016,8 +15180,8 @@ async function handleVisualizeCommand(argv, cwd) {
14016
15180
  console.error("No connected symbols found. Retry with: npm run visualize -- orphans");
14017
15181
  return 1;
14018
15182
  }
14019
- const outputPath = path25.join(os5.tmpdir(), `call-graph-${Date.now()}.html`);
14020
- (0, import_fs15.writeFileSync)(outputPath, generateVisualizationHtml(vizData), "utf-8");
15183
+ const outputPath = path26.join(os6.tmpdir(), `call-graph-${Date.now()}.html`);
15184
+ (0, import_fs16.writeFileSync)(outputPath, generateVisualizationHtml(vizData), "utf-8");
14021
15185
  console.log(`Temporal call graph visualization generated: ${outputPath}`);
14022
15186
  console.log(`Nodes: ${vizData.nodes.length} | Edges: ${vizData.edges.length}`);
14023
15187
  console.log(`Recent change lenses: ${vizData.changes?.length ?? 0}`);
@@ -14046,7 +15210,7 @@ async function main() {
14046
15210
  const transport = new import_stdio.StdioServerTransport();
14047
15211
  await server.connect(transport);
14048
15212
  let watcher = null;
14049
- const isHomeDir = path25.resolve(args.project) === path25.resolve(os5.homedir());
15213
+ const isHomeDir = path26.resolve(args.project) === path26.resolve(os6.homedir());
14050
15214
  const isValidProject = !isHomeDir && (!config.indexing.requireProjectMarker || hasProjectMarker(args.project));
14051
15215
  if (config.indexing.autoIndex && isValidProject) {
14052
15216
  const indexer = getIndexerForProject(args.project, args.host);
@@ -14061,14 +15225,25 @@ async function main() {
14061
15225
  args.config ? { configPath: args.config } : {}
14062
15226
  );
14063
15227
  }
14064
- const shutdown = () => {
14065
- watcher?.stop();
14066
- server.close().catch(() => {
14067
- });
14068
- process.exit(0);
15228
+ let shuttingDown = false;
15229
+ const shutdown = async () => {
15230
+ if (shuttingDown) return;
15231
+ shuttingDown = true;
15232
+ try {
15233
+ await watcher?.stop();
15234
+ await server.close();
15235
+ process.exit(0);
15236
+ } catch (error) {
15237
+ console.error("Failed to stop MCP server cleanly:", error);
15238
+ process.exit(1);
15239
+ }
14069
15240
  };
14070
- process.on("SIGINT", shutdown);
14071
- process.on("SIGTERM", shutdown);
15241
+ process.on("SIGINT", () => {
15242
+ void shutdown();
15243
+ });
15244
+ process.on("SIGTERM", () => {
15245
+ void shutdown();
15246
+ });
14072
15247
  }
14073
15248
  function handleMainError(error) {
14074
15249
  if (error instanceof Error && error.message.startsWith("Invalid host mode")) {