opencode-codebase-index 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -62,11 +62,11 @@ var require_eventemitter3 = __commonJS({
62
62
  if (--emitter._eventsCount === 0) emitter._events = new Events();
63
63
  else delete emitter._events[evt];
64
64
  }
65
- function EventEmitter2() {
65
+ function EventEmitter3() {
66
66
  this._events = new Events();
67
67
  this._eventsCount = 0;
68
68
  }
69
- EventEmitter2.prototype.eventNames = function eventNames() {
69
+ EventEmitter3.prototype.eventNames = function eventNames() {
70
70
  var names = [], events, name;
71
71
  if (this._eventsCount === 0) return names;
72
72
  for (name in events = this._events) {
@@ -77,7 +77,7 @@ var require_eventemitter3 = __commonJS({
77
77
  }
78
78
  return names;
79
79
  };
80
- EventEmitter2.prototype.listeners = function listeners(event) {
80
+ EventEmitter3.prototype.listeners = function listeners(event) {
81
81
  var evt = prefix ? prefix + event : event, handlers = this._events[evt];
82
82
  if (!handlers) return [];
83
83
  if (handlers.fn) return [handlers.fn];
@@ -86,13 +86,13 @@ var require_eventemitter3 = __commonJS({
86
86
  }
87
87
  return ee;
88
88
  };
89
- EventEmitter2.prototype.listenerCount = function listenerCount(event) {
89
+ EventEmitter3.prototype.listenerCount = function listenerCount(event) {
90
90
  var evt = prefix ? prefix + event : event, listeners = this._events[evt];
91
91
  if (!listeners) return 0;
92
92
  if (listeners.fn) return 1;
93
93
  return listeners.length;
94
94
  };
95
- EventEmitter2.prototype.emit = function emit(event, a1, a2, a3, a4, a5) {
95
+ EventEmitter3.prototype.emit = function emit(event, a1, a2, a3, a4, a5) {
96
96
  var evt = prefix ? prefix + event : event;
97
97
  if (!this._events[evt]) return false;
98
98
  var listeners = this._events[evt], len = arguments.length, args, i;
@@ -143,13 +143,13 @@ var require_eventemitter3 = __commonJS({
143
143
  }
144
144
  return true;
145
145
  };
146
- EventEmitter2.prototype.on = function on(event, fn, context) {
146
+ EventEmitter3.prototype.on = function on(event, fn, context) {
147
147
  return addListener(this, event, fn, context, false);
148
148
  };
149
- EventEmitter2.prototype.once = function once(event, fn, context) {
149
+ EventEmitter3.prototype.once = function once(event, fn, context) {
150
150
  return addListener(this, event, fn, context, true);
151
151
  };
152
- EventEmitter2.prototype.removeListener = function removeListener(event, fn, context, once) {
152
+ EventEmitter3.prototype.removeListener = function removeListener(event, fn, context, once) {
153
153
  var evt = prefix ? prefix + event : event;
154
154
  if (!this._events[evt]) return this;
155
155
  if (!fn) {
@@ -172,7 +172,7 @@ var require_eventemitter3 = __commonJS({
172
172
  }
173
173
  return this;
174
174
  };
175
- EventEmitter2.prototype.removeAllListeners = function removeAllListeners(event) {
175
+ EventEmitter3.prototype.removeAllListeners = function removeAllListeners(event) {
176
176
  var evt;
177
177
  if (event) {
178
178
  evt = prefix ? prefix + event : event;
@@ -183,12 +183,12 @@ var require_eventemitter3 = __commonJS({
183
183
  }
184
184
  return this;
185
185
  };
186
- EventEmitter2.prototype.off = EventEmitter2.prototype.removeListener;
187
- EventEmitter2.prototype.addListener = EventEmitter2.prototype.on;
188
- EventEmitter2.prefixed = prefix;
189
- EventEmitter2.EventEmitter = EventEmitter2;
186
+ EventEmitter3.prototype.off = EventEmitter3.prototype.removeListener;
187
+ EventEmitter3.prototype.addListener = EventEmitter3.prototype.on;
188
+ EventEmitter3.prefixed = prefix;
189
+ EventEmitter3.EventEmitter = EventEmitter3;
190
190
  if ("undefined" !== typeof module2) {
191
- module2.exports = EventEmitter2;
191
+ module2.exports = EventEmitter3;
192
192
  }
193
193
  }
194
194
  });
@@ -211,7 +211,7 @@ var require_ignore = __commonJS({
211
211
  var REGEX_SPLITALL_CRLF = /\r?\n/g;
212
212
  var REGEX_TEST_INVALID_PATH = /^\.{0,2}\/|^\.{1,2}$/;
213
213
  var REGEX_TEST_TRAILING_SLASH = /\/$/;
214
- var SLASH = "/";
214
+ var SLASH2 = "/";
215
215
  var TMP_KEY_IGNORE = "node-ignore";
216
216
  if (typeof Symbol !== "undefined") {
217
217
  TMP_KEY_IGNORE = /* @__PURE__ */ Symbol.for("node-ignore");
@@ -491,7 +491,7 @@ var require_ignore = __commonJS({
491
491
  // path matching.
492
492
  // - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
493
493
  // @returns {TestResult} true if a file is ignored
494
- test(path19, checkUnignored, mode) {
494
+ test(path25, checkUnignored, mode) {
495
495
  let ignored = false;
496
496
  let unignored = false;
497
497
  let matchedRule;
@@ -500,7 +500,7 @@ var require_ignore = __commonJS({
500
500
  if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) {
501
501
  return;
502
502
  }
503
- const matched = rule[mode].test(path19);
503
+ const matched = rule[mode].test(path25);
504
504
  if (!matched) {
505
505
  return;
506
506
  }
@@ -521,17 +521,17 @@ var require_ignore = __commonJS({
521
521
  var throwError = (message, Ctor) => {
522
522
  throw new Ctor(message);
523
523
  };
524
- var checkPath = (path19, originalPath, doThrow) => {
525
- if (!isString(path19)) {
524
+ var checkPath = (path25, originalPath, doThrow) => {
525
+ if (!isString(path25)) {
526
526
  return doThrow(
527
527
  `path must be a string, but got \`${originalPath}\``,
528
528
  TypeError
529
529
  );
530
530
  }
531
- if (!path19) {
531
+ if (!path25) {
532
532
  return doThrow(`path must not be empty`, TypeError);
533
533
  }
534
- if (checkPath.isNotRelative(path19)) {
534
+ if (checkPath.isNotRelative(path25)) {
535
535
  const r = "`path.relative()`d";
536
536
  return doThrow(
537
537
  `path should be a ${r} string, but got "${originalPath}"`,
@@ -540,7 +540,7 @@ var require_ignore = __commonJS({
540
540
  }
541
541
  return true;
542
542
  };
543
- var isNotRelative = (path19) => REGEX_TEST_INVALID_PATH.test(path19);
543
+ var isNotRelative = (path25) => REGEX_TEST_INVALID_PATH.test(path25);
544
544
  checkPath.isNotRelative = isNotRelative;
545
545
  checkPath.convert = (p) => p;
546
546
  var Ignore2 = class {
@@ -570,23 +570,23 @@ var require_ignore = __commonJS({
570
570
  }
571
571
  // @returns {TestResult}
572
572
  _test(originalPath, cache, checkUnignored, slices) {
573
- const path19 = originalPath && checkPath.convert(originalPath);
573
+ const path25 = originalPath && checkPath.convert(originalPath);
574
574
  checkPath(
575
- path19,
575
+ path25,
576
576
  originalPath,
577
577
  this._strictPathCheck ? throwError : RETURN_FALSE
578
578
  );
579
- return this._t(path19, cache, checkUnignored, slices);
579
+ return this._t(path25, cache, checkUnignored, slices);
580
580
  }
581
- checkIgnore(path19) {
582
- if (!REGEX_TEST_TRAILING_SLASH.test(path19)) {
583
- return this.test(path19);
581
+ checkIgnore(path25) {
582
+ if (!REGEX_TEST_TRAILING_SLASH.test(path25)) {
583
+ return this.test(path25);
584
584
  }
585
- const slices = path19.split(SLASH).filter(Boolean);
585
+ const slices = path25.split(SLASH2).filter(Boolean);
586
586
  slices.pop();
587
587
  if (slices.length) {
588
588
  const parent = this._t(
589
- slices.join(SLASH) + SLASH,
589
+ slices.join(SLASH2) + SLASH2,
590
590
  this._testCache,
591
591
  true,
592
592
  slices
@@ -595,48 +595,48 @@ var require_ignore = __commonJS({
595
595
  return parent;
596
596
  }
597
597
  }
598
- return this._rules.test(path19, false, MODE_CHECK_IGNORE);
598
+ return this._rules.test(path25, false, MODE_CHECK_IGNORE);
599
599
  }
600
- _t(path19, cache, checkUnignored, slices) {
601
- if (path19 in cache) {
602
- return cache[path19];
600
+ _t(path25, cache, checkUnignored, slices) {
601
+ if (path25 in cache) {
602
+ return cache[path25];
603
603
  }
604
604
  if (!slices) {
605
- slices = path19.split(SLASH).filter(Boolean);
605
+ slices = path25.split(SLASH2).filter(Boolean);
606
606
  }
607
607
  slices.pop();
608
608
  if (!slices.length) {
609
- return cache[path19] = this._rules.test(path19, checkUnignored, MODE_IGNORE);
609
+ return cache[path25] = this._rules.test(path25, checkUnignored, MODE_IGNORE);
610
610
  }
611
611
  const parent = this._t(
612
- slices.join(SLASH) + SLASH,
612
+ slices.join(SLASH2) + SLASH2,
613
613
  cache,
614
614
  checkUnignored,
615
615
  slices
616
616
  );
617
- return cache[path19] = parent.ignored ? parent : this._rules.test(path19, checkUnignored, MODE_IGNORE);
617
+ return cache[path25] = parent.ignored ? parent : this._rules.test(path25, checkUnignored, MODE_IGNORE);
618
618
  }
619
- ignores(path19) {
620
- return this._test(path19, this._ignoreCache, false).ignored;
619
+ ignores(path25) {
620
+ return this._test(path25, this._ignoreCache, false).ignored;
621
621
  }
622
622
  createFilter() {
623
- return (path19) => !this.ignores(path19);
623
+ return (path25) => !this.ignores(path25);
624
624
  }
625
625
  filter(paths) {
626
626
  return makeArray(paths).filter(this.createFilter());
627
627
  }
628
628
  // @returns {TestResult}
629
- test(path19) {
630
- return this._test(path19, this._testCache, true);
629
+ test(path25) {
630
+ return this._test(path25, this._testCache, true);
631
631
  }
632
632
  };
633
633
  var factory = (options) => new Ignore2(options);
634
- var isPathValid = (path19) => checkPath(path19 && checkPath.convert(path19), path19, RETURN_FALSE);
634
+ var isPathValid = (path25) => checkPath(path25 && checkPath.convert(path25), path25, RETURN_FALSE);
635
635
  var setupWindows = () => {
636
636
  const makePosix = (str) => /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str) ? str : str.replace(/\\/g, "/");
637
637
  checkPath.convert = makePosix;
638
638
  const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
639
- checkPath.isNotRelative = (path19) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path19) || isNotRelative(path19);
639
+ checkPath.isNotRelative = (path25) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path25) || isNotRelative(path25);
640
640
  };
641
641
  if (
642
642
  // Detect `process` so that it can run in browsers.
@@ -653,7 +653,10 @@ var require_ignore = __commonJS({
653
653
 
654
654
  // src/cli.ts
655
655
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
656
- import * as path18 from "path";
656
+ import { writeFileSync as writeFileSync6 } from "fs";
657
+ import * as os5 from "os";
658
+ import * as path24 from "path";
659
+ import { pathToFileURL } from "url";
657
660
 
658
661
  // src/config/constants.ts
659
662
  var DEFAULT_INCLUDE = [
@@ -1038,6 +1041,19 @@ var autoDetectProviders = AUTO_DETECT_PROVIDER_ORDER.filter(
1038
1041
  (provider) => provider in EMBEDDING_MODELS
1039
1042
  );
1040
1043
 
1044
+ // src/config/host.ts
1045
+ var HOST_MODES = ["opencode", "codex", "claude", "pi"];
1046
+ function isSupportedHostMode(value) {
1047
+ return HOST_MODES.includes(value);
1048
+ }
1049
+ function parseHostMode(value) {
1050
+ const normalized = (value ?? "").toLowerCase();
1051
+ if (isSupportedHostMode(normalized)) {
1052
+ return normalized;
1053
+ }
1054
+ throw new Error(`Invalid host mode: ${value ?? "(none)"}. Allowed values: ${HOST_MODES.join(", ")}.`);
1055
+ }
1056
+
1041
1057
  // src/eval/compare.ts
1042
1058
  function metricDelta(current, baseline) {
1043
1059
  const absolute = current - baseline;
@@ -1081,9 +1097,9 @@ import { mkdirSync, readFileSync, writeFileSync } from "fs";
1081
1097
  import * as path from "path";
1082
1098
 
1083
1099
  // src/eval/report-formatters.ts
1084
- function assertFiniteNumber(value, path19) {
1100
+ function assertFiniteNumber(value, path25) {
1085
1101
  if (typeof value !== "number" || Number.isNaN(value) || !Number.isFinite(value)) {
1086
- throw new Error(`${path19} must be a finite number`);
1102
+ throw new Error(`${path25} must be a finite number`);
1087
1103
  }
1088
1104
  return value;
1089
1105
  }
@@ -1304,7 +1320,7 @@ function pTimeout(promise, options) {
1304
1320
  } = options;
1305
1321
  let timer;
1306
1322
  let abortHandler;
1307
- const wrappedPromise = new Promise((resolve11, reject) => {
1323
+ const wrappedPromise = new Promise((resolve15, reject) => {
1308
1324
  if (typeof milliseconds !== "number" || Math.sign(milliseconds) !== 1) {
1309
1325
  throw new TypeError(`Expected \`milliseconds\` to be a positive number, got \`${milliseconds}\``);
1310
1326
  }
@@ -1318,7 +1334,7 @@ function pTimeout(promise, options) {
1318
1334
  };
1319
1335
  signal.addEventListener("abort", abortHandler, { once: true });
1320
1336
  }
1321
- promise.then(resolve11, reject);
1337
+ promise.then(resolve15, reject);
1322
1338
  if (milliseconds === Number.POSITIVE_INFINITY) {
1323
1339
  return;
1324
1340
  }
@@ -1326,7 +1342,7 @@ function pTimeout(promise, options) {
1326
1342
  timer = customTimers.setTimeout.call(void 0, () => {
1327
1343
  if (fallback) {
1328
1344
  try {
1329
- resolve11(fallback());
1345
+ resolve15(fallback());
1330
1346
  } catch (error) {
1331
1347
  reject(error);
1332
1348
  }
@@ -1336,7 +1352,7 @@ function pTimeout(promise, options) {
1336
1352
  promise.cancel();
1337
1353
  }
1338
1354
  if (message === false) {
1339
- resolve11();
1355
+ resolve15();
1340
1356
  } else if (message instanceof Error) {
1341
1357
  reject(message);
1342
1358
  } else {
@@ -1738,7 +1754,7 @@ var PQueue = class extends import_index.default {
1738
1754
  // Assign unique ID if not provided
1739
1755
  id: options.id ?? (this.#idAssigner++).toString()
1740
1756
  };
1741
- return new Promise((resolve11, reject) => {
1757
+ return new Promise((resolve15, reject) => {
1742
1758
  const taskSymbol = /* @__PURE__ */ Symbol(`task-${options.id}`);
1743
1759
  let cleanupQueueAbortHandler = () => void 0;
1744
1760
  const run = async () => {
@@ -1778,7 +1794,7 @@ var PQueue = class extends import_index.default {
1778
1794
  })]);
1779
1795
  }
1780
1796
  const result = await operation;
1781
- resolve11(result);
1797
+ resolve15(result);
1782
1798
  this.emit("completed", result);
1783
1799
  } catch (error) {
1784
1800
  reject(error);
@@ -1966,13 +1982,13 @@ var PQueue = class extends import_index.default {
1966
1982
  });
1967
1983
  }
1968
1984
  async #onEvent(event, filter) {
1969
- return new Promise((resolve11) => {
1985
+ return new Promise((resolve15) => {
1970
1986
  const listener = () => {
1971
1987
  if (filter && !filter()) {
1972
1988
  return;
1973
1989
  }
1974
1990
  this.off(event, listener);
1975
- resolve11();
1991
+ resolve15();
1976
1992
  };
1977
1993
  this.on(event, listener);
1978
1994
  });
@@ -2258,7 +2274,7 @@ async function onAttemptFailure({ error, attemptNumber, retriesConsumed, startTi
2258
2274
  const finalDelay = Math.min(delayTime, remainingTime);
2259
2275
  options.signal?.throwIfAborted();
2260
2276
  if (finalDelay > 0) {
2261
- await new Promise((resolve11, reject) => {
2277
+ await new Promise((resolve15, reject) => {
2262
2278
  const onAbort = () => {
2263
2279
  clearTimeout(timeoutToken);
2264
2280
  options.signal?.removeEventListener("abort", onAbort);
@@ -2266,7 +2282,7 @@ async function onAttemptFailure({ error, attemptNumber, retriesConsumed, startTi
2266
2282
  };
2267
2283
  const timeoutToken = setTimeout(() => {
2268
2284
  options.signal?.removeEventListener("abort", onAbort);
2269
- resolve11();
2285
+ resolve15();
2270
2286
  }, finalDelay);
2271
2287
  if (options.unref) {
2272
2288
  timeoutToken.unref?.();
@@ -3054,8 +3070,64 @@ function isHiddenPathSegment(part) {
3054
3070
  function isBuildPathSegment(part) {
3055
3071
  return part.toLowerCase().includes("build");
3056
3072
  }
3073
+ function hasFilteredPathSegment(relativePath, separator = path3.sep) {
3074
+ return relativePath.split(separator).some(
3075
+ (part) => isHiddenPathSegment(part) || isBuildPathSegment(part)
3076
+ );
3077
+ }
3078
+ var RESTRICTED_DIRECTORIES = /* @__PURE__ */ new Set([
3079
+ // macOS
3080
+ "library",
3081
+ "applications",
3082
+ "system",
3083
+ "volumes",
3084
+ "private",
3085
+ "cores",
3086
+ // Linux
3087
+ "proc",
3088
+ "sys",
3089
+ "dev",
3090
+ "run",
3091
+ "snap",
3092
+ // Windows
3093
+ "windows",
3094
+ "programdata",
3095
+ "program files",
3096
+ "program files (x86)",
3097
+ "$recycle.bin"
3098
+ ]);
3099
+ function isRestrictedDirectory(relativePath, separator = path3.sep) {
3100
+ const firstSegment = relativePath.split(separator)[0];
3101
+ if (!firstSegment) return false;
3102
+ return RESTRICTED_DIRECTORIES.has(firstSegment.toLowerCase());
3103
+ }
3057
3104
 
3058
3105
  // src/utils/files.ts
3106
+ var PROJECT_MARKERS = [
3107
+ ".git",
3108
+ "package.json",
3109
+ "Cargo.toml",
3110
+ "go.mod",
3111
+ "pyproject.toml",
3112
+ "setup.py",
3113
+ "requirements.txt",
3114
+ "Gemfile",
3115
+ "composer.json",
3116
+ "pom.xml",
3117
+ "build.gradle",
3118
+ "CMakeLists.txt",
3119
+ "Makefile",
3120
+ ".opencode",
3121
+ ".codebase-index"
3122
+ ];
3123
+ function hasProjectMarker(projectRoot) {
3124
+ for (const marker of PROJECT_MARKERS) {
3125
+ if (existsSync2(path4.join(projectRoot, marker))) {
3126
+ return true;
3127
+ }
3128
+ }
3129
+ return false;
3130
+ }
3059
3131
  function createIgnoreFilter(projectRoot) {
3060
3132
  const ig = (0, import_ignore.default)();
3061
3133
  const defaultIgnores = [
@@ -3070,6 +3142,7 @@ function createIgnoreFilter(projectRoot) {
3070
3142
  "target",
3071
3143
  "vendor",
3072
3144
  ".opencode",
3145
+ ".codebase-index",
3073
3146
  ".*",
3074
3147
  "**/.*",
3075
3148
  "**/.*/**",
@@ -3083,6 +3156,26 @@ function createIgnoreFilter(projectRoot) {
3083
3156
  }
3084
3157
  return ig;
3085
3158
  }
3159
+ function shouldIncludeFile(filePath, projectRoot, includePatterns, excludePatterns, ignoreFilter) {
3160
+ const relativePath = path4.relative(projectRoot, filePath);
3161
+ if (hasFilteredPathSegment(relativePath, path4.sep)) {
3162
+ return false;
3163
+ }
3164
+ if (ignoreFilter.ignores(relativePath)) {
3165
+ return false;
3166
+ }
3167
+ for (const pattern of excludePatterns) {
3168
+ if (matchGlob(relativePath, pattern)) {
3169
+ return false;
3170
+ }
3171
+ }
3172
+ for (const pattern of includePatterns) {
3173
+ if (matchGlob(relativePath, pattern)) {
3174
+ return true;
3175
+ }
3176
+ }
3177
+ return false;
3178
+ }
3086
3179
  function matchGlob(filePath, pattern) {
3087
3180
  if (pattern.startsWith("**/")) {
3088
3181
  const withoutPrefix = pattern.slice(3);
@@ -3124,8 +3217,8 @@ async function* walkDirectory(dir, projectRoot, includePatterns, excludePatterns
3124
3217
  if (entry.isDirectory()) {
3125
3218
  subdirs.push({ fullPath, relativePath });
3126
3219
  } else if (entry.isFile()) {
3127
- const stat = await fsPromises.stat(fullPath);
3128
- if (stat.size > maxFileSize) {
3220
+ const stat4 = await fsPromises.stat(fullPath);
3221
+ if (stat4.size > maxFileSize) {
3129
3222
  skipped.push({ path: relativePath, reason: "too_large" });
3130
3223
  continue;
3131
3224
  }
@@ -3143,7 +3236,7 @@ async function* walkDirectory(dir, projectRoot, includePatterns, excludePatterns
3143
3236
  }
3144
3237
  }
3145
3238
  if (matched) {
3146
- filesInDir.push({ path: fullPath, size: stat.size });
3239
+ filesInDir.push({ path: fullPath, size: stat4.size });
3147
3240
  }
3148
3241
  }
3149
3242
  }
@@ -3200,8 +3293,8 @@ async function collectFiles(projectRoot, includePatterns, excludePatterns, maxFi
3200
3293
  }
3201
3294
  for (const resolvedKbRoot of normalizedRoots) {
3202
3295
  try {
3203
- const stat = await fsPromises.stat(resolvedKbRoot);
3204
- if (!stat.isDirectory()) {
3296
+ const stat4 = await fsPromises.stat(resolvedKbRoot);
3297
+ if (!stat4.isDirectory()) {
3205
3298
  skipped.push({ path: resolvedKbRoot, reason: "excluded" });
3206
3299
  continue;
3207
3300
  }
@@ -4408,11 +4501,11 @@ function resolveGitDir(repoRoot) {
4408
4501
  return null;
4409
4502
  }
4410
4503
  try {
4411
- const stat = statSync2(gitPath);
4412
- if (stat.isDirectory()) {
4504
+ const stat4 = statSync2(gitPath);
4505
+ if (stat4.isDirectory()) {
4413
4506
  return gitPath;
4414
4507
  }
4415
- if (stat.isFile()) {
4508
+ if (stat4.isFile()) {
4416
4509
  const content = readFileSync5(gitPath, "utf-8").trim();
4417
4510
  const match = content.match(/^gitdir:\s*(.+)$/);
4418
4511
  if (match) {
@@ -4477,13 +4570,46 @@ function getBranchOrDefault(repoRoot) {
4477
4570
  }
4478
4571
  return getCurrentBranch(repoRoot) ?? "default";
4479
4572
  }
4573
+ function getHeadPath(repoRoot) {
4574
+ const gitDir = resolveGitDir(repoRoot);
4575
+ if (gitDir) {
4576
+ return path7.join(gitDir, "HEAD");
4577
+ }
4578
+ return path7.join(repoRoot, ".git", "HEAD");
4579
+ }
4480
4580
 
4481
4581
  // src/config/paths.ts
4482
4582
  import { existsSync as existsSync5 } from "fs";
4483
4583
  import * as os3 from "os";
4484
4584
  import * as path8 from "path";
4485
- var PROJECT_CONFIG_RELATIVE_PATH = path8.join(".opencode", "codebase-index.json");
4486
- var PROJECT_INDEX_RELATIVE_PATH = path8.join(".opencode", "index");
4585
+ var OPENCODE_PROJECT_CONFIG_RELATIVE_PATH = path8.join(".opencode", "codebase-index.json");
4586
+ var OPENCODE_PROJECT_INDEX_RELATIVE_PATH = path8.join(".opencode", "index");
4587
+ var CODEBASE_INDEX_DIR = ".codebase-index";
4588
+ var CODEBASE_PROJECT_CONFIG_RELATIVE_PATH = path8.join(CODEBASE_INDEX_DIR, "config.json");
4589
+ var CODEBASE_PROJECT_INDEX_RELATIVE_PATH = path8.join(CODEBASE_INDEX_DIR, "index");
4590
+ var CLAUDE_DIR = ".claude";
4591
+ var CLAUDE_PROJECT_CONFIG_RELATIVE_PATH = path8.join(CLAUDE_DIR, "codebase-index.json");
4592
+ var CLAUDE_PROJECT_INDEX_RELATIVE_PATH = path8.join(CLAUDE_DIR, "index");
4593
+ function getProjectConfigRelativePath(host) {
4594
+ switch (host) {
4595
+ case "opencode":
4596
+ return OPENCODE_PROJECT_CONFIG_RELATIVE_PATH;
4597
+ case "claude":
4598
+ return CLAUDE_PROJECT_CONFIG_RELATIVE_PATH;
4599
+ default:
4600
+ return CODEBASE_PROJECT_CONFIG_RELATIVE_PATH;
4601
+ }
4602
+ }
4603
+ function getProjectIndexRelativePath(host) {
4604
+ switch (host) {
4605
+ case "opencode":
4606
+ return OPENCODE_PROJECT_INDEX_RELATIVE_PATH;
4607
+ case "claude":
4608
+ return CLAUDE_PROJECT_INDEX_RELATIVE_PATH;
4609
+ default:
4610
+ return CODEBASE_PROJECT_INDEX_RELATIVE_PATH;
4611
+ }
4612
+ }
4487
4613
  function resolveWorktreeFallbackPath(projectRoot, relativePath) {
4488
4614
  const mainRepoRoot = resolveWorktreeMainRepoRoot(projectRoot);
4489
4615
  if (!mainRepoRoot) {
@@ -4492,31 +4618,122 @@ function resolveWorktreeFallbackPath(projectRoot, relativePath) {
4492
4618
  const fallbackPath = path8.join(mainRepoRoot, relativePath);
4493
4619
  return existsSync5(fallbackPath) ? fallbackPath : null;
4494
4620
  }
4495
- function hasProjectConfig(projectRoot) {
4496
- return existsSync5(path8.join(projectRoot, PROJECT_CONFIG_RELATIVE_PATH));
4621
+ function resolveWorktreeFallbackProjectIndexPath(projectRoot, host) {
4622
+ const inheritedHostPath = resolveWorktreeFallbackPath(projectRoot, getProjectIndexRelativePath(host));
4623
+ if (inheritedHostPath) {
4624
+ return inheritedHostPath;
4625
+ }
4626
+ if (host !== "opencode") {
4627
+ return resolveWorktreeFallbackPath(projectRoot, OPENCODE_PROJECT_INDEX_RELATIVE_PATH);
4628
+ }
4629
+ return null;
4497
4630
  }
4498
- function getGlobalIndexPath() {
4499
- return path8.join(os3.homedir(), ".opencode", "global-index");
4631
+ function getHostProjectIndexRelativePath(host) {
4632
+ return getProjectIndexRelativePath(host);
4500
4633
  }
4501
- function resolveProjectConfigPath(projectRoot) {
4502
- const localConfigPath = path8.join(projectRoot, PROJECT_CONFIG_RELATIVE_PATH);
4503
- if (existsSync5(localConfigPath)) {
4504
- return localConfigPath;
4634
+ function hasHostProjectConfig(projectRoot, host) {
4635
+ return existsSync5(path8.join(projectRoot, getProjectConfigRelativePath(host)));
4636
+ }
4637
+ function getGlobalIndexPath(host = "opencode") {
4638
+ switch (host) {
4639
+ case "opencode":
4640
+ return path8.join(os3.homedir(), ".opencode", "global-index");
4641
+ case "claude":
4642
+ return path8.join(os3.homedir(), ".claude", "global-index");
4643
+ default:
4644
+ return path8.join(os3.homedir(), ".codebase-index", "global-index");
4645
+ }
4646
+ }
4647
+ function getGlobalConfigPath(host = "opencode") {
4648
+ switch (host) {
4649
+ case "opencode":
4650
+ return path8.join(os3.homedir(), ".config", "opencode", "codebase-index.json");
4651
+ case "claude":
4652
+ return path8.join(os3.homedir(), ".claude", "codebase-index.json");
4653
+ default:
4654
+ return path8.join(os3.homedir(), ".config", "codebase-index", "config.json");
4655
+ }
4656
+ }
4657
+ function resolveGlobalConfigPath(host = "opencode") {
4658
+ const hostConfigPath = getGlobalConfigPath(host);
4659
+ if (existsSync5(hostConfigPath)) {
4660
+ return hostConfigPath;
4661
+ }
4662
+ if (host !== "opencode") {
4663
+ const legacyConfigPath = getGlobalConfigPath("opencode");
4664
+ if (existsSync5(legacyConfigPath)) {
4665
+ return legacyConfigPath;
4666
+ }
4667
+ }
4668
+ return hostConfigPath;
4669
+ }
4670
+ function resolveGlobalIndexPath(host = "opencode") {
4671
+ const hostIndexPath = getGlobalIndexPath(host);
4672
+ if (existsSync5(hostIndexPath)) {
4673
+ return hostIndexPath;
4674
+ }
4675
+ if (host !== "opencode") {
4676
+ const legacyIndexPath = getGlobalIndexPath("opencode");
4677
+ if (existsSync5(legacyIndexPath)) {
4678
+ return legacyIndexPath;
4679
+ }
4680
+ }
4681
+ return hostIndexPath;
4682
+ }
4683
+ function resolveProjectConfigPath(projectRoot, host = "opencode") {
4684
+ const hostConfigPath = path8.join(projectRoot, getProjectConfigRelativePath(host));
4685
+ if (existsSync5(hostConfigPath)) {
4686
+ return hostConfigPath;
4687
+ }
4688
+ if (host !== "opencode") {
4689
+ const legacyConfigPath = path8.join(projectRoot, OPENCODE_PROJECT_CONFIG_RELATIVE_PATH);
4690
+ if (existsSync5(legacyConfigPath)) {
4691
+ return legacyConfigPath;
4692
+ }
4693
+ }
4694
+ const hostFallback = resolveWorktreeFallbackPath(projectRoot, getProjectConfigRelativePath(host));
4695
+ if (hostFallback) {
4696
+ return hostFallback;
4697
+ }
4698
+ if (host !== "opencode") {
4699
+ const legacyFallback = resolveWorktreeFallbackPath(projectRoot, OPENCODE_PROJECT_CONFIG_RELATIVE_PATH);
4700
+ if (legacyFallback) {
4701
+ return legacyFallback;
4702
+ }
4505
4703
  }
4506
- return resolveWorktreeFallbackPath(projectRoot, PROJECT_CONFIG_RELATIVE_PATH) ?? localConfigPath;
4704
+ return hostConfigPath;
4507
4705
  }
4508
- function resolveProjectIndexPath(projectRoot, scope) {
4706
+ function resolveWritableProjectConfigPath(projectRoot, host = "opencode") {
4707
+ return path8.join(projectRoot, getProjectConfigRelativePath(host));
4708
+ }
4709
+ function resolveProjectIndexPath(projectRoot, scope, host = "opencode") {
4509
4710
  if (scope === "global") {
4510
- return getGlobalIndexPath();
4711
+ return resolveGlobalIndexPath(host);
4511
4712
  }
4512
- const localIndexPath = path8.join(projectRoot, PROJECT_INDEX_RELATIVE_PATH);
4713
+ const localIndexPath = path8.join(projectRoot, getProjectIndexRelativePath(host));
4513
4714
  if (existsSync5(localIndexPath)) {
4514
4715
  return localIndexPath;
4515
4716
  }
4516
- if (hasProjectConfig(projectRoot)) {
4717
+ if (host !== "opencode") {
4718
+ const legacyIndexPath = path8.join(projectRoot, OPENCODE_PROJECT_INDEX_RELATIVE_PATH);
4719
+ if (existsSync5(legacyIndexPath) && !hasHostProjectConfig(projectRoot, host)) {
4720
+ return legacyIndexPath;
4721
+ }
4722
+ }
4723
+ if (hasHostProjectConfig(projectRoot, host)) {
4517
4724
  return localIndexPath;
4518
4725
  }
4519
- return resolveWorktreeFallbackPath(projectRoot, PROJECT_INDEX_RELATIVE_PATH) ?? localIndexPath;
4726
+ const hostFallback = resolveWorktreeFallbackPath(projectRoot, getProjectIndexRelativePath(host));
4727
+ if (hostFallback) {
4728
+ return hostFallback;
4729
+ }
4730
+ if (host !== "opencode") {
4731
+ const legacyFallback = resolveWorktreeFallbackPath(projectRoot, OPENCODE_PROJECT_INDEX_RELATIVE_PATH);
4732
+ if (legacyFallback) {
4733
+ return legacyFallback;
4734
+ }
4735
+ }
4736
+ return localIndexPath;
4520
4737
  }
4521
4738
 
4522
4739
  // src/tools/changed-files.ts
@@ -4609,8 +4826,8 @@ function normalizeFiles(rawFiles, projectRoot) {
4609
4826
  const trimmed = raw.trim();
4610
4827
  if (!trimmed) continue;
4611
4828
  const absolute = path9.resolve(projectRoot, trimmed);
4612
- const relative5 = path9.relative(projectRoot, absolute);
4613
- const cleaned = relative5.startsWith("./") ? relative5.slice(2) : relative5;
4829
+ const relative10 = path9.relative(projectRoot, absolute);
4830
+ const cleaned = relative10.startsWith("./") ? relative10.slice(2) : relative10;
4614
4831
  if (!seen.has(cleaned)) {
4615
4832
  seen.add(cleaned);
4616
4833
  result.push(cleaned);
@@ -5888,6 +6105,7 @@ function unionCandidates(semanticCandidates, keywordCandidates) {
5888
6105
  return Array.from(byId.values());
5889
6106
  }
5890
6107
  var Indexer = class {
6108
+ host;
5891
6109
  config;
5892
6110
  projectRoot;
5893
6111
  indexPath;
@@ -5909,9 +6127,10 @@ var Indexer = class {
5909
6127
  querySimilarityThreshold = 0.85;
5910
6128
  indexCompatibility = null;
5911
6129
  indexingLockPath = "";
5912
- constructor(projectRoot, config) {
6130
+ constructor(projectRoot, config, host = "opencode") {
5913
6131
  this.projectRoot = projectRoot;
5914
6132
  this.config = config;
6133
+ this.host = host;
5915
6134
  this.indexPath = this.getIndexPath();
5916
6135
  this.fileHashCachePath = path10.join(this.indexPath, "file-hashes.json");
5917
6136
  this.failedBatchesPath = path10.join(this.indexPath, "failed-batches.json");
@@ -5919,7 +6138,7 @@ var Indexer = class {
5919
6138
  this.logger = initializeLogger(config.debug);
5920
6139
  }
5921
6140
  getIndexPath() {
5922
- return resolveProjectIndexPath(this.projectRoot, this.config.scope);
6141
+ return resolveProjectIndexPath(this.projectRoot, this.config.scope, this.host);
5923
6142
  }
5924
6143
  loadFileHashCache() {
5925
6144
  if (!existsSync6(this.fileHashCachePath)) {
@@ -7357,7 +7576,7 @@ var Indexer = class {
7357
7576
  for (const requestBatch of requestBatches) {
7358
7577
  queue.add(async () => {
7359
7578
  if (rateLimitBackoffMs > 0) {
7360
- await new Promise((resolve11) => setTimeout(resolve11, rateLimitBackoffMs));
7579
+ await new Promise((resolve15) => setTimeout(resolve15, rateLimitBackoffMs));
7361
7580
  }
7362
7581
  try {
7363
7582
  const result = await pRetry(
@@ -7918,8 +8137,14 @@ var Indexer = class {
7918
8137
  this.indexCompatibility = compatibility;
7919
8138
  return;
7920
8139
  }
7921
- const localProjectIndexPath = path10.join(this.projectRoot, ".opencode", "index");
7922
- if (path10.resolve(this.indexPath) !== path10.resolve(localProjectIndexPath)) {
8140
+ const localProjectIndexPaths = [path10.join(this.projectRoot, getHostProjectIndexRelativePath(this.host))];
8141
+ if (this.host !== "opencode") {
8142
+ localProjectIndexPaths.push(path10.join(this.projectRoot, getHostProjectIndexRelativePath("opencode")));
8143
+ }
8144
+ const isLocalProjectIndex = localProjectIndexPaths.some(
8145
+ (localPath) => path10.resolve(this.indexPath) === path10.resolve(localPath)
8146
+ );
8147
+ if (!isLocalProjectIndex) {
7923
8148
  throw new Error(
7924
8149
  "Project-scoped force rebuild is unsafe while using an inherited worktree index. Create a local project config boundary before clearing the index."
7925
8150
  );
@@ -8364,9 +8589,9 @@ var Indexer = class {
8364
8589
  const { database } = await this.ensureInitialized();
8365
8590
  let shortest = [];
8366
8591
  for (const branchKey of this.getBranchCatalogKeys()) {
8367
- const path19 = database.findShortestPath(fromName, toName, branchKey, maxDepth);
8368
- if (path19.length > 0 && (shortest.length === 0 || path19.length < shortest.length)) {
8369
- shortest = path19;
8592
+ const path25 = database.findShortestPath(fromName, toName, branchKey, maxDepth);
8593
+ if (path25.length > 0 && (shortest.length === 0 || path25.length < shortest.length)) {
8594
+ shortest = path25;
8370
8595
  }
8371
8596
  }
8372
8597
  return shortest;
@@ -8552,6 +8777,46 @@ var Indexer = class {
8552
8777
  conflictingPRs
8553
8778
  };
8554
8779
  }
8780
+ async getVisualizationData(options) {
8781
+ const { database, store } = await this.ensureInitialized();
8782
+ const seenSymbols = /* @__PURE__ */ new Map();
8783
+ const seenEdges = /* @__PURE__ */ new Map();
8784
+ for (const branchKey of this.getBranchCatalogKeys()) {
8785
+ const symbolIds = database.getBranchSymbolIds(branchKey);
8786
+ const symbolIdSet = new Set(symbolIds);
8787
+ const chunkIds = database.getBranchChunkIds(branchKey);
8788
+ const metadataMap = chunkIds.length > 0 ? store.getMetadataBatch(chunkIds) : /* @__PURE__ */ new Map();
8789
+ const filePaths = /* @__PURE__ */ new Set();
8790
+ for (const [, meta] of metadataMap) {
8791
+ if (meta.filePath) filePaths.add(meta.filePath);
8792
+ }
8793
+ const directory = options?.directory?.replace(/\/$/, "");
8794
+ const absoluteDirectoryFilter = directory ? path10.resolve(this.projectRoot, directory) : void 0;
8795
+ for (const filePath of filePaths) {
8796
+ if (directory) {
8797
+ const absoluteFilePath = path10.resolve(filePath);
8798
+ const matchesRelative = filePath === directory || filePath.startsWith(directory + "/");
8799
+ const matchesProjectRelative = absoluteDirectoryFilter !== void 0 && (absoluteFilePath === absoluteDirectoryFilter || absoluteFilePath.startsWith(absoluteDirectoryFilter + path10.sep));
8800
+ if (!matchesRelative && !matchesProjectRelative) {
8801
+ continue;
8802
+ }
8803
+ }
8804
+ for (const sym of database.getSymbolsByFile(filePath)) {
8805
+ if (symbolIdSet.has(sym.id) && !seenSymbols.has(sym.id)) {
8806
+ seenSymbols.set(sym.id, sym);
8807
+ }
8808
+ }
8809
+ }
8810
+ for (const symbolId of seenSymbols.keys()) {
8811
+ for (const edge of database.getCallees(symbolId, branchKey)) {
8812
+ if (!seenEdges.has(edge.id)) {
8813
+ seenEdges.set(edge.id, edge);
8814
+ }
8815
+ }
8816
+ }
8817
+ }
8818
+ return { symbols: [...seenSymbols.values()], edges: [...seenEdges.values()] };
8819
+ }
8555
8820
  async close() {
8556
8821
  await this.database?.close();
8557
8822
  this.database = null;
@@ -9024,23 +9289,23 @@ function isRecord2(value) {
9024
9289
  function isStringArray3(value) {
9025
9290
  return Array.isArray(value) && value.every((item) => typeof item === "string");
9026
9291
  }
9027
- function asPositiveNumber(value, path19) {
9292
+ function asPositiveNumber(value, path25) {
9028
9293
  if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
9029
- throw new Error(`${path19} must be a non-negative number`);
9294
+ throw new Error(`${path25} must be a non-negative number`);
9030
9295
  }
9031
9296
  return value;
9032
9297
  }
9033
- function parseQueryType(value, path19) {
9298
+ function parseQueryType(value, path25) {
9034
9299
  if (value === "definition" || value === "implementation-intent" || value === "similarity" || value === "keyword-heavy") {
9035
9300
  return value;
9036
9301
  }
9037
9302
  throw new Error(
9038
- `${path19} must be one of: definition, implementation-intent, similarity, keyword-heavy`
9303
+ `${path25} must be one of: definition, implementation-intent, similarity, keyword-heavy`
9039
9304
  );
9040
9305
  }
9041
- function parseExpected(input, path19) {
9306
+ function parseExpected(input, path25) {
9042
9307
  if (!isRecord2(input)) {
9043
- throw new Error(`${path19} must be an object`);
9308
+ throw new Error(`${path25} must be an object`);
9044
9309
  }
9045
9310
  const filePathRaw = input.filePath;
9046
9311
  const acceptableFilesRaw = input.acceptableFiles;
@@ -9049,16 +9314,16 @@ function parseExpected(input, path19) {
9049
9314
  const filePath = typeof filePathRaw === "string" ? filePathRaw : void 0;
9050
9315
  const acceptableFiles = isStringArray3(acceptableFilesRaw) ? acceptableFilesRaw : void 0;
9051
9316
  if (!filePath && (!acceptableFiles || acceptableFiles.length === 0)) {
9052
- throw new Error(`${path19} must include either expected.filePath or expected.acceptableFiles`);
9317
+ throw new Error(`${path25} must include either expected.filePath or expected.acceptableFiles`);
9053
9318
  }
9054
9319
  if (acceptableFilesRaw !== void 0 && !isStringArray3(acceptableFilesRaw)) {
9055
- throw new Error(`${path19}.acceptableFiles must be an array of strings`);
9320
+ throw new Error(`${path25}.acceptableFiles must be an array of strings`);
9056
9321
  }
9057
9322
  if (symbolRaw !== void 0 && typeof symbolRaw !== "string") {
9058
- throw new Error(`${path19}.symbol must be a string when provided`);
9323
+ throw new Error(`${path25}.symbol must be a string when provided`);
9059
9324
  }
9060
9325
  if (branchRaw !== void 0 && typeof branchRaw !== "string") {
9061
- throw new Error(`${path19}.branch must be a string when provided`);
9326
+ throw new Error(`${path25}.branch must be a string when provided`);
9062
9327
  }
9063
9328
  return {
9064
9329
  filePath,
@@ -9068,25 +9333,25 @@ function parseExpected(input, path19) {
9068
9333
  };
9069
9334
  }
9070
9335
  function parseQuery(input, index) {
9071
- const path19 = `queries[${index}]`;
9336
+ const path25 = `queries[${index}]`;
9072
9337
  if (!isRecord2(input)) {
9073
- throw new Error(`${path19} must be an object`);
9338
+ throw new Error(`${path25} must be an object`);
9074
9339
  }
9075
9340
  const id = input.id;
9076
9341
  const query = input.query;
9077
9342
  const queryType = input.queryType;
9078
9343
  const expected = input.expected;
9079
9344
  if (typeof id !== "string" || id.trim().length === 0) {
9080
- throw new Error(`${path19}.id must be a non-empty string`);
9345
+ throw new Error(`${path25}.id must be a non-empty string`);
9081
9346
  }
9082
9347
  if (typeof query !== "string" || query.trim().length === 0) {
9083
- throw new Error(`${path19}.query must be a non-empty string`);
9348
+ throw new Error(`${path25}.query must be a non-empty string`);
9084
9349
  }
9085
9350
  return {
9086
9351
  id,
9087
9352
  query,
9088
- queryType: parseQueryType(queryType, `${path19}.queryType`),
9089
- expected: parseExpected(expected, `${path19}.expected`)
9353
+ queryType: parseQueryType(queryType, `${path25}.queryType`),
9354
+ expected: parseExpected(expected, `${path25}.expected`)
9090
9355
  };
9091
9356
  }
9092
9357
  function parseGoldenDataset(raw, sourceLabel) {
@@ -9669,8 +9934,7 @@ async function handleEvalCommand(args, cwd) {
9669
9934
 
9670
9935
  // src/mcp-server.ts
9671
9936
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9672
- import * as path17 from "path";
9673
- import { existsSync as existsSync10 } from "fs";
9937
+ import { readFileSync as readFileSync10 } from "fs";
9674
9938
 
9675
9939
  // src/mcp-server/register-prompts.ts
9676
9940
  import { z } from "zod";
@@ -9765,168 +10029,6 @@ Use the implementation_lookup tool to find where this symbol is defined. This pr
9765
10029
  // src/mcp-server/register-tools.ts
9766
10030
  import { z as z2 } from "zod";
9767
10031
 
9768
- // src/config/merger.ts
9769
- import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
9770
- import * as os5 from "os";
9771
- import * as path16 from "path";
9772
- var PROJECT_OVERRIDE_KEYS = [
9773
- "embeddingProvider",
9774
- "customProvider",
9775
- "embeddingModel",
9776
- "reranker",
9777
- "include",
9778
- "exclude",
9779
- "indexing",
9780
- "search",
9781
- "debug",
9782
- "scope"
9783
- ];
9784
- var MERGE_ARRAY_KEYS = ["knowledgeBases", "additionalInclude"];
9785
- function isRecord3(value) {
9786
- return typeof value === "object" && value !== null && !Array.isArray(value);
9787
- }
9788
- function isStringArray4(value) {
9789
- return Array.isArray(value) && value.every((item) => typeof item === "string");
9790
- }
9791
- function applyProjectOverride(merged, normalizedProjectConfig, globalConfig, key) {
9792
- if (key in normalizedProjectConfig) {
9793
- merged[key] = normalizedProjectConfig[key];
9794
- return;
9795
- }
9796
- if (key in globalConfig) {
9797
- merged[key] = globalConfig[key];
9798
- }
9799
- }
9800
- function mergeUniqueStringArray(values) {
9801
- return [...new Set(values.map((value) => String(value).trim()))];
9802
- }
9803
- function normalizeKnowledgeBasePath(value) {
9804
- let normalized = path16.normalize(String(value).trim());
9805
- const root = path16.parse(normalized).root;
9806
- while (normalized.length > root.length && /[\\/]$/.test(normalized)) {
9807
- normalized = normalized.slice(0, -1);
9808
- }
9809
- return normalized;
9810
- }
9811
- function mergeKnowledgeBasePaths(values) {
9812
- return [...new Set(values.map((value) => normalizeKnowledgeBasePath(value)).filter((value) => value.length > 0))];
9813
- }
9814
- function validateConfigLayerShape(rawConfig, filePath) {
9815
- if (!isRecord3(rawConfig)) {
9816
- throw new Error(`Config file ${filePath} must contain a JSON object at the root.`);
9817
- }
9818
- if (rawConfig.knowledgeBases !== void 0 && !isStringArray4(rawConfig.knowledgeBases)) {
9819
- throw new Error(`Config file ${filePath} field 'knowledgeBases' must be an array of strings.`);
9820
- }
9821
- if (rawConfig.additionalInclude !== void 0 && !isStringArray4(rawConfig.additionalInclude)) {
9822
- throw new Error(`Config file ${filePath} field 'additionalInclude' must be an array of strings.`);
9823
- }
9824
- if (rawConfig.include !== void 0 && !isStringArray4(rawConfig.include)) {
9825
- throw new Error(`Config file ${filePath} field 'include' must be an array of strings.`);
9826
- }
9827
- if (rawConfig.exclude !== void 0 && !isStringArray4(rawConfig.exclude)) {
9828
- throw new Error(`Config file ${filePath} field 'exclude' must be an array of strings.`);
9829
- }
9830
- for (const section of ["customProvider", "indexing", "search", "debug", "reranker"]) {
9831
- const value = rawConfig[section];
9832
- if (value !== void 0 && !isRecord3(value)) {
9833
- throw new Error(`Config file ${filePath} field '${section}' must be an object.`);
9834
- }
9835
- }
9836
- return rawConfig;
9837
- }
9838
- function loadJsonFile(filePath) {
9839
- if (!existsSync9(filePath)) {
9840
- return null;
9841
- }
9842
- try {
9843
- const content = readFileSync9(filePath, "utf-8");
9844
- return validateConfigLayerShape(JSON.parse(content), filePath);
9845
- } catch (error) {
9846
- if (error instanceof Error && error.message.startsWith("Config file ")) {
9847
- throw error;
9848
- }
9849
- const message = error instanceof Error ? error.message : String(error);
9850
- throw new Error(`Failed to load config file ${filePath}: ${message}`);
9851
- }
9852
- return null;
9853
- }
9854
- function materializeLocalProjectConfig(projectRoot, config) {
9855
- const localConfigPath = path16.join(projectRoot, ".opencode", "codebase-index.json");
9856
- mkdirSync4(path16.dirname(localConfigPath), { recursive: true });
9857
- writeFileSync4(localConfigPath, JSON.stringify(config, null, 2), "utf-8");
9858
- return localConfigPath;
9859
- }
9860
- function loadProjectConfigLayer(projectRoot) {
9861
- const projectConfigPath = resolveProjectConfigPath(projectRoot);
9862
- const projectConfig = loadJsonFile(projectConfigPath);
9863
- if (!projectConfig) {
9864
- return {};
9865
- }
9866
- const normalizedConfig = { ...projectConfig };
9867
- const projectConfigBaseDir = path16.dirname(path16.dirname(projectConfigPath));
9868
- if (Array.isArray(normalizedConfig.knowledgeBases)) {
9869
- normalizedConfig.knowledgeBases = resolveInheritedKnowledgeBaseEntries(
9870
- normalizedConfig.knowledgeBases,
9871
- projectConfigBaseDir,
9872
- projectRoot
9873
- );
9874
- }
9875
- return normalizedConfig;
9876
- }
9877
- function loadMergedConfig(projectRoot) {
9878
- const globalConfigPath = os5.homedir() + "/.config/opencode/codebase-index.json";
9879
- const projectConfigPath = resolveProjectConfigPath(projectRoot);
9880
- let globalConfig = null;
9881
- let globalConfigError = null;
9882
- try {
9883
- globalConfig = loadJsonFile(globalConfigPath);
9884
- } catch (error) {
9885
- globalConfigError = error instanceof Error ? error : new Error(String(error));
9886
- }
9887
- const projectConfig = loadJsonFile(projectConfigPath);
9888
- const normalizedProjectConfig = loadProjectConfigLayer(projectRoot);
9889
- if (globalConfigError) {
9890
- if (!projectConfig) {
9891
- throw globalConfigError;
9892
- }
9893
- globalConfig = null;
9894
- }
9895
- if (!globalConfig && !projectConfig) {
9896
- return {};
9897
- }
9898
- if (!projectConfig && globalConfig) {
9899
- return globalConfig;
9900
- }
9901
- if (!globalConfig && projectConfig) {
9902
- return normalizedProjectConfig;
9903
- }
9904
- if (!globalConfig || !projectConfig) {
9905
- return globalConfig ?? normalizedProjectConfig;
9906
- }
9907
- const merged = { ...globalConfig };
9908
- for (const key of PROJECT_OVERRIDE_KEYS) {
9909
- applyProjectOverride(merged, normalizedProjectConfig, globalConfig, key);
9910
- }
9911
- if (projectConfig) {
9912
- for (const key of Object.keys(projectConfig)) {
9913
- if (PROJECT_OVERRIDE_KEYS.includes(key) || MERGE_ARRAY_KEYS.includes(key)) {
9914
- continue;
9915
- }
9916
- merged[key] = normalizedProjectConfig[key];
9917
- }
9918
- }
9919
- const globalKbs = globalConfig && Array.isArray(globalConfig.knowledgeBases) ? globalConfig.knowledgeBases : [];
9920
- const projectKbs = projectConfig ? Array.isArray(normalizedProjectConfig.knowledgeBases) ? normalizedProjectConfig.knowledgeBases : [] : [];
9921
- const allKbs = [...globalKbs, ...projectKbs];
9922
- merged.knowledgeBases = mergeKnowledgeBasePaths(allKbs);
9923
- const globalAdditional = globalConfig && Array.isArray(globalConfig.additionalInclude) ? globalConfig.additionalInclude : [];
9924
- const projectAdditional = projectConfig && Array.isArray(projectConfig.additionalInclude) ? projectConfig.additionalInclude : [];
9925
- const allAdditional = [...globalAdditional, ...projectAdditional];
9926
- merged.additionalInclude = mergeUniqueStringArray(allAdditional);
9927
- return merged;
9928
- }
9929
-
9930
10032
  // src/tools/utils.ts
9931
10033
  var MAX_CONTENT_LINES = 30;
9932
10034
  function truncateContent(content) {
@@ -10038,6 +10140,36 @@ function formatStatus(status) {
10038
10140
  }
10039
10141
  return lines.join("\n");
10040
10142
  }
10143
+ function formatProgressTitle(progress) {
10144
+ switch (progress.phase) {
10145
+ case "scanning":
10146
+ return "Scanning files...";
10147
+ case "parsing":
10148
+ return `Parsing: ${progress.filesProcessed}/${progress.totalFiles} files`;
10149
+ case "embedding":
10150
+ return `Embedding: ${progress.chunksProcessed}/${progress.totalChunks} chunks`;
10151
+ case "storing":
10152
+ return "Storing index...";
10153
+ case "complete":
10154
+ return "Indexing complete";
10155
+ default:
10156
+ return "Indexing...";
10157
+ }
10158
+ }
10159
+ function calculatePercentage(progress) {
10160
+ if (progress.phase === "scanning") return 0;
10161
+ if (progress.phase === "complete") return 100;
10162
+ if (progress.phase === "parsing") {
10163
+ if (progress.totalFiles === 0) return 5;
10164
+ return Math.round(5 + progress.filesProcessed / progress.totalFiles * 15);
10165
+ }
10166
+ if (progress.phase === "embedding") {
10167
+ if (progress.totalChunks === 0) return 20;
10168
+ return Math.round(20 + progress.chunksProcessed / progress.totalChunks * 70);
10169
+ }
10170
+ if (progress.phase === "storing") return 95;
10171
+ return 0;
10172
+ }
10041
10173
  function formatHealthCheck(result) {
10042
10174
  if (result.resetCorruptedIndex) {
10043
10175
  return result.warning ?? "Detected a corrupted local index and reset it. Run index_codebase to rebuild search data.";
@@ -10134,20 +10266,424 @@ function formatPrImpact(result) {
10134
10266
  return lines.join("\n");
10135
10267
  }
10136
10268
 
10137
- // src/mcp-server/shared.ts
10138
- var MAX_CONTENT_LINES2 = 30;
10139
- function truncateContent2(content) {
10140
- const lines = content.split("\n");
10141
- if (lines.length <= MAX_CONTENT_LINES2) return content;
10142
- return lines.slice(0, MAX_CONTENT_LINES2).join("\n") + `
10143
- // ... (${lines.length - MAX_CONTENT_LINES2} more lines)`;
10144
- }
10145
- var CHUNK_TYPE_ENUM = [
10146
- "function",
10147
- "class",
10148
- "method",
10149
- "interface",
10150
- "type",
10269
+ // src/tools/operations.ts
10270
+ import { existsSync as existsSync11, realpathSync, statSync as statSync3 } from "fs";
10271
+ import * as path19 from "path";
10272
+
10273
+ // src/config/merger.ts
10274
+ import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
10275
+ import * as path16 from "path";
10276
+ var PROJECT_OVERRIDE_KEYS = [
10277
+ "embeddingProvider",
10278
+ "customProvider",
10279
+ "embeddingModel",
10280
+ "reranker",
10281
+ "include",
10282
+ "exclude",
10283
+ "indexing",
10284
+ "search",
10285
+ "debug",
10286
+ "scope"
10287
+ ];
10288
+ var MERGE_ARRAY_KEYS = ["knowledgeBases", "additionalInclude"];
10289
+ function isRecord3(value) {
10290
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10291
+ }
10292
+ function isStringArray4(value) {
10293
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
10294
+ }
10295
+ function applyProjectOverride(merged, normalizedProjectConfig, globalConfig, key) {
10296
+ if (key in normalizedProjectConfig) {
10297
+ merged[key] = normalizedProjectConfig[key];
10298
+ return;
10299
+ }
10300
+ if (key in globalConfig) {
10301
+ merged[key] = globalConfig[key];
10302
+ }
10303
+ }
10304
+ function mergeUniqueStringArray(values) {
10305
+ return [...new Set(values.map((value) => String(value).trim()))];
10306
+ }
10307
+ function normalizeKnowledgeBasePath(value) {
10308
+ let normalized = path16.normalize(String(value).trim());
10309
+ const root = path16.parse(normalized).root;
10310
+ while (normalized.length > root.length && /[\\/]$/.test(normalized)) {
10311
+ normalized = normalized.slice(0, -1);
10312
+ }
10313
+ return normalized;
10314
+ }
10315
+ function mergeKnowledgeBasePaths(values) {
10316
+ return [...new Set(values.map((value) => normalizeKnowledgeBasePath(value)).filter((value) => value.length > 0))];
10317
+ }
10318
+ function validateConfigLayerShape(rawConfig, filePath) {
10319
+ if (!isRecord3(rawConfig)) {
10320
+ throw new Error(`Config file ${filePath} must contain a JSON object at the root.`);
10321
+ }
10322
+ if (rawConfig.knowledgeBases !== void 0 && !isStringArray4(rawConfig.knowledgeBases)) {
10323
+ throw new Error(`Config file ${filePath} field 'knowledgeBases' must be an array of strings.`);
10324
+ }
10325
+ if (rawConfig.additionalInclude !== void 0 && !isStringArray4(rawConfig.additionalInclude)) {
10326
+ throw new Error(`Config file ${filePath} field 'additionalInclude' must be an array of strings.`);
10327
+ }
10328
+ if (rawConfig.include !== void 0 && !isStringArray4(rawConfig.include)) {
10329
+ throw new Error(`Config file ${filePath} field 'include' must be an array of strings.`);
10330
+ }
10331
+ if (rawConfig.exclude !== void 0 && !isStringArray4(rawConfig.exclude)) {
10332
+ throw new Error(`Config file ${filePath} field 'exclude' must be an array of strings.`);
10333
+ }
10334
+ for (const section of ["customProvider", "indexing", "search", "debug", "reranker"]) {
10335
+ const value = rawConfig[section];
10336
+ if (value !== void 0 && !isRecord3(value)) {
10337
+ throw new Error(`Config file ${filePath} field '${section}' must be an object.`);
10338
+ }
10339
+ }
10340
+ return rawConfig;
10341
+ }
10342
+ function loadJsonFile(filePath) {
10343
+ if (!existsSync9(filePath)) {
10344
+ return null;
10345
+ }
10346
+ try {
10347
+ const content = readFileSync9(filePath, "utf-8");
10348
+ return validateConfigLayerShape(JSON.parse(content), filePath);
10349
+ } catch (error) {
10350
+ if (error instanceof Error && error.message.startsWith("Config file ")) {
10351
+ throw error;
10352
+ }
10353
+ const message = error instanceof Error ? error.message : String(error);
10354
+ throw new Error(`Failed to load config file ${filePath}: ${message}`);
10355
+ }
10356
+ }
10357
+ function loadConfigFile(filePath) {
10358
+ return loadJsonFile(filePath);
10359
+ }
10360
+ function materializeLocalProjectConfig(projectRoot, config, host = "opencode") {
10361
+ const localConfigPath = resolveWritableProjectConfigPath(projectRoot, host);
10362
+ mkdirSync4(path16.dirname(localConfigPath), { recursive: true });
10363
+ writeFileSync4(localConfigPath, JSON.stringify(config, null, 2), "utf-8");
10364
+ return localConfigPath;
10365
+ }
10366
+ function loadProjectConfigLayer(projectRoot, host = "opencode") {
10367
+ const projectConfigPath = resolveProjectConfigPath(projectRoot, host);
10368
+ const projectConfig = loadJsonFile(projectConfigPath);
10369
+ if (!projectConfig) {
10370
+ return {};
10371
+ }
10372
+ const normalizedConfig = { ...projectConfig };
10373
+ const projectConfigBaseDir = path16.dirname(path16.dirname(projectConfigPath));
10374
+ if (Array.isArray(normalizedConfig.knowledgeBases)) {
10375
+ normalizedConfig.knowledgeBases = resolveInheritedKnowledgeBaseEntries(
10376
+ normalizedConfig.knowledgeBases,
10377
+ projectConfigBaseDir,
10378
+ projectRoot
10379
+ );
10380
+ }
10381
+ return normalizedConfig;
10382
+ }
10383
+ function loadMergedConfig(projectRoot, host = "opencode") {
10384
+ const globalConfigPath = resolveGlobalConfigPath(host);
10385
+ const projectConfigPath = resolveProjectConfigPath(projectRoot, host);
10386
+ let globalConfig = null;
10387
+ let globalConfigError = null;
10388
+ try {
10389
+ globalConfig = loadJsonFile(globalConfigPath);
10390
+ } catch (error) {
10391
+ globalConfigError = error instanceof Error ? error : new Error(String(error));
10392
+ }
10393
+ const projectConfig = loadJsonFile(projectConfigPath);
10394
+ const normalizedProjectConfig = loadProjectConfigLayer(projectRoot, host);
10395
+ if (globalConfigError) {
10396
+ if (!projectConfig) {
10397
+ throw globalConfigError;
10398
+ }
10399
+ globalConfig = null;
10400
+ }
10401
+ if (!globalConfig && !projectConfig) {
10402
+ return {};
10403
+ }
10404
+ if (!projectConfig && globalConfig) {
10405
+ return globalConfig;
10406
+ }
10407
+ if (!globalConfig && projectConfig) {
10408
+ return normalizedProjectConfig;
10409
+ }
10410
+ if (!globalConfig || !projectConfig) {
10411
+ return globalConfig ?? normalizedProjectConfig;
10412
+ }
10413
+ const merged = { ...globalConfig };
10414
+ for (const key of PROJECT_OVERRIDE_KEYS) {
10415
+ applyProjectOverride(merged, normalizedProjectConfig, globalConfig, key);
10416
+ }
10417
+ if (projectConfig) {
10418
+ for (const key of Object.keys(projectConfig)) {
10419
+ if (PROJECT_OVERRIDE_KEYS.includes(key) || MERGE_ARRAY_KEYS.includes(key)) {
10420
+ continue;
10421
+ }
10422
+ merged[key] = normalizedProjectConfig[key];
10423
+ }
10424
+ }
10425
+ const globalKbs = globalConfig && Array.isArray(globalConfig.knowledgeBases) ? globalConfig.knowledgeBases : [];
10426
+ const projectKbs = projectConfig ? Array.isArray(normalizedProjectConfig.knowledgeBases) ? normalizedProjectConfig.knowledgeBases : [] : [];
10427
+ const allKbs = [...globalKbs, ...projectKbs];
10428
+ merged.knowledgeBases = mergeKnowledgeBasePaths(allKbs);
10429
+ const globalAdditional = globalConfig && Array.isArray(globalConfig.additionalInclude) ? globalConfig.additionalInclude : [];
10430
+ const projectAdditional = projectConfig && Array.isArray(projectConfig.additionalInclude) ? projectConfig.additionalInclude : [];
10431
+ const allAdditional = [...globalAdditional, ...projectAdditional];
10432
+ merged.additionalInclude = mergeUniqueStringArray(allAdditional);
10433
+ return merged;
10434
+ }
10435
+
10436
+ // src/tools/knowledge-base-paths.ts
10437
+ import * as path17 from "path";
10438
+ function resolveConfigPathValue(value, baseDir) {
10439
+ const trimmed = value.trim();
10440
+ if (!trimmed) {
10441
+ return trimmed;
10442
+ }
10443
+ const absolutePath = path17.isAbsolute(trimmed) ? trimmed : path17.resolve(baseDir, trimmed);
10444
+ return path17.normalize(absolutePath);
10445
+ }
10446
+
10447
+ // src/tools/config-state.ts
10448
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
10449
+ import * as path18 from "path";
10450
+ function normalizeKnowledgeBasePaths(config, projectRoot) {
10451
+ const normalized = { ...config };
10452
+ if (Array.isArray(normalized.knowledgeBases)) {
10453
+ normalized.knowledgeBases = normalized.knowledgeBases.map(
10454
+ (kb) => resolveConfigPathValue(kb, projectRoot)
10455
+ );
10456
+ }
10457
+ return normalized;
10458
+ }
10459
+ function toConfigRecord(rawConfig) {
10460
+ if (!rawConfig || typeof rawConfig !== "object") {
10461
+ return {};
10462
+ }
10463
+ return { ...rawConfig };
10464
+ }
10465
+ function loadRuntimeConfig(projectRoot, host = "opencode") {
10466
+ return normalizeKnowledgeBasePaths(toConfigRecord(loadMergedConfig(projectRoot, host)), projectRoot);
10467
+ }
10468
+
10469
+ // src/tools/operations.ts
10470
+ var indexerCache = /* @__PURE__ */ new Map();
10471
+ var configCache = /* @__PURE__ */ new Map();
10472
+ var defaultProjectRoots = /* @__PURE__ */ new Map();
10473
+ function getProjectRoot(projectRoot, host) {
10474
+ if (projectRoot) {
10475
+ return projectRoot;
10476
+ }
10477
+ const root = defaultProjectRoots.get(host);
10478
+ if (!root) {
10479
+ throw new Error("Codebase index tools not initialized. Plugin may not be loaded correctly.");
10480
+ }
10481
+ return root;
10482
+ }
10483
+ function getIndexerCacheKey(projectRoot, host) {
10484
+ return `${host}::${projectRoot}`;
10485
+ }
10486
+ function getOrCreateIndexer(projectRoot, host) {
10487
+ const key = getIndexerCacheKey(projectRoot, host);
10488
+ const cached = indexerCache.get(key);
10489
+ if (cached) {
10490
+ return cached;
10491
+ }
10492
+ const config = parseConfig(loadRuntimeConfig(projectRoot, host));
10493
+ const indexer = new Indexer(projectRoot, config, host);
10494
+ indexerCache.set(key, indexer);
10495
+ configCache.set(key, config);
10496
+ return indexer;
10497
+ }
10498
+ function initializeTools(projectRoot, config, host = "opencode") {
10499
+ defaultProjectRoots.set(host, projectRoot);
10500
+ const key = getIndexerCacheKey(projectRoot, host);
10501
+ const indexer = new Indexer(projectRoot, config, host);
10502
+ indexerCache.set(key, indexer);
10503
+ configCache.set(key, config);
10504
+ }
10505
+ function getIndexerForProject(projectRoot, host = "opencode") {
10506
+ const root = getProjectRoot(projectRoot, host);
10507
+ return getOrCreateIndexer(root, host);
10508
+ }
10509
+ function refreshIndexerForDirectory(projectRoot, host = "opencode", config = parseConfig(loadRuntimeConfig(projectRoot, host))) {
10510
+ const key = getIndexerCacheKey(projectRoot, host);
10511
+ const indexer = new Indexer(projectRoot, config, host);
10512
+ indexerCache.set(key, indexer);
10513
+ configCache.set(key, config);
10514
+ }
10515
+ function shouldForceLocalizeProjectIndex(projectRoot, host = "opencode") {
10516
+ const root = getProjectRoot(projectRoot, host);
10517
+ const localIndexPath = path19.join(root, getHostProjectIndexRelativePath(host));
10518
+ if (existsSync11(localIndexPath)) {
10519
+ return false;
10520
+ }
10521
+ const inheritedIndexPath = resolveWorktreeFallbackProjectIndexPath(root, host);
10522
+ return inheritedIndexPath !== null;
10523
+ }
10524
+ async function searchCodebase(projectRoot, host, query, options = {}) {
10525
+ const indexer = getIndexerForProject(projectRoot, host);
10526
+ return indexer.search(query, options.limit, {
10527
+ fileType: options.fileType,
10528
+ directory: options.directory,
10529
+ chunkType: options.chunkType,
10530
+ contextLines: options.contextLines,
10531
+ metadataOnly: options.metadataOnly,
10532
+ definitionIntent: options.definitionIntent
10533
+ });
10534
+ }
10535
+ async function findSimilarCode(projectRoot, host, code, options = {}) {
10536
+ const indexer = getIndexerForProject(projectRoot, host);
10537
+ return indexer.findSimilar(code, options.limit, {
10538
+ fileType: options.fileType,
10539
+ directory: options.directory,
10540
+ chunkType: options.chunkType,
10541
+ excludeFile: options.excludeFile
10542
+ });
10543
+ }
10544
+ async function implementationLookup(projectRoot, host, query, options = {}) {
10545
+ const indexer = getIndexerForProject(projectRoot, host);
10546
+ return indexer.search(query, options.limit, {
10547
+ fileType: options.fileType,
10548
+ directory: options.directory,
10549
+ definitionIntent: true
10550
+ });
10551
+ }
10552
+ async function getCallGraphData(projectRoot, host, params) {
10553
+ const indexer = getIndexerForProject(projectRoot, host);
10554
+ if (params.direction === "callees") {
10555
+ if (!params.symbolId) {
10556
+ return { direction: "callees", callees: [], callers: [] };
10557
+ }
10558
+ const callees = await indexer.getCallees(params.symbolId, params.relationshipType);
10559
+ return { direction: "callees", callees, callers: [] };
10560
+ }
10561
+ const callers = await indexer.getCallers(params.name, params.relationshipType);
10562
+ return { direction: "callers", callers, callees: [] };
10563
+ }
10564
+ async function getCallGraphPath(projectRoot, host, from, to, maxDepth) {
10565
+ const indexer = getIndexerForProject(projectRoot, host);
10566
+ return indexer.findCallPath(from, to, maxDepth);
10567
+ }
10568
+ async function runIndexCodebase(projectRoot, host, args, onProgress) {
10569
+ const root = getProjectRoot(projectRoot, host);
10570
+ const key = getIndexerCacheKey(root, host);
10571
+ const cachedConfig = configCache.get(key);
10572
+ let indexer = getIndexerForProject(root, host);
10573
+ if (args.estimateOnly) {
10574
+ return { kind: "estimate", estimate: await indexer.estimateCost() };
10575
+ }
10576
+ if (args.force) {
10577
+ if (shouldForceLocalizeProjectIndex(root, host)) {
10578
+ materializeLocalProjectConfig(root, loadProjectConfigLayer(root, host), host);
10579
+ refreshIndexerForDirectory(root, host, cachedConfig);
10580
+ indexer = getIndexerForProject(root, host);
10581
+ }
10582
+ await indexer.clearIndex();
10583
+ refreshIndexerForDirectory(root, host, cachedConfig);
10584
+ indexer = getIndexerForProject(root, host);
10585
+ }
10586
+ const stats = await indexer.index((progress) => {
10587
+ if (onProgress) {
10588
+ return onProgress(formatProgressTitle(progress), {
10589
+ phase: progress.phase,
10590
+ filesProcessed: progress.filesProcessed,
10591
+ totalFiles: progress.totalFiles,
10592
+ chunksProcessed: progress.chunksProcessed,
10593
+ totalChunks: progress.totalChunks,
10594
+ percentage: calculatePercentage(progress)
10595
+ });
10596
+ }
10597
+ return Promise.resolve();
10598
+ });
10599
+ return { kind: "stats", stats };
10600
+ }
10601
+ async function getIndexStatus(projectRoot, host) {
10602
+ const indexer = getIndexerForProject(projectRoot, host);
10603
+ return indexer.getStatus();
10604
+ }
10605
+ async function getIndexHealthCheck(projectRoot, host) {
10606
+ const indexer = getIndexerForProject(projectRoot, host);
10607
+ return indexer.healthCheck();
10608
+ }
10609
+ async function getPrImpact(projectRoot, host, params) {
10610
+ const indexer = getIndexerForProject(projectRoot, host);
10611
+ return indexer.getPrImpact({
10612
+ pr: params.pr,
10613
+ branch: params.branch,
10614
+ maxDepth: params.maxDepth,
10615
+ hubThreshold: params.hubThreshold,
10616
+ checkConflicts: params.checkConflicts,
10617
+ direction: params.direction
10618
+ });
10619
+ }
10620
+ async function getIndexMetrics(projectRoot, host) {
10621
+ const indexer = getIndexerForProject(projectRoot, host);
10622
+ const logger = indexer.getLogger();
10623
+ if (!logger.isEnabled()) {
10624
+ return {
10625
+ enabled: false,
10626
+ metricsEnabled: false,
10627
+ text: 'Debug mode is disabled. Enable it in your config:\n\n```json\n{\n "debug": {\n "enabled": true,\n "metrics": true\n }\n}\n```'
10628
+ };
10629
+ }
10630
+ if (!logger.isMetricsEnabled()) {
10631
+ return {
10632
+ enabled: true,
10633
+ metricsEnabled: false,
10634
+ text: 'Metrics collection is disabled. Enable it in your config:\n\n```json\n{\n "debug": {\n "enabled": true,\n "metrics": true\n }\n}\n```'
10635
+ };
10636
+ }
10637
+ return {
10638
+ enabled: true,
10639
+ metricsEnabled: true,
10640
+ text: logger.formatMetrics()
10641
+ };
10642
+ }
10643
+ async function getIndexLogs(projectRoot, host, args) {
10644
+ const indexer = getIndexerForProject(projectRoot, host);
10645
+ const logger = indexer.getLogger();
10646
+ if (!logger.isEnabled()) {
10647
+ return {
10648
+ kind: "disabled",
10649
+ text: 'Debug mode is disabled. Enable it in your config:\n\n```json\n{\n "debug": {\n "enabled": true\n }\n}\n```'
10650
+ };
10651
+ }
10652
+ let logs;
10653
+ if (args.category) {
10654
+ logs = logger.getLogsByCategory(args.category, args.limit);
10655
+ } else if (args.level) {
10656
+ logs = logger.getLogsByLevel(args.level, args.limit);
10657
+ } else {
10658
+ logs = logger.getLogs(args.limit);
10659
+ }
10660
+ if (logs.length === 0) {
10661
+ return {
10662
+ kind: "entries",
10663
+ text: "No logs recorded yet. Logs are captured during indexing and search operations."
10664
+ };
10665
+ }
10666
+ const text = logs.map((entry) => {
10667
+ const dataStr = entry.data ? ` ${JSON.stringify(entry.data)}` : "";
10668
+ return `[${entry.timestamp}] [${entry.level.toUpperCase()}] [${entry.category}] ${entry.message}${dataStr}`;
10669
+ }).join("\n");
10670
+ return { kind: "entries", text };
10671
+ }
10672
+
10673
+ // src/mcp-server/shared.ts
10674
+ var MAX_CONTENT_LINES2 = 30;
10675
+ function truncateContent2(content) {
10676
+ const lines = content.split("\n");
10677
+ if (lines.length <= MAX_CONTENT_LINES2) return content;
10678
+ return lines.slice(0, MAX_CONTENT_LINES2).join("\n") + `
10679
+ // ... (${lines.length - MAX_CONTENT_LINES2} more lines)`;
10680
+ }
10681
+ var CHUNK_TYPE_ENUM = [
10682
+ "function",
10683
+ "class",
10684
+ "method",
10685
+ "interface",
10686
+ "type",
10151
10687
  "enum",
10152
10688
  "struct",
10153
10689
  "impl",
@@ -10170,8 +10706,8 @@ function registerMcpTools(server, runtime) {
10170
10706
  contextLines: z2.number().optional().describe("Number of extra lines to include before/after each match (default: 0)")
10171
10707
  },
10172
10708
  async (args) => {
10173
- await runtime.ensureInitialized();
10174
- const results = await runtime.getIndexer().search(args.query, args.limit ?? 5, {
10709
+ const results = await searchCodebase(runtime.projectRoot, runtime.host, args.query, {
10710
+ limit: args.limit ?? 5,
10175
10711
  fileType: args.fileType,
10176
10712
  directory: args.directory,
10177
10713
  chunkType: args.chunkType,
@@ -10203,8 +10739,8 @@ ${formatted.join("\n\n")}` }] };
10203
10739
  chunkType: z2.enum(CHUNK_TYPE_ENUM).optional().describe("Filter by code chunk type")
10204
10740
  },
10205
10741
  async (args) => {
10206
- await runtime.ensureInitialized();
10207
- const results = await runtime.getIndexer().search(args.query, args.limit ?? 10, {
10742
+ const results = await searchCodebase(runtime.projectRoot, runtime.host, args.query, {
10743
+ limit: args.limit ?? 10,
10208
10744
  fileType: args.fileType,
10209
10745
  directory: args.directory,
10210
10746
  chunkType: args.chunkType,
@@ -10234,25 +10770,9 @@ Use Read tool to examine specific files.` }] };
10234
10770
  verbose: z2.boolean().optional().default(false).describe("Show detailed info about skipped files and parsing failures")
10235
10771
  },
10236
10772
  async (args) => {
10237
- if (args.estimateOnly) {
10238
- await runtime.ensureInitialized();
10239
- const estimate = await runtime.getIndexer().estimateCost();
10240
- return { content: [{ type: "text", text: formatCostEstimate(estimate) }] };
10241
- }
10242
- if (args.force) {
10243
- if (runtime.shouldForceLocalizeProjectIndex()) {
10244
- materializeLocalProjectConfig(runtime.projectRoot, loadProjectConfigLayer(runtime.projectRoot));
10245
- runtime.refreshIndexerFromConfig();
10246
- }
10247
- await runtime.ensureInitialized();
10248
- await runtime.getIndexer().clearIndex();
10249
- runtime.refreshIndexerFromConfig();
10250
- await runtime.ensureInitialized();
10251
- } else {
10252
- await runtime.ensureInitialized();
10253
- }
10254
- const stats = await runtime.getIndexer().index();
10255
- return { content: [{ type: "text", text: formatIndexStats(stats, args.verbose ?? false) }] };
10773
+ const result = await runIndexCodebase(runtime.projectRoot, runtime.host, args);
10774
+ const text = result.kind === "estimate" ? formatCostEstimate(result.estimate) : formatIndexStats(result.stats, args.verbose ?? false);
10775
+ return { content: [{ type: "text", text }] };
10256
10776
  }
10257
10777
  );
10258
10778
  server.tool(
@@ -10260,8 +10780,7 @@ Use Read tool to examine specific files.` }] };
10260
10780
  "Check the status of the codebase index. Shows whether the codebase is indexed, how many chunks are stored, and the embedding provider being used.",
10261
10781
  {},
10262
10782
  async () => {
10263
- await runtime.ensureInitialized();
10264
- const status = await runtime.getIndexer().getStatus();
10783
+ const status = await getIndexStatus(runtime.projectRoot, runtime.host);
10265
10784
  return { content: [{ type: "text", text: formatStatus(status) }] };
10266
10785
  }
10267
10786
  );
@@ -10270,8 +10789,7 @@ Use Read tool to examine specific files.` }] };
10270
10789
  "Check index health and remove stale entries from deleted files. Run this to clean up the index after files have been deleted.",
10271
10790
  {},
10272
10791
  async () => {
10273
- await runtime.ensureInitialized();
10274
- const result = await runtime.getIndexer().healthCheck();
10792
+ const result = await getIndexHealthCheck(runtime.projectRoot, runtime.host);
10275
10793
  return { content: [{ type: "text", text: formatHealthCheck(result) }] };
10276
10794
  }
10277
10795
  );
@@ -10280,15 +10798,8 @@ Use Read tool to examine specific files.` }] };
10280
10798
  "Get metrics and performance statistics for the codebase index. Requires debug.enabled=true and debug.metrics=true in config.",
10281
10799
  {},
10282
10800
  async () => {
10283
- await runtime.ensureInitialized();
10284
- const logger = runtime.getIndexer().getLogger();
10285
- if (!logger.isEnabled()) {
10286
- return { content: [{ type: "text", text: 'Debug mode is disabled. Enable it in your config:\n\n```json\n{\n "debug": {\n "enabled": true,\n "metrics": true\n }\n}\n```' }] };
10287
- }
10288
- if (!logger.isMetricsEnabled()) {
10289
- return { content: [{ type: "text", text: 'Metrics collection is disabled. Enable it in your config:\n\n```json\n{\n "debug": {\n "enabled": true,\n "metrics": true\n }\n}\n```' }] };
10290
- }
10291
- return { content: [{ type: "text", text: logger.formatMetrics() }] };
10801
+ const result = await getIndexMetrics(runtime.projectRoot, runtime.host);
10802
+ return { content: [{ type: "text", text: result.text }] };
10292
10803
  }
10293
10804
  );
10294
10805
  server.tool(
@@ -10300,27 +10811,8 @@ Use Read tool to examine specific files.` }] };
10300
10811
  level: z2.enum(["error", "warn", "info", "debug"]).optional().describe("Filter by minimum log level")
10301
10812
  },
10302
10813
  async (args) => {
10303
- await runtime.ensureInitialized();
10304
- const logger = runtime.getIndexer().getLogger();
10305
- if (!logger.isEnabled()) {
10306
- return { content: [{ type: "text", text: 'Debug mode is disabled. Enable it in your config:\n\n```json\n{\n "debug": {\n "enabled": true\n }\n}\n```' }] };
10307
- }
10308
- let logs;
10309
- if (args.category) {
10310
- logs = logger.getLogsByCategory(args.category, args.limit);
10311
- } else if (args.level) {
10312
- logs = logger.getLogsByLevel(args.level, args.limit);
10313
- } else {
10314
- logs = logger.getLogs(args.limit);
10315
- }
10316
- if (logs.length === 0) {
10317
- return { content: [{ type: "text", text: "No logs recorded yet. Logs are captured during indexing and search operations." }] };
10318
- }
10319
- const text = logs.map((l) => {
10320
- const dataStr = l.data ? ` ${JSON.stringify(l.data)}` : "";
10321
- return `[${l.timestamp}] [${l.level.toUpperCase()}] [${l.category}] ${l.message}${dataStr}`;
10322
- }).join("\n");
10323
- return { content: [{ type: "text", text }] };
10814
+ const result = await getIndexLogs(runtime.projectRoot, runtime.host, args);
10815
+ return { content: [{ type: "text", text: result.text }] };
10324
10816
  }
10325
10817
  );
10326
10818
  server.tool(
@@ -10335,8 +10827,8 @@ Use Read tool to examine specific files.` }] };
10335
10827
  excludeFile: z2.string().optional().describe("Exclude results from this file path")
10336
10828
  },
10337
10829
  async (args) => {
10338
- await runtime.ensureInitialized();
10339
- const results = await runtime.getIndexer().findSimilar(args.code, args.limit ?? 10, {
10830
+ const results = await findSimilarCode(runtime.projectRoot, runtime.host, args.code, {
10831
+ limit: args.limit ?? 10,
10340
10832
  fileType: args.fileType,
10341
10833
  directory: args.directory,
10342
10834
  chunkType: args.chunkType,
@@ -10367,11 +10859,10 @@ ${formatted.join("\n\n")}` }] };
10367
10859
  directory: z2.string().optional().describe("Filter by directory path (e.g., 'src/utils')")
10368
10860
  },
10369
10861
  async (args) => {
10370
- await runtime.ensureInitialized();
10371
- const results = await runtime.getIndexer().search(args.query, args.limit ?? 5, {
10862
+ const results = await implementationLookup(runtime.projectRoot, runtime.host, args.query, {
10863
+ limit: args.limit ?? 5,
10372
10864
  fileType: args.fileType,
10373
- directory: args.directory,
10374
- definitionIntent: true
10865
+ directory: args.directory
10375
10866
  });
10376
10867
  return { content: [{ type: "text", text: formatDefinitionLookup(results, args.query) }] };
10377
10868
  }
@@ -10386,13 +10877,11 @@ ${formatted.join("\n\n")}` }] };
10386
10877
  relationshipType: z2.enum(["Call", "MethodCall", "Constructor", "Import", "Inherits", "Implements"]).optional().describe("Filter by relationship type. Omit to show all.")
10387
10878
  },
10388
10879
  async (args) => {
10389
- await runtime.ensureInitialized();
10390
- const indexer = runtime.getIndexer();
10391
10880
  if (args.direction === "callees") {
10392
10881
  if (!args.symbolId) {
10393
10882
  return { content: [{ type: "text", text: "Error: 'symbolId' is required when direction is 'callees'." }] };
10394
10883
  }
10395
- const callees = await indexer.getCallees(args.symbolId, args.relationshipType);
10884
+ const { callees } = await getCallGraphData(runtime.projectRoot, runtime.host, args);
10396
10885
  if (callees.length === 0) {
10397
10886
  return { content: [{ type: "text", text: `No callees found for symbol ${args.symbolId}${args.relationshipType ? ` with type ${args.relationshipType}` : ""}.` }] };
10398
10887
  }
@@ -10404,7 +10893,7 @@ ${formatted.join("\n\n")}` }] };
10404
10893
 
10405
10894
  ${formatted2.join("\n")}` }] };
10406
10895
  }
10407
- const callers = await indexer.getCallers(args.name, args.relationshipType);
10896
+ const { callers } = await getCallGraphData(runtime.projectRoot, runtime.host, args);
10408
10897
  if (callers.length === 0) {
10409
10898
  return { content: [{ type: "text", text: `No callers found for "${args.name}"${args.relationshipType ? ` with type ${args.relationshipType}` : ""}.` }] };
10410
10899
  }
@@ -10426,18 +10915,16 @@ ${formatted.join("\n")}` }] };
10426
10915
  maxDepth: z2.number().optional().default(10).describe("Maximum traversal depth (default: 10)")
10427
10916
  },
10428
10917
  async (args) => {
10429
- await runtime.ensureInitialized();
10430
- const indexer = runtime.getIndexer();
10431
- const path19 = await indexer.findCallPath(args.from, args.to, args.maxDepth);
10432
- if (path19.length === 0) {
10918
+ const path25 = await getCallGraphPath(runtime.projectRoot, runtime.host, args.from, args.to, args.maxDepth);
10919
+ if (path25.length === 0) {
10433
10920
  return { content: [{ type: "text", text: `No path found between "${args.from}" and "${args.to}". They may be in disconnected components, or the call graph index needs updating.` }] };
10434
10921
  }
10435
- const formatted = path19.map((hop, i) => {
10922
+ const formatted = path25.map((hop, i) => {
10436
10923
  const prefix = i === 0 ? "[start]" : `--${hop.callType}-->`;
10437
10924
  const location = hop.filePath ? ` (${hop.filePath}:${hop.line})` : "";
10438
10925
  return `${prefix} ${hop.symbolName}${location}`;
10439
10926
  });
10440
- return { content: [{ type: "text", text: `Path (${path19.length} hops):
10927
+ return { content: [{ type: "text", text: `Path (${path25.length} hops):
10441
10928
  ${formatted.join("\n")}` }] };
10442
10929
  }
10443
10930
  );
@@ -10453,10 +10940,8 @@ ${formatted.join("\n")}` }] };
10453
10940
  direction: z2.enum(["callers", "callees", "both"]).optional().default("both").describe("Call-graph traversal direction: 'callers' for upstream, 'callees' for downstream, 'both' for union (default: both)")
10454
10941
  },
10455
10942
  async (args) => {
10456
- await runtime.ensureInitialized();
10457
- const indexer = runtime.getIndexer();
10458
10943
  try {
10459
- const result = await indexer.getPrImpact({
10944
+ const result = await getPrImpact(runtime.projectRoot, runtime.host, {
10460
10945
  pr: args.pr,
10461
10946
  branch: args.branch,
10462
10947
  maxDepth: args.maxDepth,
@@ -10474,80 +10959,2922 @@ ${formatted.join("\n")}` }] };
10474
10959
  }
10475
10960
 
10476
10961
  // src/mcp-server.ts
10477
- function createMcpServer(projectRoot, config) {
10962
+ function getPackageVersion() {
10963
+ const raw = JSON.parse(readFileSync10(new URL("../package.json", import.meta.url), "utf-8"));
10964
+ if (raw && typeof raw === "object" && "version" in raw && typeof raw.version === "string") {
10965
+ return raw.version;
10966
+ }
10967
+ return "0.0.0";
10968
+ }
10969
+ function createMcpServer(projectRoot, config, host = "opencode") {
10478
10970
  const server = new McpServer({
10479
10971
  name: "opencode-codebase-index",
10480
- version: "0.5.1"
10972
+ version: getPackageVersion()
10481
10973
  });
10482
- let indexer = new Indexer(projectRoot, config);
10483
- let initialized = false;
10484
- function refreshIndexerFromConfig() {
10485
- indexer = new Indexer(projectRoot, config);
10486
- initialized = false;
10487
- }
10488
- function shouldForceLocalizeProjectIndex() {
10489
- if (config.scope !== "project") {
10490
- return false;
10491
- }
10492
- const localIndexPath = path17.join(projectRoot, ".opencode", "index");
10493
- const mainRepoRoot = resolveWorktreeMainRepoRoot(projectRoot);
10494
- if (!mainRepoRoot) {
10495
- return false;
10496
- }
10497
- const inheritedIndexPath = path17.join(mainRepoRoot, ".opencode", "index");
10498
- return !existsSync10(localIndexPath) && existsSync10(inheritedIndexPath);
10499
- }
10500
- async function ensureInitialized() {
10501
- if (!initialized) {
10502
- await indexer.initialize();
10503
- initialized = true;
10504
- }
10505
- }
10974
+ initializeTools(projectRoot, config, host);
10506
10975
  registerMcpTools(server, {
10507
10976
  projectRoot,
10508
- ensureInitialized,
10509
- getIndexer: () => indexer,
10510
- refreshIndexerFromConfig,
10511
- shouldForceLocalizeProjectIndex
10977
+ host
10512
10978
  });
10513
10979
  registerMcpPrompts(server);
10514
10980
  return server;
10515
10981
  }
10516
10982
 
10517
- // src/cli.ts
10518
- function parseArgs(argv) {
10519
- let project = process.cwd();
10520
- let config;
10521
- for (let i = 2; i < argv.length; i++) {
10522
- if (argv[i] === "--project" && argv[i + 1]) {
10523
- project = path18.resolve(argv[++i]);
10524
- } else if (argv[i] === "--config" && argv[i + 1]) {
10525
- config = path18.resolve(argv[++i]);
10526
- }
10527
- }
10528
- return { project, config };
10529
- }
10530
- async function main() {
10531
- if (process.argv[2] === "eval") {
10532
- const exitCode = await handleEvalCommand(process.argv.slice(3), process.cwd());
10533
- process.exit(exitCode);
10534
- }
10535
- const args = parseArgs(process.argv);
10536
- const rawConfig = loadMergedConfig(args.project);
10537
- const config = parseConfig(rawConfig);
10538
- const server = createMcpServer(args.project, config);
10539
- const transport = new StdioServerTransport();
10540
- await server.connect(transport);
10541
- const shutdown = () => {
10542
- server.close().catch(() => {
10983
+ // src/utils/auto-index.ts
10984
+ function getErrorMessage3(error) {
10985
+ return error instanceof Error ? error.message : String(error);
10986
+ }
10987
+ function startAutoIndex(indexer, projectRoot) {
10988
+ indexer.initialize().then(() => {
10989
+ indexer.index().catch((error) => {
10990
+ console.error(`[codebase-index] Auto-index failed for "${projectRoot}": ${getErrorMessage3(error)}`);
10991
+ });
10992
+ }).catch((error) => {
10993
+ console.error(`[codebase-index] Auto-index initialization failed for "${projectRoot}": ${getErrorMessage3(error)}`);
10994
+ });
10995
+ }
10996
+
10997
+ // node_modules/chokidar/index.js
10998
+ import { EventEmitter as EventEmitter2 } from "events";
10999
+ import { stat as statcb, Stats } from "fs";
11000
+ import { readdir as readdir2, stat as stat3 } from "fs/promises";
11001
+ import * as sp2 from "path";
11002
+
11003
+ // node_modules/readdirp/index.js
11004
+ import { lstat, readdir, realpath, stat } from "fs/promises";
11005
+ import { join as pjoin, relative as prelative, resolve as presolve, sep as psep } from "path";
11006
+ import { Readable } from "stream";
11007
+ var EntryTypes = {
11008
+ FILE_TYPE: "files",
11009
+ DIR_TYPE: "directories",
11010
+ FILE_DIR_TYPE: "files_directories",
11011
+ EVERYTHING_TYPE: "all"
11012
+ };
11013
+ var defaultOptions = {
11014
+ root: ".",
11015
+ fileFilter: (_entryInfo) => true,
11016
+ directoryFilter: (_entryInfo) => true,
11017
+ type: EntryTypes.FILE_TYPE,
11018
+ lstat: false,
11019
+ depth: 2147483648,
11020
+ alwaysStat: false,
11021
+ highWaterMark: 4096
11022
+ };
11023
+ Object.freeze(defaultOptions);
11024
+ var RECURSIVE_ERROR_CODE = "READDIRP_RECURSIVE_ERROR";
11025
+ var NORMAL_FLOW_ERRORS = /* @__PURE__ */ new Set(["ENOENT", "EPERM", "EACCES", "ELOOP", RECURSIVE_ERROR_CODE]);
11026
+ var ALL_TYPES = [
11027
+ EntryTypes.DIR_TYPE,
11028
+ EntryTypes.EVERYTHING_TYPE,
11029
+ EntryTypes.FILE_DIR_TYPE,
11030
+ EntryTypes.FILE_TYPE
11031
+ ];
11032
+ var DIR_TYPES = /* @__PURE__ */ new Set([
11033
+ EntryTypes.DIR_TYPE,
11034
+ EntryTypes.EVERYTHING_TYPE,
11035
+ EntryTypes.FILE_DIR_TYPE
11036
+ ]);
11037
+ var FILE_TYPES = /* @__PURE__ */ new Set([
11038
+ EntryTypes.EVERYTHING_TYPE,
11039
+ EntryTypes.FILE_DIR_TYPE,
11040
+ EntryTypes.FILE_TYPE
11041
+ ]);
11042
+ var isNormalFlowError = (error) => NORMAL_FLOW_ERRORS.has(error.code);
11043
+ var wantBigintFsStats = process.platform === "win32";
11044
+ var emptyFn = (_entryInfo) => true;
11045
+ var normalizeFilter = (filter) => {
11046
+ if (filter === void 0)
11047
+ return emptyFn;
11048
+ if (typeof filter === "function")
11049
+ return filter;
11050
+ if (typeof filter === "string") {
11051
+ const fl = filter.trim();
11052
+ return (entry) => entry.basename === fl;
11053
+ }
11054
+ if (Array.isArray(filter)) {
11055
+ const trItems = filter.map((item) => item.trim());
11056
+ return (entry) => trItems.some((f) => entry.basename === f);
11057
+ }
11058
+ return emptyFn;
11059
+ };
11060
+ var ReaddirpStream = class extends Readable {
11061
+ parents;
11062
+ reading;
11063
+ parent;
11064
+ _stat;
11065
+ _maxDepth;
11066
+ _wantsDir;
11067
+ _wantsFile;
11068
+ _wantsEverything;
11069
+ _root;
11070
+ _isDirent;
11071
+ _statsProp;
11072
+ _rdOptions;
11073
+ _fileFilter;
11074
+ _directoryFilter;
11075
+ constructor(options = {}) {
11076
+ super({
11077
+ objectMode: true,
11078
+ autoDestroy: true,
11079
+ highWaterMark: options.highWaterMark
11080
+ });
11081
+ const opts = { ...defaultOptions, ...options };
11082
+ const { root, type } = opts;
11083
+ this._fileFilter = normalizeFilter(opts.fileFilter);
11084
+ this._directoryFilter = normalizeFilter(opts.directoryFilter);
11085
+ const statMethod = opts.lstat ? lstat : stat;
11086
+ if (wantBigintFsStats) {
11087
+ this._stat = (path25) => statMethod(path25, { bigint: true });
11088
+ } else {
11089
+ this._stat = statMethod;
11090
+ }
11091
+ this._maxDepth = opts.depth != null && Number.isSafeInteger(opts.depth) ? opts.depth : defaultOptions.depth;
11092
+ this._wantsDir = type ? DIR_TYPES.has(type) : false;
11093
+ this._wantsFile = type ? FILE_TYPES.has(type) : false;
11094
+ this._wantsEverything = type === EntryTypes.EVERYTHING_TYPE;
11095
+ this._root = presolve(root);
11096
+ this._isDirent = !opts.alwaysStat;
11097
+ this._statsProp = this._isDirent ? "dirent" : "stats";
11098
+ this._rdOptions = { encoding: "utf8", withFileTypes: this._isDirent };
11099
+ this.parents = [this._exploreDir(root, 1)];
11100
+ this.reading = false;
11101
+ this.parent = void 0;
11102
+ }
11103
+ async _read(batch) {
11104
+ if (this.reading)
11105
+ return;
11106
+ this.reading = true;
11107
+ try {
11108
+ while (!this.destroyed && batch > 0) {
11109
+ const par = this.parent;
11110
+ const fil = par && par.files;
11111
+ if (fil && fil.length > 0) {
11112
+ const { path: path25, depth } = par;
11113
+ const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path25));
11114
+ const awaited = await Promise.all(slice);
11115
+ for (const entry of awaited) {
11116
+ if (!entry)
11117
+ continue;
11118
+ if (this.destroyed)
11119
+ return;
11120
+ const entryType = await this._getEntryType(entry);
11121
+ if (entryType === "directory" && this._directoryFilter(entry)) {
11122
+ if (depth <= this._maxDepth) {
11123
+ this.parents.push(this._exploreDir(entry.fullPath, depth + 1));
11124
+ }
11125
+ if (this._wantsDir) {
11126
+ this.push(entry);
11127
+ batch--;
11128
+ }
11129
+ } else if ((entryType === "file" || this._includeAsFile(entry)) && this._fileFilter(entry)) {
11130
+ if (this._wantsFile) {
11131
+ this.push(entry);
11132
+ batch--;
11133
+ }
11134
+ }
11135
+ }
11136
+ } else {
11137
+ const parent = this.parents.pop();
11138
+ if (!parent) {
11139
+ this.push(null);
11140
+ break;
11141
+ }
11142
+ this.parent = await parent;
11143
+ if (this.destroyed)
11144
+ return;
11145
+ }
11146
+ }
11147
+ } catch (error) {
11148
+ this.destroy(error);
11149
+ } finally {
11150
+ this.reading = false;
11151
+ }
11152
+ }
11153
+ async _exploreDir(path25, depth) {
11154
+ let files;
11155
+ try {
11156
+ files = await readdir(path25, this._rdOptions);
11157
+ } catch (error) {
11158
+ this._onError(error);
11159
+ }
11160
+ return { files, depth, path: path25 };
11161
+ }
11162
+ async _formatEntry(dirent, path25) {
11163
+ let entry;
11164
+ const basename5 = this._isDirent ? dirent.name : dirent;
11165
+ try {
11166
+ const fullPath = presolve(pjoin(path25, basename5));
11167
+ entry = { path: prelative(this._root, fullPath), fullPath, basename: basename5 };
11168
+ entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
11169
+ } catch (err) {
11170
+ this._onError(err);
11171
+ return;
11172
+ }
11173
+ return entry;
11174
+ }
11175
+ _onError(err) {
11176
+ if (isNormalFlowError(err) && !this.destroyed) {
11177
+ this.emit("warn", err);
11178
+ } else {
11179
+ this.destroy(err);
11180
+ }
11181
+ }
11182
+ async _getEntryType(entry) {
11183
+ if (!entry && this._statsProp in entry) {
11184
+ return "";
11185
+ }
11186
+ const stats = entry[this._statsProp];
11187
+ if (stats.isFile())
11188
+ return "file";
11189
+ if (stats.isDirectory())
11190
+ return "directory";
11191
+ if (stats && stats.isSymbolicLink()) {
11192
+ const full = entry.fullPath;
11193
+ try {
11194
+ const entryRealPath = await realpath(full);
11195
+ const entryRealPathStats = await lstat(entryRealPath);
11196
+ if (entryRealPathStats.isFile()) {
11197
+ return "file";
11198
+ }
11199
+ if (entryRealPathStats.isDirectory()) {
11200
+ const len = entryRealPath.length;
11201
+ if (full.startsWith(entryRealPath) && full.substr(len, 1) === psep) {
11202
+ const recursiveError = new Error(`Circular symlink detected: "${full}" points to "${entryRealPath}"`);
11203
+ recursiveError.code = RECURSIVE_ERROR_CODE;
11204
+ return this._onError(recursiveError);
11205
+ }
11206
+ return "directory";
11207
+ }
11208
+ } catch (error) {
11209
+ this._onError(error);
11210
+ return "";
11211
+ }
11212
+ }
11213
+ }
11214
+ _includeAsFile(entry) {
11215
+ const stats = entry && entry[this._statsProp];
11216
+ return stats && this._wantsEverything && !stats.isDirectory();
11217
+ }
11218
+ };
11219
+ function readdirp(root, options = {}) {
11220
+ let type = options.entryType || options.type;
11221
+ if (type === "both")
11222
+ type = EntryTypes.FILE_DIR_TYPE;
11223
+ if (type)
11224
+ options.type = type;
11225
+ if (!root) {
11226
+ throw new Error("readdirp: root argument is required. Usage: readdirp(root, options)");
11227
+ } else if (typeof root !== "string") {
11228
+ throw new TypeError("readdirp: root argument must be a string. Usage: readdirp(root, options)");
11229
+ } else if (type && !ALL_TYPES.includes(type)) {
11230
+ throw new Error(`readdirp: Invalid type passed. Use one of ${ALL_TYPES.join(", ")}`);
11231
+ }
11232
+ options.root = root;
11233
+ return new ReaddirpStream(options);
11234
+ }
11235
+
11236
+ // node_modules/chokidar/handler.js
11237
+ import { watch as fs_watch, unwatchFile, watchFile } from "fs";
11238
+ import { realpath as fsrealpath, lstat as lstat2, open, stat as stat2 } from "fs/promises";
11239
+ import { type as osType } from "os";
11240
+ import * as sp from "path";
11241
+ var STR_DATA = "data";
11242
+ var STR_END = "end";
11243
+ var STR_CLOSE = "close";
11244
+ var EMPTY_FN = () => {
11245
+ };
11246
+ var pl = process.platform;
11247
+ var isWindows = pl === "win32";
11248
+ var isMacos = pl === "darwin";
11249
+ var isLinux = pl === "linux";
11250
+ var isFreeBSD = pl === "freebsd";
11251
+ var isIBMi = osType() === "OS400";
11252
+ var EVENTS = {
11253
+ ALL: "all",
11254
+ READY: "ready",
11255
+ ADD: "add",
11256
+ CHANGE: "change",
11257
+ ADD_DIR: "addDir",
11258
+ UNLINK: "unlink",
11259
+ UNLINK_DIR: "unlinkDir",
11260
+ RAW: "raw",
11261
+ ERROR: "error"
11262
+ };
11263
+ var EV = EVENTS;
11264
+ var THROTTLE_MODE_WATCH = "watch";
11265
+ var statMethods = { lstat: lstat2, stat: stat2 };
11266
+ var KEY_LISTENERS = "listeners";
11267
+ var KEY_ERR = "errHandlers";
11268
+ var KEY_RAW = "rawEmitters";
11269
+ var HANDLER_KEYS = [KEY_LISTENERS, KEY_ERR, KEY_RAW];
11270
+ var binaryExtensions = /* @__PURE__ */ new Set([
11271
+ "3dm",
11272
+ "3ds",
11273
+ "3g2",
11274
+ "3gp",
11275
+ "7z",
11276
+ "a",
11277
+ "aac",
11278
+ "adp",
11279
+ "afdesign",
11280
+ "afphoto",
11281
+ "afpub",
11282
+ "ai",
11283
+ "aif",
11284
+ "aiff",
11285
+ "alz",
11286
+ "ape",
11287
+ "apk",
11288
+ "appimage",
11289
+ "ar",
11290
+ "arj",
11291
+ "asf",
11292
+ "au",
11293
+ "avi",
11294
+ "bak",
11295
+ "baml",
11296
+ "bh",
11297
+ "bin",
11298
+ "bk",
11299
+ "bmp",
11300
+ "btif",
11301
+ "bz2",
11302
+ "bzip2",
11303
+ "cab",
11304
+ "caf",
11305
+ "cgm",
11306
+ "class",
11307
+ "cmx",
11308
+ "cpio",
11309
+ "cr2",
11310
+ "cur",
11311
+ "dat",
11312
+ "dcm",
11313
+ "deb",
11314
+ "dex",
11315
+ "djvu",
11316
+ "dll",
11317
+ "dmg",
11318
+ "dng",
11319
+ "doc",
11320
+ "docm",
11321
+ "docx",
11322
+ "dot",
11323
+ "dotm",
11324
+ "dra",
11325
+ "DS_Store",
11326
+ "dsk",
11327
+ "dts",
11328
+ "dtshd",
11329
+ "dvb",
11330
+ "dwg",
11331
+ "dxf",
11332
+ "ecelp4800",
11333
+ "ecelp7470",
11334
+ "ecelp9600",
11335
+ "egg",
11336
+ "eol",
11337
+ "eot",
11338
+ "epub",
11339
+ "exe",
11340
+ "f4v",
11341
+ "fbs",
11342
+ "fh",
11343
+ "fla",
11344
+ "flac",
11345
+ "flatpak",
11346
+ "fli",
11347
+ "flv",
11348
+ "fpx",
11349
+ "fst",
11350
+ "fvt",
11351
+ "g3",
11352
+ "gh",
11353
+ "gif",
11354
+ "graffle",
11355
+ "gz",
11356
+ "gzip",
11357
+ "h261",
11358
+ "h263",
11359
+ "h264",
11360
+ "icns",
11361
+ "ico",
11362
+ "ief",
11363
+ "img",
11364
+ "ipa",
11365
+ "iso",
11366
+ "jar",
11367
+ "jpeg",
11368
+ "jpg",
11369
+ "jpgv",
11370
+ "jpm",
11371
+ "jxr",
11372
+ "key",
11373
+ "ktx",
11374
+ "lha",
11375
+ "lib",
11376
+ "lvp",
11377
+ "lz",
11378
+ "lzh",
11379
+ "lzma",
11380
+ "lzo",
11381
+ "m3u",
11382
+ "m4a",
11383
+ "m4v",
11384
+ "mar",
11385
+ "mdi",
11386
+ "mht",
11387
+ "mid",
11388
+ "midi",
11389
+ "mj2",
11390
+ "mka",
11391
+ "mkv",
11392
+ "mmr",
11393
+ "mng",
11394
+ "mobi",
11395
+ "mov",
11396
+ "movie",
11397
+ "mp3",
11398
+ "mp4",
11399
+ "mp4a",
11400
+ "mpeg",
11401
+ "mpg",
11402
+ "mpga",
11403
+ "mxu",
11404
+ "nef",
11405
+ "npx",
11406
+ "numbers",
11407
+ "nupkg",
11408
+ "o",
11409
+ "odp",
11410
+ "ods",
11411
+ "odt",
11412
+ "oga",
11413
+ "ogg",
11414
+ "ogv",
11415
+ "otf",
11416
+ "ott",
11417
+ "pages",
11418
+ "pbm",
11419
+ "pcx",
11420
+ "pdb",
11421
+ "pdf",
11422
+ "pea",
11423
+ "pgm",
11424
+ "pic",
11425
+ "png",
11426
+ "pnm",
11427
+ "pot",
11428
+ "potm",
11429
+ "potx",
11430
+ "ppa",
11431
+ "ppam",
11432
+ "ppm",
11433
+ "pps",
11434
+ "ppsm",
11435
+ "ppsx",
11436
+ "ppt",
11437
+ "pptm",
11438
+ "pptx",
11439
+ "psd",
11440
+ "pya",
11441
+ "pyc",
11442
+ "pyo",
11443
+ "pyv",
11444
+ "qt",
11445
+ "rar",
11446
+ "ras",
11447
+ "raw",
11448
+ "resources",
11449
+ "rgb",
11450
+ "rip",
11451
+ "rlc",
11452
+ "rmf",
11453
+ "rmvb",
11454
+ "rpm",
11455
+ "rtf",
11456
+ "rz",
11457
+ "s3m",
11458
+ "s7z",
11459
+ "scpt",
11460
+ "sgi",
11461
+ "shar",
11462
+ "snap",
11463
+ "sil",
11464
+ "sketch",
11465
+ "slk",
11466
+ "smv",
11467
+ "snk",
11468
+ "so",
11469
+ "stl",
11470
+ "suo",
11471
+ "sub",
11472
+ "swf",
11473
+ "tar",
11474
+ "tbz",
11475
+ "tbz2",
11476
+ "tga",
11477
+ "tgz",
11478
+ "thmx",
11479
+ "tif",
11480
+ "tiff",
11481
+ "tlz",
11482
+ "ttc",
11483
+ "ttf",
11484
+ "txz",
11485
+ "udf",
11486
+ "uvh",
11487
+ "uvi",
11488
+ "uvm",
11489
+ "uvp",
11490
+ "uvs",
11491
+ "uvu",
11492
+ "viv",
11493
+ "vob",
11494
+ "war",
11495
+ "wav",
11496
+ "wax",
11497
+ "wbmp",
11498
+ "wdp",
11499
+ "weba",
11500
+ "webm",
11501
+ "webp",
11502
+ "whl",
11503
+ "wim",
11504
+ "wm",
11505
+ "wma",
11506
+ "wmv",
11507
+ "wmx",
11508
+ "woff",
11509
+ "woff2",
11510
+ "wrm",
11511
+ "wvx",
11512
+ "xbm",
11513
+ "xif",
11514
+ "xla",
11515
+ "xlam",
11516
+ "xls",
11517
+ "xlsb",
11518
+ "xlsm",
11519
+ "xlsx",
11520
+ "xlt",
11521
+ "xltm",
11522
+ "xltx",
11523
+ "xm",
11524
+ "xmind",
11525
+ "xpi",
11526
+ "xpm",
11527
+ "xwd",
11528
+ "xz",
11529
+ "z",
11530
+ "zip",
11531
+ "zipx"
11532
+ ]);
11533
+ var isBinaryPath = (filePath) => binaryExtensions.has(sp.extname(filePath).slice(1).toLowerCase());
11534
+ var foreach = (val, fn) => {
11535
+ if (val instanceof Set) {
11536
+ val.forEach(fn);
11537
+ } else {
11538
+ fn(val);
11539
+ }
11540
+ };
11541
+ var addAndConvert = (main2, prop, item) => {
11542
+ let container = main2[prop];
11543
+ if (!(container instanceof Set)) {
11544
+ main2[prop] = container = /* @__PURE__ */ new Set([container]);
11545
+ }
11546
+ container.add(item);
11547
+ };
11548
+ var clearItem = (cont) => (key) => {
11549
+ const set = cont[key];
11550
+ if (set instanceof Set) {
11551
+ set.clear();
11552
+ } else {
11553
+ delete cont[key];
11554
+ }
11555
+ };
11556
+ var delFromSet = (main2, prop, item) => {
11557
+ const container = main2[prop];
11558
+ if (container instanceof Set) {
11559
+ container.delete(item);
11560
+ } else if (container === item) {
11561
+ delete main2[prop];
11562
+ }
11563
+ };
11564
+ var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
11565
+ var FsWatchInstances = /* @__PURE__ */ new Map();
11566
+ function createFsWatchInstance(path25, options, listener, errHandler, emitRaw) {
11567
+ const handleEvent = (rawEvent, evPath) => {
11568
+ listener(path25);
11569
+ emitRaw(rawEvent, evPath, { watchedPath: path25 });
11570
+ if (evPath && path25 !== evPath) {
11571
+ fsWatchBroadcast(sp.resolve(path25, evPath), KEY_LISTENERS, sp.join(path25, evPath));
11572
+ }
11573
+ };
11574
+ try {
11575
+ return fs_watch(path25, {
11576
+ persistent: options.persistent
11577
+ }, handleEvent);
11578
+ } catch (error) {
11579
+ errHandler(error);
11580
+ return void 0;
11581
+ }
11582
+ }
11583
+ var fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
11584
+ const cont = FsWatchInstances.get(fullPath);
11585
+ if (!cont)
11586
+ return;
11587
+ foreach(cont[listenerType], (listener) => {
11588
+ listener(val1, val2, val3);
11589
+ });
11590
+ };
11591
+ var setFsWatchListener = (path25, fullPath, options, handlers) => {
11592
+ const { listener, errHandler, rawEmitter } = handlers;
11593
+ let cont = FsWatchInstances.get(fullPath);
11594
+ let watcher;
11595
+ if (!options.persistent) {
11596
+ watcher = createFsWatchInstance(path25, options, listener, errHandler, rawEmitter);
11597
+ if (!watcher)
11598
+ return;
11599
+ return watcher.close.bind(watcher);
11600
+ }
11601
+ if (cont) {
11602
+ addAndConvert(cont, KEY_LISTENERS, listener);
11603
+ addAndConvert(cont, KEY_ERR, errHandler);
11604
+ addAndConvert(cont, KEY_RAW, rawEmitter);
11605
+ } else {
11606
+ watcher = createFsWatchInstance(
11607
+ path25,
11608
+ options,
11609
+ fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS),
11610
+ errHandler,
11611
+ // no need to use broadcast here
11612
+ fsWatchBroadcast.bind(null, fullPath, KEY_RAW)
11613
+ );
11614
+ if (!watcher)
11615
+ return;
11616
+ watcher.on(EV.ERROR, async (error) => {
11617
+ const broadcastErr = fsWatchBroadcast.bind(null, fullPath, KEY_ERR);
11618
+ if (cont)
11619
+ cont.watcherUnusable = true;
11620
+ if (isWindows && error.code === "EPERM") {
11621
+ try {
11622
+ const fd = await open(path25, "r");
11623
+ await fd.close();
11624
+ broadcastErr(error);
11625
+ } catch (err) {
11626
+ }
11627
+ } else {
11628
+ broadcastErr(error);
11629
+ }
11630
+ });
11631
+ cont = {
11632
+ listeners: listener,
11633
+ errHandlers: errHandler,
11634
+ rawEmitters: rawEmitter,
11635
+ watcher
11636
+ };
11637
+ FsWatchInstances.set(fullPath, cont);
11638
+ }
11639
+ return () => {
11640
+ delFromSet(cont, KEY_LISTENERS, listener);
11641
+ delFromSet(cont, KEY_ERR, errHandler);
11642
+ delFromSet(cont, KEY_RAW, rawEmitter);
11643
+ if (isEmptySet(cont.listeners)) {
11644
+ cont.watcher.close();
11645
+ FsWatchInstances.delete(fullPath);
11646
+ HANDLER_KEYS.forEach(clearItem(cont));
11647
+ cont.watcher = void 0;
11648
+ Object.freeze(cont);
11649
+ }
11650
+ };
11651
+ };
11652
+ var FsWatchFileInstances = /* @__PURE__ */ new Map();
11653
+ var setFsWatchFileListener = (path25, fullPath, options, handlers) => {
11654
+ const { listener, rawEmitter } = handlers;
11655
+ let cont = FsWatchFileInstances.get(fullPath);
11656
+ const copts = cont && cont.options;
11657
+ if (copts && (copts.persistent < options.persistent || copts.interval > options.interval)) {
11658
+ unwatchFile(fullPath);
11659
+ cont = void 0;
11660
+ }
11661
+ if (cont) {
11662
+ addAndConvert(cont, KEY_LISTENERS, listener);
11663
+ addAndConvert(cont, KEY_RAW, rawEmitter);
11664
+ } else {
11665
+ cont = {
11666
+ listeners: listener,
11667
+ rawEmitters: rawEmitter,
11668
+ options,
11669
+ watcher: watchFile(fullPath, options, (curr, prev) => {
11670
+ foreach(cont.rawEmitters, (rawEmitter2) => {
11671
+ rawEmitter2(EV.CHANGE, fullPath, { curr, prev });
11672
+ });
11673
+ const currmtime = curr.mtimeMs;
11674
+ if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
11675
+ foreach(cont.listeners, (listener2) => listener2(path25, curr));
11676
+ }
11677
+ })
11678
+ };
11679
+ FsWatchFileInstances.set(fullPath, cont);
11680
+ }
11681
+ return () => {
11682
+ delFromSet(cont, KEY_LISTENERS, listener);
11683
+ delFromSet(cont, KEY_RAW, rawEmitter);
11684
+ if (isEmptySet(cont.listeners)) {
11685
+ FsWatchFileInstances.delete(fullPath);
11686
+ unwatchFile(fullPath);
11687
+ cont.options = cont.watcher = void 0;
11688
+ Object.freeze(cont);
11689
+ }
11690
+ };
11691
+ };
11692
+ var NodeFsHandler = class {
11693
+ fsw;
11694
+ _boundHandleError;
11695
+ constructor(fsW) {
11696
+ this.fsw = fsW;
11697
+ this._boundHandleError = (error) => fsW._handleError(error);
11698
+ }
11699
+ /**
11700
+ * Watch file for changes with fs_watchFile or fs_watch.
11701
+ * @param path to file or dir
11702
+ * @param listener on fs change
11703
+ * @returns closer for the watcher instance
11704
+ */
11705
+ _watchWithNodeFs(path25, listener) {
11706
+ const opts = this.fsw.options;
11707
+ const directory = sp.dirname(path25);
11708
+ const basename5 = sp.basename(path25);
11709
+ const parent = this.fsw._getWatchedDir(directory);
11710
+ parent.add(basename5);
11711
+ const absolutePath = sp.resolve(path25);
11712
+ const options = {
11713
+ persistent: opts.persistent
11714
+ };
11715
+ if (!listener)
11716
+ listener = EMPTY_FN;
11717
+ let closer;
11718
+ if (opts.usePolling) {
11719
+ const enableBin = opts.interval !== opts.binaryInterval;
11720
+ options.interval = enableBin && isBinaryPath(basename5) ? opts.binaryInterval : opts.interval;
11721
+ closer = setFsWatchFileListener(path25, absolutePath, options, {
11722
+ listener,
11723
+ rawEmitter: this.fsw._emitRaw
11724
+ });
11725
+ } else {
11726
+ closer = setFsWatchListener(path25, absolutePath, options, {
11727
+ listener,
11728
+ errHandler: this._boundHandleError,
11729
+ rawEmitter: this.fsw._emitRaw
11730
+ });
11731
+ }
11732
+ return closer;
11733
+ }
11734
+ /**
11735
+ * Watch a file and emit add event if warranted.
11736
+ * @returns closer for the watcher instance
11737
+ */
11738
+ _handleFile(file, stats, initialAdd) {
11739
+ if (this.fsw.closed) {
11740
+ return;
11741
+ }
11742
+ const dirname10 = sp.dirname(file);
11743
+ const basename5 = sp.basename(file);
11744
+ const parent = this.fsw._getWatchedDir(dirname10);
11745
+ let prevStats = stats;
11746
+ if (parent.has(basename5))
11747
+ return;
11748
+ const listener = async (path25, newStats) => {
11749
+ if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
11750
+ return;
11751
+ if (!newStats || newStats.mtimeMs === 0) {
11752
+ try {
11753
+ const newStats2 = await stat2(file);
11754
+ if (this.fsw.closed)
11755
+ return;
11756
+ const at = newStats2.atimeMs;
11757
+ const mt = newStats2.mtimeMs;
11758
+ if (!at || at <= mt || mt !== prevStats.mtimeMs) {
11759
+ this.fsw._emit(EV.CHANGE, file, newStats2);
11760
+ }
11761
+ if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats2.ino) {
11762
+ this.fsw._closeFile(path25);
11763
+ prevStats = newStats2;
11764
+ const closer2 = this._watchWithNodeFs(file, listener);
11765
+ if (closer2)
11766
+ this.fsw._addPathCloser(path25, closer2);
11767
+ } else {
11768
+ prevStats = newStats2;
11769
+ }
11770
+ } catch (error) {
11771
+ this.fsw._remove(dirname10, basename5);
11772
+ }
11773
+ } else if (parent.has(basename5)) {
11774
+ const at = newStats.atimeMs;
11775
+ const mt = newStats.mtimeMs;
11776
+ if (!at || at <= mt || mt !== prevStats.mtimeMs) {
11777
+ this.fsw._emit(EV.CHANGE, file, newStats);
11778
+ }
11779
+ prevStats = newStats;
11780
+ }
11781
+ };
11782
+ const closer = this._watchWithNodeFs(file, listener);
11783
+ if (!(initialAdd && this.fsw.options.ignoreInitial) && this.fsw._isntIgnored(file)) {
11784
+ if (!this.fsw._throttle(EV.ADD, file, 0))
11785
+ return;
11786
+ this.fsw._emit(EV.ADD, file, stats);
11787
+ }
11788
+ return closer;
11789
+ }
11790
+ /**
11791
+ * Handle symlinks encountered while reading a dir.
11792
+ * @param entry returned by readdirp
11793
+ * @param directory path of dir being read
11794
+ * @param path of this item
11795
+ * @param item basename of this item
11796
+ * @returns true if no more processing is needed for this entry.
11797
+ */
11798
+ async _handleSymlink(entry, directory, path25, item) {
11799
+ if (this.fsw.closed) {
11800
+ return;
11801
+ }
11802
+ const full = entry.fullPath;
11803
+ const dir = this.fsw._getWatchedDir(directory);
11804
+ if (!this.fsw.options.followSymlinks) {
11805
+ this.fsw._incrReadyCount();
11806
+ let linkPath;
11807
+ try {
11808
+ linkPath = await fsrealpath(path25);
11809
+ } catch (e) {
11810
+ this.fsw._emitReady();
11811
+ return true;
11812
+ }
11813
+ if (this.fsw.closed)
11814
+ return;
11815
+ if (dir.has(item)) {
11816
+ if (this.fsw._symlinkPaths.get(full) !== linkPath) {
11817
+ this.fsw._symlinkPaths.set(full, linkPath);
11818
+ this.fsw._emit(EV.CHANGE, path25, entry.stats);
11819
+ }
11820
+ } else {
11821
+ dir.add(item);
11822
+ this.fsw._symlinkPaths.set(full, linkPath);
11823
+ this.fsw._emit(EV.ADD, path25, entry.stats);
11824
+ }
11825
+ this.fsw._emitReady();
11826
+ return true;
11827
+ }
11828
+ if (this.fsw._symlinkPaths.has(full)) {
11829
+ return true;
11830
+ }
11831
+ this.fsw._symlinkPaths.set(full, true);
11832
+ }
11833
+ _handleRead(directory, initialAdd, wh, target, dir, depth, throttler) {
11834
+ directory = sp.join(directory, "");
11835
+ const throttleKey = target ? `${directory}:${target}` : directory;
11836
+ throttler = this.fsw._throttle("readdir", throttleKey, 1e3);
11837
+ if (!throttler)
11838
+ return;
11839
+ const previous = this.fsw._getWatchedDir(wh.path);
11840
+ const current = /* @__PURE__ */ new Set();
11841
+ let stream = this.fsw._readdirp(directory, {
11842
+ fileFilter: (entry) => wh.filterPath(entry),
11843
+ directoryFilter: (entry) => wh.filterDir(entry)
11844
+ });
11845
+ if (!stream)
11846
+ return;
11847
+ stream.on(STR_DATA, async (entry) => {
11848
+ if (this.fsw.closed) {
11849
+ stream = void 0;
11850
+ return;
11851
+ }
11852
+ const item = entry.path;
11853
+ let path25 = sp.join(directory, item);
11854
+ current.add(item);
11855
+ if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path25, item)) {
11856
+ return;
11857
+ }
11858
+ if (this.fsw.closed) {
11859
+ stream = void 0;
11860
+ return;
11861
+ }
11862
+ if (item === target || !target && !previous.has(item)) {
11863
+ this.fsw._incrReadyCount();
11864
+ path25 = sp.join(dir, sp.relative(dir, path25));
11865
+ this._addToNodeFs(path25, initialAdd, wh, depth + 1);
11866
+ }
11867
+ }).on(EV.ERROR, this._boundHandleError);
11868
+ return new Promise((resolve15, reject) => {
11869
+ if (!stream)
11870
+ return reject();
11871
+ stream.once(STR_END, () => {
11872
+ if (this.fsw.closed) {
11873
+ stream = void 0;
11874
+ return;
11875
+ }
11876
+ const wasThrottled = throttler ? throttler.clear() : false;
11877
+ resolve15(void 0);
11878
+ previous.getChildren().filter((item) => {
11879
+ return item !== directory && !current.has(item);
11880
+ }).forEach((item) => {
11881
+ this.fsw._remove(directory, item);
11882
+ });
11883
+ stream = void 0;
11884
+ if (wasThrottled)
11885
+ this._handleRead(directory, false, wh, target, dir, depth, throttler);
11886
+ });
11887
+ });
11888
+ }
11889
+ /**
11890
+ * Read directory to add / remove files from `@watched` list and re-read it on change.
11891
+ * @param dir fs path
11892
+ * @param stats
11893
+ * @param initialAdd
11894
+ * @param depth relative to user-supplied path
11895
+ * @param target child path targeted for watch
11896
+ * @param wh Common watch helpers for this path
11897
+ * @param realpath
11898
+ * @returns closer for the watcher instance.
11899
+ */
11900
+ async _handleDir(dir, stats, initialAdd, depth, target, wh, realpath2) {
11901
+ const parentDir = this.fsw._getWatchedDir(sp.dirname(dir));
11902
+ const tracked = parentDir.has(sp.basename(dir));
11903
+ if (!(initialAdd && this.fsw.options.ignoreInitial) && !target && !tracked) {
11904
+ this.fsw._emit(EV.ADD_DIR, dir, stats);
11905
+ }
11906
+ parentDir.add(sp.basename(dir));
11907
+ this.fsw._getWatchedDir(dir);
11908
+ let throttler;
11909
+ let closer;
11910
+ const oDepth = this.fsw.options.depth;
11911
+ if ((oDepth == null || depth <= oDepth) && !this.fsw._symlinkPaths.has(realpath2)) {
11912
+ if (!target) {
11913
+ await this._handleRead(dir, initialAdd, wh, target, dir, depth, throttler);
11914
+ if (this.fsw.closed)
11915
+ return;
11916
+ }
11917
+ closer = this._watchWithNodeFs(dir, (dirPath, stats2) => {
11918
+ if (stats2 && stats2.mtimeMs === 0)
11919
+ return;
11920
+ this._handleRead(dirPath, false, wh, target, dir, depth, throttler);
11921
+ });
11922
+ }
11923
+ return closer;
11924
+ }
11925
+ /**
11926
+ * Handle added file, directory, or glob pattern.
11927
+ * Delegates call to _handleFile / _handleDir after checks.
11928
+ * @param path to file or ir
11929
+ * @param initialAdd was the file added at watch instantiation?
11930
+ * @param priorWh depth relative to user-supplied path
11931
+ * @param depth Child path actually targeted for watch
11932
+ * @param target Child path actually targeted for watch
11933
+ */
11934
+ async _addToNodeFs(path25, initialAdd, priorWh, depth, target) {
11935
+ const ready = this.fsw._emitReady;
11936
+ if (this.fsw._isIgnored(path25) || this.fsw.closed) {
11937
+ ready();
11938
+ return false;
11939
+ }
11940
+ const wh = this.fsw._getWatchHelpers(path25);
11941
+ if (priorWh) {
11942
+ wh.filterPath = (entry) => priorWh.filterPath(entry);
11943
+ wh.filterDir = (entry) => priorWh.filterDir(entry);
11944
+ }
11945
+ try {
11946
+ const stats = await statMethods[wh.statMethod](wh.watchPath);
11947
+ if (this.fsw.closed)
11948
+ return;
11949
+ if (this.fsw._isIgnored(wh.watchPath, stats)) {
11950
+ ready();
11951
+ return false;
11952
+ }
11953
+ const follow = this.fsw.options.followSymlinks;
11954
+ let closer;
11955
+ if (stats.isDirectory()) {
11956
+ const absPath = sp.resolve(path25);
11957
+ const targetPath = follow ? await fsrealpath(path25) : path25;
11958
+ if (this.fsw.closed)
11959
+ return;
11960
+ closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
11961
+ if (this.fsw.closed)
11962
+ return;
11963
+ if (absPath !== targetPath && targetPath !== void 0) {
11964
+ this.fsw._symlinkPaths.set(absPath, targetPath);
11965
+ }
11966
+ } else if (stats.isSymbolicLink()) {
11967
+ const targetPath = follow ? await fsrealpath(path25) : path25;
11968
+ if (this.fsw.closed)
11969
+ return;
11970
+ const parent = sp.dirname(wh.watchPath);
11971
+ this.fsw._getWatchedDir(parent).add(wh.watchPath);
11972
+ this.fsw._emit(EV.ADD, wh.watchPath, stats);
11973
+ closer = await this._handleDir(parent, stats, initialAdd, depth, path25, wh, targetPath);
11974
+ if (this.fsw.closed)
11975
+ return;
11976
+ if (targetPath !== void 0) {
11977
+ this.fsw._symlinkPaths.set(sp.resolve(path25), targetPath);
11978
+ }
11979
+ } else {
11980
+ closer = this._handleFile(wh.watchPath, stats, initialAdd);
11981
+ }
11982
+ ready();
11983
+ if (closer)
11984
+ this.fsw._addPathCloser(path25, closer);
11985
+ return false;
11986
+ } catch (error) {
11987
+ if (this.fsw._handleError(error)) {
11988
+ ready();
11989
+ return path25;
11990
+ }
11991
+ }
11992
+ }
11993
+ };
11994
+
11995
+ // node_modules/chokidar/index.js
11996
+ var SLASH = "/";
11997
+ var SLASH_SLASH = "//";
11998
+ var ONE_DOT = ".";
11999
+ var TWO_DOTS = "..";
12000
+ var STRING_TYPE = "string";
12001
+ var BACK_SLASH_RE = /\\/g;
12002
+ var DOUBLE_SLASH_RE = /\/\//g;
12003
+ var DOT_RE = /\..*\.(sw[px])$|~$|\.subl.*\.tmp/;
12004
+ var REPLACER_RE = /^\.[/\\]/;
12005
+ function arrify(item) {
12006
+ return Array.isArray(item) ? item : [item];
12007
+ }
12008
+ var isMatcherObject = (matcher) => typeof matcher === "object" && matcher !== null && !(matcher instanceof RegExp);
12009
+ function createPattern(matcher) {
12010
+ if (typeof matcher === "function")
12011
+ return matcher;
12012
+ if (typeof matcher === "string")
12013
+ return (string) => matcher === string;
12014
+ if (matcher instanceof RegExp)
12015
+ return (string) => matcher.test(string);
12016
+ if (typeof matcher === "object" && matcher !== null) {
12017
+ return (string) => {
12018
+ if (matcher.path === string)
12019
+ return true;
12020
+ if (matcher.recursive) {
12021
+ const relative10 = sp2.relative(matcher.path, string);
12022
+ if (!relative10) {
12023
+ return false;
12024
+ }
12025
+ return !relative10.startsWith("..") && !sp2.isAbsolute(relative10);
12026
+ }
12027
+ return false;
12028
+ };
12029
+ }
12030
+ return () => false;
12031
+ }
12032
+ function normalizePath2(path25) {
12033
+ if (typeof path25 !== "string")
12034
+ throw new Error("string expected");
12035
+ path25 = sp2.normalize(path25);
12036
+ path25 = path25.replace(/\\/g, "/");
12037
+ let prepend = false;
12038
+ if (path25.startsWith("//"))
12039
+ prepend = true;
12040
+ path25 = path25.replace(DOUBLE_SLASH_RE, "/");
12041
+ if (prepend)
12042
+ path25 = "/" + path25;
12043
+ return path25;
12044
+ }
12045
+ function matchPatterns(patterns, testString, stats) {
12046
+ const path25 = normalizePath2(testString);
12047
+ for (let index = 0; index < patterns.length; index++) {
12048
+ const pattern = patterns[index];
12049
+ if (pattern(path25, stats)) {
12050
+ return true;
12051
+ }
12052
+ }
12053
+ return false;
12054
+ }
12055
+ function anymatch(matchers, testString) {
12056
+ if (matchers == null) {
12057
+ throw new TypeError("anymatch: specify first argument");
12058
+ }
12059
+ const matchersArray = arrify(matchers);
12060
+ const patterns = matchersArray.map((matcher) => createPattern(matcher));
12061
+ if (testString == null) {
12062
+ return (testString2, stats) => {
12063
+ return matchPatterns(patterns, testString2, stats);
12064
+ };
12065
+ }
12066
+ return matchPatterns(patterns, testString);
12067
+ }
12068
+ var unifyPaths = (paths_) => {
12069
+ const paths = arrify(paths_).flat();
12070
+ if (!paths.every((p) => typeof p === STRING_TYPE)) {
12071
+ throw new TypeError(`Non-string provided as watch path: ${paths}`);
12072
+ }
12073
+ return paths.map(normalizePathToUnix);
12074
+ };
12075
+ var toUnix = (string) => {
12076
+ let str = string.replace(BACK_SLASH_RE, SLASH);
12077
+ let prepend = false;
12078
+ if (str.startsWith(SLASH_SLASH)) {
12079
+ prepend = true;
12080
+ }
12081
+ str = str.replace(DOUBLE_SLASH_RE, SLASH);
12082
+ if (prepend) {
12083
+ str = SLASH + str;
12084
+ }
12085
+ return str;
12086
+ };
12087
+ var normalizePathToUnix = (path25) => toUnix(sp2.normalize(toUnix(path25)));
12088
+ var normalizeIgnored = (cwd = "") => (path25) => {
12089
+ if (typeof path25 === "string") {
12090
+ return normalizePathToUnix(sp2.isAbsolute(path25) ? path25 : sp2.join(cwd, path25));
12091
+ } else {
12092
+ return path25;
12093
+ }
12094
+ };
12095
+ var getAbsolutePath = (path25, cwd) => {
12096
+ if (sp2.isAbsolute(path25)) {
12097
+ return path25;
12098
+ }
12099
+ return sp2.join(cwd, path25);
12100
+ };
12101
+ var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
12102
+ var DirEntry = class {
12103
+ path;
12104
+ _removeWatcher;
12105
+ items;
12106
+ constructor(dir, removeWatcher) {
12107
+ this.path = dir;
12108
+ this._removeWatcher = removeWatcher;
12109
+ this.items = /* @__PURE__ */ new Set();
12110
+ }
12111
+ add(item) {
12112
+ const { items } = this;
12113
+ if (!items)
12114
+ return;
12115
+ if (item !== ONE_DOT && item !== TWO_DOTS)
12116
+ items.add(item);
12117
+ }
12118
+ async remove(item) {
12119
+ const { items } = this;
12120
+ if (!items)
12121
+ return;
12122
+ items.delete(item);
12123
+ if (items.size > 0)
12124
+ return;
12125
+ const dir = this.path;
12126
+ try {
12127
+ await readdir2(dir);
12128
+ } catch (err) {
12129
+ if (this._removeWatcher) {
12130
+ this._removeWatcher(sp2.dirname(dir), sp2.basename(dir));
12131
+ }
12132
+ }
12133
+ }
12134
+ has(item) {
12135
+ const { items } = this;
12136
+ if (!items)
12137
+ return;
12138
+ return items.has(item);
12139
+ }
12140
+ getChildren() {
12141
+ const { items } = this;
12142
+ if (!items)
12143
+ return [];
12144
+ return [...items.values()];
12145
+ }
12146
+ dispose() {
12147
+ this.items.clear();
12148
+ this.path = "";
12149
+ this._removeWatcher = EMPTY_FN;
12150
+ this.items = EMPTY_SET;
12151
+ Object.freeze(this);
12152
+ }
12153
+ };
12154
+ var STAT_METHOD_F = "stat";
12155
+ var STAT_METHOD_L = "lstat";
12156
+ var WatchHelper = class {
12157
+ fsw;
12158
+ path;
12159
+ watchPath;
12160
+ fullWatchPath;
12161
+ dirParts;
12162
+ followSymlinks;
12163
+ statMethod;
12164
+ constructor(path25, follow, fsw) {
12165
+ this.fsw = fsw;
12166
+ const watchPath = path25;
12167
+ this.path = path25 = path25.replace(REPLACER_RE, "");
12168
+ this.watchPath = watchPath;
12169
+ this.fullWatchPath = sp2.resolve(watchPath);
12170
+ this.dirParts = [];
12171
+ this.dirParts.forEach((parts) => {
12172
+ if (parts.length > 1)
12173
+ parts.pop();
12174
+ });
12175
+ this.followSymlinks = follow;
12176
+ this.statMethod = follow ? STAT_METHOD_F : STAT_METHOD_L;
12177
+ }
12178
+ entryPath(entry) {
12179
+ return sp2.join(this.watchPath, sp2.relative(this.watchPath, entry.fullPath));
12180
+ }
12181
+ filterPath(entry) {
12182
+ const { stats } = entry;
12183
+ if (stats && stats.isSymbolicLink())
12184
+ return this.filterDir(entry);
12185
+ const resolvedPath = this.entryPath(entry);
12186
+ return this.fsw._isntIgnored(resolvedPath, stats) && this.fsw._hasReadPermissions(stats);
12187
+ }
12188
+ filterDir(entry) {
12189
+ return this.fsw._isntIgnored(this.entryPath(entry), entry.stats);
12190
+ }
12191
+ };
12192
+ var FSWatcher = class extends EventEmitter2 {
12193
+ closed;
12194
+ options;
12195
+ _closers;
12196
+ _ignoredPaths;
12197
+ _throttled;
12198
+ _streams;
12199
+ _symlinkPaths;
12200
+ _watched;
12201
+ _pendingWrites;
12202
+ _pendingUnlinks;
12203
+ _readyCount;
12204
+ _emitReady;
12205
+ _closePromise;
12206
+ _userIgnored;
12207
+ _readyEmitted;
12208
+ _emitRaw;
12209
+ _boundRemove;
12210
+ _nodeFsHandler;
12211
+ // Not indenting methods for history sake; for now.
12212
+ constructor(_opts = {}) {
12213
+ super();
12214
+ this.closed = false;
12215
+ this._closers = /* @__PURE__ */ new Map();
12216
+ this._ignoredPaths = /* @__PURE__ */ new Set();
12217
+ this._throttled = /* @__PURE__ */ new Map();
12218
+ this._streams = /* @__PURE__ */ new Set();
12219
+ this._symlinkPaths = /* @__PURE__ */ new Map();
12220
+ this._watched = /* @__PURE__ */ new Map();
12221
+ this._pendingWrites = /* @__PURE__ */ new Map();
12222
+ this._pendingUnlinks = /* @__PURE__ */ new Map();
12223
+ this._readyCount = 0;
12224
+ this._readyEmitted = false;
12225
+ const awf = _opts.awaitWriteFinish;
12226
+ const DEF_AWF = { stabilityThreshold: 2e3, pollInterval: 100 };
12227
+ const opts = {
12228
+ // Defaults
12229
+ persistent: true,
12230
+ ignoreInitial: false,
12231
+ ignorePermissionErrors: false,
12232
+ interval: 100,
12233
+ binaryInterval: 300,
12234
+ followSymlinks: true,
12235
+ usePolling: false,
12236
+ // useAsync: false,
12237
+ atomic: true,
12238
+ // NOTE: overwritten later (depends on usePolling)
12239
+ ..._opts,
12240
+ // Change format
12241
+ ignored: _opts.ignored ? arrify(_opts.ignored) : arrify([]),
12242
+ awaitWriteFinish: awf === true ? DEF_AWF : typeof awf === "object" ? { ...DEF_AWF, ...awf } : false
12243
+ };
12244
+ if (isIBMi)
12245
+ opts.usePolling = true;
12246
+ if (opts.atomic === void 0)
12247
+ opts.atomic = !opts.usePolling;
12248
+ const envPoll = process.env.CHOKIDAR_USEPOLLING;
12249
+ if (envPoll !== void 0) {
12250
+ const envLower = envPoll.toLowerCase();
12251
+ if (envLower === "false" || envLower === "0")
12252
+ opts.usePolling = false;
12253
+ else if (envLower === "true" || envLower === "1")
12254
+ opts.usePolling = true;
12255
+ else
12256
+ opts.usePolling = !!envLower;
12257
+ }
12258
+ const envInterval = process.env.CHOKIDAR_INTERVAL;
12259
+ if (envInterval)
12260
+ opts.interval = Number.parseInt(envInterval, 10);
12261
+ let readyCalls = 0;
12262
+ this._emitReady = () => {
12263
+ readyCalls++;
12264
+ if (readyCalls >= this._readyCount) {
12265
+ this._emitReady = EMPTY_FN;
12266
+ this._readyEmitted = true;
12267
+ process.nextTick(() => this.emit(EVENTS.READY));
12268
+ }
12269
+ };
12270
+ this._emitRaw = (...args) => this.emit(EVENTS.RAW, ...args);
12271
+ this._boundRemove = this._remove.bind(this);
12272
+ this.options = opts;
12273
+ this._nodeFsHandler = new NodeFsHandler(this);
12274
+ Object.freeze(opts);
12275
+ }
12276
+ _addIgnoredPath(matcher) {
12277
+ if (isMatcherObject(matcher)) {
12278
+ for (const ignored of this._ignoredPaths) {
12279
+ if (isMatcherObject(ignored) && ignored.path === matcher.path && ignored.recursive === matcher.recursive) {
12280
+ return;
12281
+ }
12282
+ }
12283
+ }
12284
+ this._ignoredPaths.add(matcher);
12285
+ }
12286
+ _removeIgnoredPath(matcher) {
12287
+ this._ignoredPaths.delete(matcher);
12288
+ if (typeof matcher === "string") {
12289
+ for (const ignored of this._ignoredPaths) {
12290
+ if (isMatcherObject(ignored) && ignored.path === matcher) {
12291
+ this._ignoredPaths.delete(ignored);
12292
+ }
12293
+ }
12294
+ }
12295
+ }
12296
+ // Public methods
12297
+ /**
12298
+ * Adds paths to be watched on an existing FSWatcher instance.
12299
+ * @param paths_ file or file list. Other arguments are unused
12300
+ */
12301
+ add(paths_, _origAdd, _internal) {
12302
+ const { cwd } = this.options;
12303
+ this.closed = false;
12304
+ this._closePromise = void 0;
12305
+ let paths = unifyPaths(paths_);
12306
+ if (cwd) {
12307
+ paths = paths.map((path25) => {
12308
+ const absPath = getAbsolutePath(path25, cwd);
12309
+ return absPath;
12310
+ });
12311
+ }
12312
+ paths.forEach((path25) => {
12313
+ this._removeIgnoredPath(path25);
12314
+ });
12315
+ this._userIgnored = void 0;
12316
+ if (!this._readyCount)
12317
+ this._readyCount = 0;
12318
+ this._readyCount += paths.length;
12319
+ Promise.all(paths.map(async (path25) => {
12320
+ const res = await this._nodeFsHandler._addToNodeFs(path25, !_internal, void 0, 0, _origAdd);
12321
+ if (res)
12322
+ this._emitReady();
12323
+ return res;
12324
+ })).then((results) => {
12325
+ if (this.closed)
12326
+ return;
12327
+ results.forEach((item) => {
12328
+ if (item)
12329
+ this.add(sp2.dirname(item), sp2.basename(_origAdd || item));
12330
+ });
12331
+ });
12332
+ return this;
12333
+ }
12334
+ /**
12335
+ * Close watchers or start ignoring events from specified paths.
12336
+ */
12337
+ unwatch(paths_) {
12338
+ if (this.closed)
12339
+ return this;
12340
+ const paths = unifyPaths(paths_);
12341
+ const { cwd } = this.options;
12342
+ paths.forEach((path25) => {
12343
+ if (!sp2.isAbsolute(path25) && !this._closers.has(path25)) {
12344
+ if (cwd)
12345
+ path25 = sp2.join(cwd, path25);
12346
+ path25 = sp2.resolve(path25);
12347
+ }
12348
+ this._closePath(path25);
12349
+ this._addIgnoredPath(path25);
12350
+ if (this._watched.has(path25)) {
12351
+ this._addIgnoredPath({
12352
+ path: path25,
12353
+ recursive: true
12354
+ });
12355
+ }
12356
+ this._userIgnored = void 0;
12357
+ });
12358
+ return this;
12359
+ }
12360
+ /**
12361
+ * Close watchers and remove all listeners from watched paths.
12362
+ */
12363
+ close() {
12364
+ if (this._closePromise) {
12365
+ return this._closePromise;
12366
+ }
12367
+ this.closed = true;
12368
+ this.removeAllListeners();
12369
+ const closers = [];
12370
+ this._closers.forEach((closerList) => closerList.forEach((closer) => {
12371
+ const promise = closer();
12372
+ if (promise instanceof Promise)
12373
+ closers.push(promise);
12374
+ }));
12375
+ this._streams.forEach((stream) => stream.destroy());
12376
+ this._userIgnored = void 0;
12377
+ this._readyCount = 0;
12378
+ this._readyEmitted = false;
12379
+ this._watched.forEach((dirent) => dirent.dispose());
12380
+ this._closers.clear();
12381
+ this._watched.clear();
12382
+ this._streams.clear();
12383
+ this._symlinkPaths.clear();
12384
+ this._throttled.clear();
12385
+ this._closePromise = closers.length ? Promise.all(closers).then(() => void 0) : Promise.resolve();
12386
+ return this._closePromise;
12387
+ }
12388
+ /**
12389
+ * Expose list of watched paths
12390
+ * @returns for chaining
12391
+ */
12392
+ getWatched() {
12393
+ const watchList = {};
12394
+ this._watched.forEach((entry, dir) => {
12395
+ const key = this.options.cwd ? sp2.relative(this.options.cwd, dir) : dir;
12396
+ const index = key || ONE_DOT;
12397
+ watchList[index] = entry.getChildren().sort();
12398
+ });
12399
+ return watchList;
12400
+ }
12401
+ emitWithAll(event, args) {
12402
+ this.emit(event, ...args);
12403
+ if (event !== EVENTS.ERROR)
12404
+ this.emit(EVENTS.ALL, event, ...args);
12405
+ }
12406
+ // Common helpers
12407
+ // --------------
12408
+ /**
12409
+ * Normalize and emit events.
12410
+ * Calling _emit DOES NOT MEAN emit() would be called!
12411
+ * @param event Type of event
12412
+ * @param path File or directory path
12413
+ * @param stats arguments to be passed with event
12414
+ * @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
12415
+ */
12416
+ async _emit(event, path25, stats) {
12417
+ if (this.closed)
12418
+ return;
12419
+ const opts = this.options;
12420
+ if (isWindows)
12421
+ path25 = sp2.normalize(path25);
12422
+ if (opts.cwd)
12423
+ path25 = sp2.relative(opts.cwd, path25);
12424
+ const args = [path25];
12425
+ if (stats != null)
12426
+ args.push(stats);
12427
+ const awf = opts.awaitWriteFinish;
12428
+ let pw;
12429
+ if (awf && (pw = this._pendingWrites.get(path25))) {
12430
+ pw.lastChange = /* @__PURE__ */ new Date();
12431
+ return this;
12432
+ }
12433
+ if (opts.atomic) {
12434
+ if (event === EVENTS.UNLINK) {
12435
+ this._pendingUnlinks.set(path25, [event, ...args]);
12436
+ setTimeout(() => {
12437
+ this._pendingUnlinks.forEach((entry, path26) => {
12438
+ this.emit(...entry);
12439
+ this.emit(EVENTS.ALL, ...entry);
12440
+ this._pendingUnlinks.delete(path26);
12441
+ });
12442
+ }, typeof opts.atomic === "number" ? opts.atomic : 100);
12443
+ return this;
12444
+ }
12445
+ if (event === EVENTS.ADD && this._pendingUnlinks.has(path25)) {
12446
+ event = EVENTS.CHANGE;
12447
+ this._pendingUnlinks.delete(path25);
12448
+ }
12449
+ }
12450
+ if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) {
12451
+ const awfEmit = (err, stats2) => {
12452
+ if (err) {
12453
+ event = EVENTS.ERROR;
12454
+ args[0] = err;
12455
+ this.emitWithAll(event, args);
12456
+ } else if (stats2) {
12457
+ if (args.length > 1) {
12458
+ args[1] = stats2;
12459
+ } else {
12460
+ args.push(stats2);
12461
+ }
12462
+ this.emitWithAll(event, args);
12463
+ }
12464
+ };
12465
+ this._awaitWriteFinish(path25, awf.stabilityThreshold, event, awfEmit);
12466
+ return this;
12467
+ }
12468
+ if (event === EVENTS.CHANGE) {
12469
+ const isThrottled = !this._throttle(EVENTS.CHANGE, path25, 50);
12470
+ if (isThrottled)
12471
+ return this;
12472
+ }
12473
+ if (opts.alwaysStat && stats === void 0 && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) {
12474
+ const fullPath = opts.cwd ? sp2.join(opts.cwd, path25) : path25;
12475
+ let stats2;
12476
+ try {
12477
+ stats2 = await stat3(fullPath);
12478
+ } catch (err) {
12479
+ }
12480
+ if (!stats2 || this.closed)
12481
+ return;
12482
+ args.push(stats2);
12483
+ }
12484
+ this.emitWithAll(event, args);
12485
+ return this;
12486
+ }
12487
+ /**
12488
+ * Common handler for errors
12489
+ * @returns The error if defined, otherwise the value of the FSWatcher instance's `closed` flag
12490
+ */
12491
+ _handleError(error) {
12492
+ const code = error && error.code;
12493
+ if (error && code !== "ENOENT" && code !== "ENOTDIR" && (!this.options.ignorePermissionErrors || code !== "EPERM" && code !== "EACCES")) {
12494
+ this.emit(EVENTS.ERROR, error);
12495
+ }
12496
+ return error || this.closed;
12497
+ }
12498
+ /**
12499
+ * Helper utility for throttling
12500
+ * @param actionType type being throttled
12501
+ * @param path being acted upon
12502
+ * @param timeout duration of time to suppress duplicate actions
12503
+ * @returns tracking object or false if action should be suppressed
12504
+ */
12505
+ _throttle(actionType, path25, timeout) {
12506
+ if (!this._throttled.has(actionType)) {
12507
+ this._throttled.set(actionType, /* @__PURE__ */ new Map());
12508
+ }
12509
+ const action = this._throttled.get(actionType);
12510
+ if (!action)
12511
+ throw new Error("invalid throttle");
12512
+ const actionPath = action.get(path25);
12513
+ if (actionPath) {
12514
+ actionPath.count++;
12515
+ return false;
12516
+ }
12517
+ let timeoutObject;
12518
+ const clear = () => {
12519
+ const item = action.get(path25);
12520
+ const count = item ? item.count : 0;
12521
+ action.delete(path25);
12522
+ clearTimeout(timeoutObject);
12523
+ if (item)
12524
+ clearTimeout(item.timeoutObject);
12525
+ return count;
12526
+ };
12527
+ timeoutObject = setTimeout(clear, timeout);
12528
+ const thr = { timeoutObject, clear, count: 0 };
12529
+ action.set(path25, thr);
12530
+ return thr;
12531
+ }
12532
+ _incrReadyCount() {
12533
+ return this._readyCount++;
12534
+ }
12535
+ /**
12536
+ * Awaits write operation to finish.
12537
+ * Polls a newly created file for size variations. When files size does not change for 'threshold' milliseconds calls callback.
12538
+ * @param path being acted upon
12539
+ * @param threshold Time in milliseconds a file size must be fixed before acknowledging write OP is finished
12540
+ * @param event
12541
+ * @param awfEmit Callback to be called when ready for event to be emitted.
12542
+ */
12543
+ _awaitWriteFinish(path25, threshold, event, awfEmit) {
12544
+ const awf = this.options.awaitWriteFinish;
12545
+ if (typeof awf !== "object")
12546
+ return;
12547
+ const pollInterval = awf.pollInterval;
12548
+ let timeoutHandler;
12549
+ let fullPath = path25;
12550
+ if (this.options.cwd && !sp2.isAbsolute(path25)) {
12551
+ fullPath = sp2.join(this.options.cwd, path25);
12552
+ }
12553
+ const now = /* @__PURE__ */ new Date();
12554
+ const writes = this._pendingWrites;
12555
+ function awaitWriteFinishFn(prevStat) {
12556
+ statcb(fullPath, (err, curStat) => {
12557
+ if (err || !writes.has(path25)) {
12558
+ if (err && err.code !== "ENOENT")
12559
+ awfEmit(err);
12560
+ return;
12561
+ }
12562
+ const now2 = Number(/* @__PURE__ */ new Date());
12563
+ if (prevStat && curStat.size !== prevStat.size) {
12564
+ writes.get(path25).lastChange = now2;
12565
+ }
12566
+ const pw = writes.get(path25);
12567
+ const df = now2 - pw.lastChange;
12568
+ if (df >= threshold) {
12569
+ writes.delete(path25);
12570
+ awfEmit(void 0, curStat);
12571
+ } else {
12572
+ timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
12573
+ }
12574
+ });
12575
+ }
12576
+ if (!writes.has(path25)) {
12577
+ writes.set(path25, {
12578
+ lastChange: now,
12579
+ cancelWait: () => {
12580
+ writes.delete(path25);
12581
+ clearTimeout(timeoutHandler);
12582
+ return event;
12583
+ }
12584
+ });
12585
+ timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval);
12586
+ }
12587
+ }
12588
+ /**
12589
+ * Determines whether user has asked to ignore this path.
12590
+ */
12591
+ _isIgnored(path25, stats) {
12592
+ if (this.options.atomic && DOT_RE.test(path25))
12593
+ return true;
12594
+ if (!this._userIgnored) {
12595
+ const { cwd } = this.options;
12596
+ const ign = this.options.ignored;
12597
+ const ignored = (ign || []).map(normalizeIgnored(cwd));
12598
+ const ignoredPaths = [...this._ignoredPaths];
12599
+ const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
12600
+ this._userIgnored = anymatch(list, void 0);
12601
+ }
12602
+ return this._userIgnored(path25, stats);
12603
+ }
12604
+ _isntIgnored(path25, stat4) {
12605
+ return !this._isIgnored(path25, stat4);
12606
+ }
12607
+ /**
12608
+ * Provides a set of common helpers and properties relating to symlink handling.
12609
+ * @param path file or directory pattern being watched
12610
+ */
12611
+ _getWatchHelpers(path25) {
12612
+ return new WatchHelper(path25, this.options.followSymlinks, this);
12613
+ }
12614
+ // Directory helpers
12615
+ // -----------------
12616
+ /**
12617
+ * Provides directory tracking objects
12618
+ * @param directory path of the directory
12619
+ */
12620
+ _getWatchedDir(directory) {
12621
+ const dir = sp2.resolve(directory);
12622
+ if (!this._watched.has(dir))
12623
+ this._watched.set(dir, new DirEntry(dir, this._boundRemove));
12624
+ return this._watched.get(dir);
12625
+ }
12626
+ // File helpers
12627
+ // ------------
12628
+ /**
12629
+ * Check for read permissions: https://stackoverflow.com/a/11781404/1358405
12630
+ */
12631
+ _hasReadPermissions(stats) {
12632
+ if (this.options.ignorePermissionErrors)
12633
+ return true;
12634
+ return Boolean(Number(stats.mode) & 256);
12635
+ }
12636
+ /**
12637
+ * Handles emitting unlink events for
12638
+ * files and directories, and via recursion, for
12639
+ * files and directories within directories that are unlinked
12640
+ * @param directory within which the following item is located
12641
+ * @param item base path of item/directory
12642
+ */
12643
+ _remove(directory, item, isDirectory) {
12644
+ const path25 = sp2.join(directory, item);
12645
+ const fullPath = sp2.resolve(path25);
12646
+ isDirectory = isDirectory != null ? isDirectory : this._watched.has(path25) || this._watched.has(fullPath);
12647
+ if (!this._throttle("remove", path25, 100))
12648
+ return;
12649
+ if (!isDirectory && this._watched.size === 1) {
12650
+ this.add(directory, item, true);
12651
+ }
12652
+ const wp = this._getWatchedDir(path25);
12653
+ const nestedDirectoryChildren = wp.getChildren();
12654
+ nestedDirectoryChildren.forEach((nested) => this._remove(path25, nested));
12655
+ const parent = this._getWatchedDir(directory);
12656
+ const wasTracked = parent.has(item);
12657
+ parent.remove(item);
12658
+ if (this._symlinkPaths.has(fullPath)) {
12659
+ this._symlinkPaths.delete(fullPath);
12660
+ }
12661
+ let relPath = path25;
12662
+ if (this.options.cwd)
12663
+ relPath = sp2.relative(this.options.cwd, path25);
12664
+ if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
12665
+ const event = this._pendingWrites.get(relPath).cancelWait();
12666
+ if (event === EVENTS.ADD)
12667
+ return;
12668
+ }
12669
+ this._watched.delete(path25);
12670
+ this._watched.delete(fullPath);
12671
+ const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK;
12672
+ if (wasTracked && !this._isIgnored(path25))
12673
+ this._emit(eventName, path25);
12674
+ this._closePath(path25);
12675
+ }
12676
+ /**
12677
+ * Closes all watchers for a path
12678
+ */
12679
+ _closePath(path25) {
12680
+ this._closeFile(path25);
12681
+ const dir = sp2.dirname(path25);
12682
+ this._getWatchedDir(dir).remove(sp2.basename(path25));
12683
+ }
12684
+ /**
12685
+ * Closes only file-specific watchers
12686
+ */
12687
+ _closeFile(path25) {
12688
+ const closers = this._closers.get(path25);
12689
+ if (!closers)
12690
+ return;
12691
+ closers.forEach((closer) => closer());
12692
+ this._closers.delete(path25);
12693
+ }
12694
+ _addPathCloser(path25, closer) {
12695
+ if (!closer)
12696
+ return;
12697
+ let list = this._closers.get(path25);
12698
+ if (!list) {
12699
+ list = [];
12700
+ this._closers.set(path25, list);
12701
+ }
12702
+ list.push(closer);
12703
+ }
12704
+ _readdirp(root, opts) {
12705
+ if (this.closed)
12706
+ return;
12707
+ const options = { type: EVENTS.ALL, alwaysStat: true, lstat: true, ...opts, depth: 0 };
12708
+ let stream = readdirp(root, options);
12709
+ this._streams.add(stream);
12710
+ stream.once(STR_CLOSE, () => {
12711
+ stream = void 0;
12712
+ });
12713
+ stream.once(STR_END, () => {
12714
+ if (stream) {
12715
+ this._streams.delete(stream);
12716
+ stream = void 0;
12717
+ }
12718
+ });
12719
+ return stream;
12720
+ }
12721
+ };
12722
+ function watch(paths, options = {}) {
12723
+ const watcher = new FSWatcher(options);
12724
+ watcher.add(paths);
12725
+ return watcher;
12726
+ }
12727
+ var chokidar_default = { watch, FSWatcher };
12728
+
12729
+ // src/watcher/file-watcher.ts
12730
+ import * as path20 from "path";
12731
+ var FileWatcher = class {
12732
+ watcher = null;
12733
+ projectRoot;
12734
+ config;
12735
+ host;
12736
+ configPath;
12737
+ pendingChanges = /* @__PURE__ */ new Map();
12738
+ debounceTimer = null;
12739
+ debounceMs = 1e3;
12740
+ onChanges = null;
12741
+ constructor(projectRoot, config, host = "opencode", options = {}) {
12742
+ this.projectRoot = projectRoot;
12743
+ this.config = config;
12744
+ this.host = host;
12745
+ this.configPath = options.configPath;
12746
+ }
12747
+ start(handler) {
12748
+ if (this.watcher) {
12749
+ return;
12750
+ }
12751
+ this.onChanges = handler;
12752
+ const ignoreFilter = createIgnoreFilter(this.projectRoot);
12753
+ const watchTargets = this.configPath ? [this.projectRoot, this.configPath] : this.projectRoot;
12754
+ this.watcher = chokidar_default.watch(watchTargets, {
12755
+ ignored: (filePath) => {
12756
+ const relativePath = path20.relative(this.projectRoot, filePath);
12757
+ if (!relativePath) return false;
12758
+ if (this.isProjectConfigPathOrAncestor(relativePath)) {
12759
+ return false;
12760
+ }
12761
+ if (hasFilteredPathSegment(relativePath, path20.sep)) {
12762
+ return true;
12763
+ }
12764
+ if (isRestrictedDirectory(relativePath, path20.sep)) {
12765
+ return true;
12766
+ }
12767
+ if (ignoreFilter.ignores(relativePath)) {
12768
+ return true;
12769
+ }
12770
+ return false;
12771
+ },
12772
+ persistent: true,
12773
+ ignoreInitial: true,
12774
+ awaitWriteFinish: {
12775
+ stabilityThreshold: 300,
12776
+ pollInterval: 100
12777
+ }
12778
+ });
12779
+ this.watcher.on("error", (error) => {
12780
+ const err = error instanceof Error ? error : null;
12781
+ if (err?.code === "EPERM" || err?.code === "EACCES") {
12782
+ return;
12783
+ }
12784
+ console.error("[codebase-index] Watcher error:", err?.message ?? error);
12785
+ });
12786
+ this.watcher.on("add", (filePath) => this.handleChange("add", filePath));
12787
+ this.watcher.on("change", (filePath) => this.handleChange("change", filePath));
12788
+ this.watcher.on("unlink", (filePath) => this.handleChange("unlink", filePath));
12789
+ }
12790
+ handleChange(type, filePath) {
12791
+ if (this.isProjectConfigPath(filePath)) {
12792
+ this.pendingChanges.set(filePath, type);
12793
+ this.scheduleFlush();
12794
+ return;
12795
+ }
12796
+ const includePatterns = [...this.config.include, ...this.config.additionalInclude ?? []];
12797
+ if (!shouldIncludeFile(
12798
+ filePath,
12799
+ this.projectRoot,
12800
+ includePatterns,
12801
+ this.config.exclude,
12802
+ createIgnoreFilter(this.projectRoot)
12803
+ )) {
12804
+ return;
12805
+ }
12806
+ this.pendingChanges.set(filePath, type);
12807
+ this.scheduleFlush();
12808
+ }
12809
+ isProjectConfigPath(filePath) {
12810
+ const relativePath = path20.relative(this.projectRoot, filePath);
12811
+ const normalizedRelativePath = path20.normalize(relativePath);
12812
+ return this.getProjectConfigRelativePaths().some((configPath) => configPath === normalizedRelativePath);
12813
+ }
12814
+ isProjectConfigPathOrAncestor(relativePath) {
12815
+ const normalizedRelativePath = path20.normalize(relativePath);
12816
+ return this.getProjectConfigRelativePaths().some(
12817
+ (configPath) => configPath === normalizedRelativePath || configPath.startsWith(`${normalizedRelativePath}${path20.sep}`)
12818
+ );
12819
+ }
12820
+ getProjectConfigRelativePaths() {
12821
+ if (this.configPath) {
12822
+ return [path20.normalize(path20.relative(this.projectRoot, this.configPath))];
12823
+ }
12824
+ return [
12825
+ resolveProjectConfigPath(this.projectRoot, this.host),
12826
+ resolveWritableProjectConfigPath(this.projectRoot, this.host)
12827
+ ].map((configPath) => path20.normalize(path20.relative(this.projectRoot, configPath)));
12828
+ }
12829
+ scheduleFlush() {
12830
+ if (this.debounceTimer) {
12831
+ clearTimeout(this.debounceTimer);
12832
+ }
12833
+ this.debounceTimer = setTimeout(() => {
12834
+ this.flush();
12835
+ }, this.debounceMs);
12836
+ }
12837
+ async flush() {
12838
+ if (this.pendingChanges.size === 0 || !this.onChanges) {
12839
+ return;
12840
+ }
12841
+ const changes = Array.from(this.pendingChanges.entries()).map(
12842
+ ([path25, type]) => ({ path: path25, type })
12843
+ );
12844
+ this.pendingChanges.clear();
12845
+ try {
12846
+ await this.onChanges(changes);
12847
+ } catch (error) {
12848
+ console.error("Error handling file changes:", error);
12849
+ }
12850
+ }
12851
+ stop() {
12852
+ if (this.debounceTimer) {
12853
+ clearTimeout(this.debounceTimer);
12854
+ this.debounceTimer = null;
12855
+ }
12856
+ if (this.watcher) {
12857
+ this.watcher.close();
12858
+ this.watcher = null;
12859
+ }
12860
+ this.pendingChanges.clear();
12861
+ this.onChanges = null;
12862
+ }
12863
+ isRunning() {
12864
+ return this.watcher !== null;
12865
+ }
12866
+ };
12867
+
12868
+ // src/watcher/git-head-watcher.ts
12869
+ import * as path21 from "path";
12870
+ var GitHeadWatcher = class {
12871
+ watcher = null;
12872
+ projectRoot;
12873
+ currentBranch = null;
12874
+ onBranchChange = null;
12875
+ debounceTimer = null;
12876
+ debounceMs = 100;
12877
+ // Short debounce for git operations
12878
+ constructor(projectRoot) {
12879
+ this.projectRoot = projectRoot;
12880
+ }
12881
+ start(handler) {
12882
+ if (this.watcher) {
12883
+ return;
12884
+ }
12885
+ if (!isGitRepo(this.projectRoot)) {
12886
+ return;
12887
+ }
12888
+ this.onBranchChange = handler;
12889
+ this.currentBranch = getCurrentBranch(this.projectRoot);
12890
+ const headPath = getHeadPath(this.projectRoot);
12891
+ const refsPath = path21.join(this.projectRoot, ".git", "refs", "heads");
12892
+ this.watcher = chokidar_default.watch([headPath, refsPath], {
12893
+ persistent: true,
12894
+ ignoreInitial: true,
12895
+ awaitWriteFinish: {
12896
+ stabilityThreshold: 50,
12897
+ pollInterval: 10
12898
+ }
12899
+ });
12900
+ this.watcher.on("change", () => this.handleHeadChange());
12901
+ this.watcher.on("add", () => this.handleHeadChange());
12902
+ }
12903
+ handleHeadChange() {
12904
+ if (this.debounceTimer) {
12905
+ clearTimeout(this.debounceTimer);
12906
+ }
12907
+ this.debounceTimer = setTimeout(() => {
12908
+ this.checkBranchChange();
12909
+ }, this.debounceMs);
12910
+ }
12911
+ async checkBranchChange() {
12912
+ const newBranch = getCurrentBranch(this.projectRoot);
12913
+ if (newBranch && newBranch !== this.currentBranch && this.onBranchChange) {
12914
+ const oldBranch = this.currentBranch;
12915
+ this.currentBranch = newBranch;
12916
+ try {
12917
+ await this.onBranchChange(oldBranch, newBranch);
12918
+ } catch (error) {
12919
+ console.error("Error handling branch change:", error);
12920
+ }
12921
+ } else if (newBranch) {
12922
+ this.currentBranch = newBranch;
12923
+ }
12924
+ }
12925
+ getCurrentBranch() {
12926
+ return this.currentBranch;
12927
+ }
12928
+ stop() {
12929
+ if (this.debounceTimer) {
12930
+ clearTimeout(this.debounceTimer);
12931
+ this.debounceTimer = null;
12932
+ }
12933
+ if (this.watcher) {
12934
+ this.watcher.close();
12935
+ this.watcher = null;
12936
+ }
12937
+ this.onBranchChange = null;
12938
+ }
12939
+ isRunning() {
12940
+ return this.watcher !== null;
12941
+ }
12942
+ };
12943
+
12944
+ // src/watcher/index.ts
12945
+ var BackgroundReindexer = class {
12946
+ constructor(runIndex) {
12947
+ this.runIndex = runIndex;
12948
+ }
12949
+ runIndex;
12950
+ running = false;
12951
+ pending = false;
12952
+ stopped = false;
12953
+ request() {
12954
+ if (this.stopped) {
12955
+ return;
12956
+ }
12957
+ this.pending = true;
12958
+ this.drain();
12959
+ }
12960
+ stop() {
12961
+ this.stopped = true;
12962
+ this.pending = false;
12963
+ }
12964
+ drain() {
12965
+ if (this.stopped || this.running || !this.pending) {
12966
+ return;
12967
+ }
12968
+ this.pending = false;
12969
+ this.running = true;
12970
+ void this.run();
12971
+ }
12972
+ async run() {
12973
+ try {
12974
+ await this.runIndex();
12975
+ } catch (error) {
12976
+ console.error("[codebase-index] Background reindex failed:", error);
12977
+ } finally {
12978
+ this.running = false;
12979
+ this.drain();
12980
+ }
12981
+ }
12982
+ };
12983
+ function createWatcherWithIndexer(getIndexer, projectRoot, config, host = "opencode", options = {}) {
12984
+ const fileWatcher = new FileWatcher(projectRoot, config, host, options);
12985
+ const backgroundReindexer = new BackgroundReindexer(async () => {
12986
+ await getIndexer().index();
12987
+ });
12988
+ fileWatcher.start(async (changes) => {
12989
+ const hasAddOrChange = changes.some(
12990
+ (c) => c.type === "add" || c.type === "change"
12991
+ );
12992
+ const hasDelete = changes.some((c) => c.type === "unlink");
12993
+ if (hasAddOrChange || hasDelete) {
12994
+ const configPaths = getConfigPaths(projectRoot, host, options);
12995
+ if (changes.some((change) => configPaths.includes(pathNormalize(change.path)))) {
12996
+ const parsedConfig = options.configPath ? parseConfig(loadConfigFile(options.configPath)) : void 0;
12997
+ refreshIndexerForDirectory(projectRoot, host, parsedConfig);
12998
+ }
12999
+ backgroundReindexer.request();
13000
+ }
13001
+ });
13002
+ let gitWatcher = null;
13003
+ if (isGitRepo(projectRoot)) {
13004
+ gitWatcher = new GitHeadWatcher(projectRoot);
13005
+ gitWatcher.start(async (oldBranch, newBranch) => {
13006
+ console.log(`Branch changed: ${oldBranch ?? "(none)"} -> ${newBranch}`);
13007
+ backgroundReindexer.request();
13008
+ });
13009
+ }
13010
+ return {
13011
+ fileWatcher,
13012
+ gitWatcher,
13013
+ stop() {
13014
+ backgroundReindexer.stop();
13015
+ fileWatcher.stop();
13016
+ gitWatcher?.stop();
13017
+ }
13018
+ };
13019
+ }
13020
+ function pathNormalize(value) {
13021
+ return value.split("\\").join("/");
13022
+ }
13023
+ function getConfigPaths(projectRoot, host, options) {
13024
+ if (options.configPath) {
13025
+ return [pathNormalize(options.configPath)];
13026
+ }
13027
+ return [
13028
+ resolveProjectConfigPath(projectRoot, host),
13029
+ resolveWritableProjectConfigPath(projectRoot, host)
13030
+ ].map((configPath) => pathNormalize(configPath));
13031
+ }
13032
+
13033
+ // src/tools/visualize/activity.ts
13034
+ import { execFileSync } from "child_process";
13035
+ import * as path22 from "path";
13036
+ function attachRecentActivity(data, projectRoot) {
13037
+ const activity = readGitActivity(projectRoot);
13038
+ const changes = activity.size > 0 ? buildGitChanges(data, activity, projectRoot) : buildGraphChanges(data);
13039
+ return {
13040
+ ...data,
13041
+ changes
13042
+ };
13043
+ }
13044
+ function readGitActivity(projectRoot) {
13045
+ try {
13046
+ const output = execFileSync(
13047
+ "git",
13048
+ ["-C", projectRoot, "log", "--since=90.days", "--numstat", "--date=short", "--pretty=format:__COMMIT__%x09%h%x09%ad%x09%s"],
13049
+ { encoding: "utf8", maxBuffer: 8 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"] }
13050
+ );
13051
+ return parseGitActivity(output);
13052
+ } catch {
13053
+ return /* @__PURE__ */ new Map();
13054
+ }
13055
+ }
13056
+ function parseGitActivity(output) {
13057
+ const activity = /* @__PURE__ */ new Map();
13058
+ let latestHash = "";
13059
+ let latestDate = "";
13060
+ let latestSubject = "";
13061
+ for (const line of output.split(/\r?\n/)) {
13062
+ if (!line) continue;
13063
+ if (line.startsWith("__COMMIT__ ")) {
13064
+ const [, hash = "", date = "", subject = ""] = line.split(" ");
13065
+ latestHash = hash;
13066
+ latestDate = date;
13067
+ latestSubject = subject;
13068
+ continue;
13069
+ }
13070
+ const [addedRaw, deletedRaw, filePath] = line.split(" ");
13071
+ if (!filePath || addedRaw === "-" || deletedRaw === "-") continue;
13072
+ const churn = Number(addedRaw) + Number(deletedRaw);
13073
+ if (!Number.isFinite(churn) || churn <= 0) continue;
13074
+ const normalizedPath = normalizePath3(filePath);
13075
+ const previous = activity.get(normalizedPath);
13076
+ activity.set(normalizedPath, {
13077
+ churn: (previous?.churn ?? 0) + churn,
13078
+ commits: (previous?.commits ?? 0) + 1,
13079
+ latestDate: previous?.latestDate ?? latestDate,
13080
+ latestHash: previous?.latestHash ?? latestHash,
13081
+ latestSubject: previous?.latestSubject ?? latestSubject
13082
+ });
13083
+ }
13084
+ return activity;
13085
+ }
13086
+ function buildGitChanges(data, activity, projectRoot) {
13087
+ const byModule = /* @__PURE__ */ new Map();
13088
+ for (const node of data.nodes) {
13089
+ const fileActivity = activity.get(toGitRelativePath(projectRoot, node.filePath));
13090
+ if (!fileActivity) continue;
13091
+ const current = byModule.get(node.moduleId) ?? {
13092
+ moduleId: node.moduleId,
13093
+ filePaths: /* @__PURE__ */ new Set(),
13094
+ churn: 0,
13095
+ commits: 0,
13096
+ latestDate: fileActivity.latestDate,
13097
+ latestHash: fileActivity.latestHash,
13098
+ latestSubject: fileActivity.latestSubject
13099
+ };
13100
+ if (!current.filePaths.has(node.filePath)) {
13101
+ current.filePaths.add(node.filePath);
13102
+ current.churn += fileActivity.churn;
13103
+ current.commits += fileActivity.commits;
13104
+ }
13105
+ if (fileActivity.latestDate > current.latestDate) {
13106
+ current.latestDate = fileActivity.latestDate;
13107
+ current.latestHash = fileActivity.latestHash;
13108
+ current.latestSubject = fileActivity.latestSubject;
13109
+ }
13110
+ byModule.set(node.moduleId, current);
13111
+ }
13112
+ return [...byModule.values()].sort((a, b) => scoreModule(data, b.moduleId, b.churn) - scoreModule(data, a.moduleId, a.churn)).slice(0, 12).map((item, index) => toGitChange(data, item, index));
13113
+ }
13114
+ function buildGraphChanges(data) {
13115
+ return data.modules.map((module2) => {
13116
+ const calls = moduleCallCount(data, module2.id);
13117
+ const focusNode = strongestNode(data, module2.id);
13118
+ const risk = riskFor(calls, module2.symbolCount);
13119
+ return {
13120
+ id: `graph-${module2.id}`,
13121
+ title: `${module2.label} is structurally important`,
13122
+ kind: risk === "high" ? "risk" : "hot",
13123
+ when: "graph-derived",
13124
+ source: "call graph",
13125
+ intent: "load-bearing path",
13126
+ summary: `${module2.symbolCount} symbols with ${calls} cross-module calls in this slice.`,
13127
+ why: "No recent Git history was available, so this highlights modules whose call relationships make them expensive to misunderstand during onboarding.",
13128
+ calls,
13129
+ churn: 0,
13130
+ risk,
13131
+ moduleId: module2.id,
13132
+ focusNodeId: focusNode?.id,
13133
+ filePaths: [...new Set(data.nodes.filter((node) => node.moduleId === module2.id).map((node) => node.filePath))].slice(0, 6)
13134
+ };
13135
+ }).sort((a, b) => b.calls - a.calls).slice(0, 8);
13136
+ }
13137
+ function toGitChange(data, item, index) {
13138
+ const module2 = data.modules.find((candidate) => candidate.id === item.moduleId);
13139
+ const label = module2?.label ?? item.moduleId;
13140
+ const calls = moduleCallCount(data, item.moduleId);
13141
+ const focusNode = strongestNode(data, item.moduleId);
13142
+ const risk = riskFor(calls, item.churn);
13143
+ const intent = inferIntent(item.latestSubject);
13144
+ return {
13145
+ id: `git-${item.moduleId}-${index}`,
13146
+ title: `${label} moved recently`,
13147
+ kind: risk === "high" ? "risk" : "hot",
13148
+ when: item.latestDate || "recently",
13149
+ source: item.latestHash ? `commit ${item.latestHash}` : "git history",
13150
+ intent,
13151
+ summary: `${item.churn} changed lines across ${item.filePaths.size} indexed files. Latest: ${item.latestSubject || "no commit subject"}.`,
13152
+ why: `${label} is both changing and connected to ${calls} call edges. Start here to understand whether the current graph is new work, load-bearing behavior, or legacy surface area.`,
13153
+ calls,
13154
+ churn: item.churn,
13155
+ risk,
13156
+ moduleId: item.moduleId,
13157
+ focusNodeId: focusNode?.id,
13158
+ filePaths: [...item.filePaths].slice(0, 8)
13159
+ };
13160
+ }
13161
+ function scoreModule(data, moduleId, churn) {
13162
+ return churn + moduleCallCount(data, moduleId) * 4;
13163
+ }
13164
+ function moduleCallCount(data, moduleId) {
13165
+ const moduleEdgeCount = data.moduleEdges.filter((edge) => edge.source === moduleId || edge.target === moduleId).reduce((total, edge) => total + edge.weight, 0);
13166
+ if (moduleEdgeCount > 0) return moduleEdgeCount;
13167
+ const nodeModules = new Map(data.nodes.map((node) => [node.id, node.moduleId]));
13168
+ return data.edges.filter((edge) => nodeModules.get(edge.source) === moduleId || nodeModules.get(edge.target) === moduleId).length;
13169
+ }
13170
+ function strongestNode(data, moduleId) {
13171
+ const degree = /* @__PURE__ */ new Map();
13172
+ for (const edge of data.edges) {
13173
+ degree.set(edge.source, (degree.get(edge.source) ?? 0) + 1);
13174
+ degree.set(edge.target, (degree.get(edge.target) ?? 0) + 1);
13175
+ }
13176
+ return data.nodes.filter((node) => node.moduleId === moduleId).sort((a, b) => (degree.get(b.id) ?? 0) - (degree.get(a.id) ?? 0))[0];
13177
+ }
13178
+ function riskFor(calls, churnOrSymbols) {
13179
+ if (calls >= 20 || churnOrSymbols >= 250) return "high";
13180
+ if (calls >= 8 || churnOrSymbols >= 80) return "medium";
13181
+ return "low";
13182
+ }
13183
+ function inferIntent(subject) {
13184
+ const normalized = subject.toLowerCase();
13185
+ if (normalized.includes("fix")) return "stability";
13186
+ if (normalized.includes("test")) return "verification";
13187
+ if (normalized.includes("refactor")) return "refactor";
13188
+ if (normalized.includes("visual")) return "visualization";
13189
+ if (normalized.includes("call") || normalized.includes("graph")) return "call graph";
13190
+ if (normalized.includes("config")) return "configuration";
13191
+ return "recent work";
13192
+ }
13193
+ function normalizePath3(filePath) {
13194
+ return filePath.replace(/\\/g, "/");
13195
+ }
13196
+ function toGitRelativePath(projectRoot, filePath) {
13197
+ const relativePath = path22.isAbsolute(filePath) ? path22.relative(projectRoot, filePath) : filePath;
13198
+ return normalizePath3(relativePath);
13199
+ }
13200
+
13201
+ // src/tools/visualize/template.ts
13202
+ function generateVisualizationHtml(data) {
13203
+ const jsonData = JSON.stringify(data).replace(/</g, "\\u003c").replace(/>/g, "\\u003e");
13204
+ return `<!DOCTYPE html>
13205
+ <html lang="en">
13206
+ <head>
13207
+ <meta charset="UTF-8">
13208
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
13209
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline'; script-src 'unsafe-inline'">
13210
+ <title>Call Graph Visualization</title>
13211
+ <style>
13212
+ *{box-sizing:border-box}body{margin:0;background:#0d1118;color:#c4cfe6;font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;overflow:hidden}
13213
+ #app{height:100vh;display:grid;grid-template-columns:300px minmax(0,1fr) 320px;grid-template-rows:auto 1fr;gap:12px;padding:14px}
13214
+ .top{grid-column:1/-1;display:flex;align-items:center;gap:10px;min-width:0}.search{width:300px;max-width:40vw;padding:8px 11px;border:1px solid #203149;border-radius:6px;background:#141e2e;color:#c4cfe6;outline:none}.search:focus{border-color:#5577d8}
13215
+ .tabs{display:flex;gap:6px;min-width:0;overflow:auto}.tab{border:1px solid #203149;background:#141e2e;color:#6f83a4;border-radius:6px;padding:7px 10px;cursor:pointer;white-space:nowrap}.tab.active{border-color:#5577d8;color:#fff;background:#111c30}
13216
+ .stats{margin-left:auto;color:#334966;font-size:12px;white-space:nowrap;border:1px solid #203149;background:#141e2e;border-radius:6px;padding:7px 11px}.stats b{color:#7f94b8}
13217
+ .panel{background:rgba(13,17,24,.86);border:1px solid #203149;border-radius:8px;min-height:0}.left,.right{padding:14px;overflow:auto;overflow-x:hidden}.title{font-size:10px;text-transform:uppercase;letter-spacing:.1em;color:#334966;margin:0 0 10px}
13218
+ .results,.list{display:grid;gap:7px;min-width:0}.result,.row,.node,.change{min-width:0;border:1px solid #203149;background:#101722;border-radius:7px;padding:9px 10px;cursor:pointer;overflow:hidden}.result:hover,.row:hover,.node:hover,.change:hover{border-color:#375171;background:#121c2a}.result.active,.node.active,.change.active{border-color:#8b6cf6;background:#15172a}
13219
+ .name{font-family:"SF Mono","Cascadia Code",Consolas,monospace;font-size:12px;color:#e0e6f7;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.meta{min-width:0;max-width:100%;font-size:11px;color:#6f83a4;margin-top:4px;display:flex;gap:8px;align-items:center;overflow:hidden}.meta span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.badge{font-family:"SF Mono",Consolas,monospace;font-size:10px;border-radius:4px;padding:2px 5px;background:#1b2940;color:#7890ac;flex:0 0 auto}.badge.function,.badge.fn{background:#18264a;color:#7896ff}.badge.class{background:#271f4b;color:#a48cff}.badge.type,.badge.interface{background:#172d36;color:#5fc4d4}.badge.enum{background:#382812;color:#e3a348}
13220
+ .main{position:relative;overflow:hidden;padding:14px}.flow{position:relative;height:100%;display:grid;grid-template-columns:minmax(220px,1fr) minmax(300px,1.15fr) minmax(220px,1fr);gap:24px;align-items:center}.lane{height:min(640px,calc(100vh - 150px));min-height:360px;border:1px solid #203149;border-radius:8px;padding:14px;display:flex;flex-direction:column;gap:10px;overflow:auto;background:rgba(16,23,34,.5)}.lane .node,.lane .row{flex:0 0 auto}.lane h3,.module-symbols h3{margin:0 0 4px;font-size:10px;text-transform:uppercase;letter-spacing:.1em;color:#334966;text-align:center}.center{align-self:center;min-width:0}.center .node{cursor:default;border-color:#8b6cf6;background:#15172a;box-shadow:0 0 0 1px rgba(139,108,246,.2)}.module-board{height:100%;display:grid;grid-template-rows:auto 260px minmax(260px,1fr);gap:12px;overflow:hidden}.module-summary .node{cursor:default;border-color:#8b6cf6;background:#15172a}.module-lanes{min-height:0;display:grid;grid-template-columns:1fr 1fr;gap:12px}.module-board .lane{height:auto;min-height:0}.module-symbols{min-height:0;border:1px solid #203149;border-radius:8px;padding:14px;background:rgba(16,23,34,.5);display:flex;flex-direction:column;gap:10px;overflow:hidden}.module-symbols .list{display:block;overflow:auto;min-height:0}.module-symbols .row{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;margin:0 0 6px;padding:8px 10px}.module-symbols .row .meta{margin-top:0;justify-content:flex-end}.weight{margin-left:auto;color:#334966;font-variant-numeric:tabular-nums}.edges{position:absolute;inset:0;pointer-events:none;z-index:1}.node{position:relative;z-index:2}
13221
+ .details{display:grid;gap:8px}.kv{display:flex;justify-content:space-between;gap:12px;border-bottom:1px solid #17243a;padding-bottom:7px;color:#6f83a4;font-size:12px}.kv span:first-child{color:#334966;text-transform:uppercase;font-size:10px;letter-spacing:.08em}.mini{display:flex;align-items:center;gap:8px;color:#6f83a4;font-size:12px}.dot{width:8px;height:8px;border-radius:50%;flex:0 0 auto}.empty{height:100%;display:grid;place-items:center;color:#334966;font-size:12px;text-align:center;line-height:1.5}.guide{border:1px solid #203149;border-radius:7px;background:#101722;padding:10px;color:#6f83a4;font-size:12px;line-height:1.45}.guide b{display:block;color:#e0e6f7;margin-bottom:4px}.guide span{display:block}
13222
+ .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:10px;align-content:start}.rank{font-size:20px;color:#8b6cf6;font-variant-numeric:tabular-nums}.change-grid{display:grid;grid-template-columns:minmax(280px,.95fr) minmax(320px,1.2fr);gap:12px;height:100%;min-height:0}.change-list,.why{display:flex;flex-direction:column;gap:9px;min-height:0;overflow:auto;overflow-x:hidden}.change-top{display:flex;align-items:center;gap:8px;margin-bottom:7px;min-width:0}.pill{font-size:10px;text-transform:uppercase;letter-spacing:.08em;border:1px solid #203149;border-radius:999px;padding:3px 7px;color:#6f83a4;flex:0 0 auto}.pill.hot{color:#d5c9ff;border-color:#634bc2}.pill.risk{color:#ffbab7;border-color:#8d3d3a}.pill.legacy{color:#c5d1df;border-color:#41536c}.why-card{min-width:0;border:1px solid #203149;background:rgba(16,23,34,.65);border-radius:8px;padding:14px;overflow:hidden}.why-card h3{margin:0 0 8px;font-size:13px}.why-card p{margin:0;color:#6f83a4;font-size:13px;line-height:1.55}.impact{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:10px}.impact div{border:1px solid #17243a;border-radius:7px;padding:8px}.impact b{display:block;font-size:16px;color:#e0e6f7}.impact span{font-size:10px;color:#334966;text-transform:uppercase;letter-spacing:.08em}
13223
+ .hint{position:absolute;left:50%;bottom:12px;transform:translateX(-50%);font-size:11px;color:#334966;border:1px solid #203149;background:rgba(13,17,24,.9);border-radius:6px;padding:6px 12px;white-space:nowrap}
13224
+ @media(max-width:1500px){body{overflow:auto}#app{height:auto;min-height:100vh;grid-template-columns:1fr;grid-template-rows:auto auto auto auto}.top{flex-wrap:wrap}.tabs{flex-wrap:wrap;overflow:visible}.search{width:100%;max-width:none}.stats{margin-left:0;width:100%}.main{order:1;min-height:820px}.right{order:2}.left{order:3}.flow{grid-template-columns:minmax(220px,1fr) minmax(300px,1.15fr) minmax(220px,1fr);gap:16px;align-items:center}.change-grid{grid-template-columns:1fr;gap:12px;align-items:stretch}.why{order:1}.change-list{order:2}.lane{height:min(560px,calc(100vh - 180px));min-height:260px}.impact{grid-template-columns:1fr}.edges{display:none}.hint{position:static;transform:none;margin-top:10px;text-align:center}}
13225
+ @media(max-width:900px){.flow,.module-lanes{grid-template-columns:1fr;gap:12px;align-items:stretch}.lane{height:auto;max-height:360px;min-height:160px}.module-board{grid-template-rows:auto auto minmax(260px,1fr);overflow:visible}.module-symbols{max-height:420px}.module-symbols .row{grid-template-columns:1fr}.module-symbols .row .meta{justify-content:flex-start;margin-top:4px}}
13226
+ </style>
13227
+ </head>
13228
+ <body>
13229
+ <div id="app">
13230
+ <div class="top">
13231
+ <input id="search" class="search" placeholder="Search changes, symbols, modules, files..." autocomplete="off">
13232
+ <div class="tabs" id="tabs"></div>
13233
+ <div class="stats"><b id="modeName">Changes</b> | <b id="nodeCount">0</b> nodes | <b id="edgeCount">0</b> edges | <b id="changeCount">0</b> change lenses</div>
13234
+ </div>
13235
+ <aside class="panel left"><h2 class="title">Search / jump</h2><div id="results" class="results"></div></aside>
13236
+ <main class="panel main"><svg id="edges" class="edges"></svg><div id="stage" class="flow"></div><div class="hint" id="hint">Scroll to pan vertically inside focus mode</div></main>
13237
+ <aside class="panel right"><h2 class="title">Details</h2><div id="details" class="details"></div></aside>
13238
+ </div>
13239
+ <script>
13240
+ const graphData = ${jsonData};
13241
+ const nodes = graphData.nodes || [];
13242
+ const edges = graphData.edges || [];
13243
+ const modules = graphData.modules || [];
13244
+ const moduleEdges = graphData.moduleEdges || [];
13245
+ const changes = graphData.changes || [];
13246
+ const modes = ["Changes", "Module Overview", "Explore Symbols", "Hotspots", "Cycles"];
13247
+ let mode = changes.length > 0 ? "Changes" : "Module Overview";
13248
+ let selected = nodes[0] ? { type: "symbol", id: nodes[0].id } : { type: "module", id: modules[0] && modules[0].id };
13249
+ let selectedChange = changes[0] && changes[0].id;
13250
+ let query = "";
13251
+
13252
+ function esc(value){return String(value == null ? "" : value).replace(/[&<>"']/g,function(c){return {"&":"&amp;","<":"&lt;",">":"&gt;","\\"":"&quot;","'":"&#39;"}[c];});}
13253
+ function nodeById(id){return nodes.find(function(node){return node.id === id;});}
13254
+ function moduleById(id){return modules.find(function(item){return item.id === id;});}
13255
+ function colorFor(id){const moduleId = nodeById(id) ? nodeById(id).moduleId : id; const index = Math.max(0, modules.findIndex(function(item){return item.id === moduleId;})); return ["#8b6cf6","#5577d8","#4aa46c","#ce6764","#d0933a","#41a6bd","#c667a4","#7890ac"][index % 8];}
13256
+ function shortPath(filePath){const parts = String(filePath || "").split(/[\\\\/]/).filter(Boolean); if(parts.length <= 3)return String(filePath || ""); return ".../" + parts.slice(-3).join("/");}
13257
+ function badge(kind){return '<span class="badge ' + esc(kind) + '">' + esc(kind) + '</span>';}
13258
+ function edgeWeight(edge){return edge.weight || 1;}
13259
+ function incomingSymbol(id){return edges.filter(function(edge){return edge.target === id;}).sort(function(a,b){return edgeWeight(b)-edgeWeight(a);});}
13260
+ function outgoingSymbol(id){return edges.filter(function(edge){return edge.source === id;}).sort(function(a,b){return edgeWeight(b)-edgeWeight(a);});}
13261
+ function incomingModule(id){return moduleEdges.filter(function(edge){return edge.target === id;}).sort(function(a,b){return b.weight-a.weight;});}
13262
+ function outgoingModule(id){return moduleEdges.filter(function(edge){return edge.source === id;}).sort(function(a,b){return b.weight-a.weight;});}
13263
+ function labelOf(id){const node = nodeById(id); if(node)return node.name; const mod = moduleById(id); return mod ? mod.label : id;}
13264
+ function nodeCard(title, meta, id, type, color, active){return '<div class="node ' + (active ? 'active' : '') + '" data-id="' + esc(id || '') + '" data-type="' + esc(type || 'symbol') + '" style="border-color:' + esc(color || "#203149") + '"><div class="name">' + esc(title) + '</div><div class="meta">' + meta + '</div></div>';}
13265
+ function symbolCard(id, weight){const node = nodeById(id); if(!node)return ""; return nodeCard(node.name, badge(node.kind) + '<span>' + esc(shortPath(node.filePath)) + '</span><span class="weight">' + weight + '</span>', node.id, "symbol", colorFor(node.id), false);}
13266
+ function moduleCard(id, weight){const item = moduleById(id); if(!item)return ""; return nodeCard(item.label, '<span>' + item.symbolCount + ' symbols</span><span class="weight">' + weight + '</span>', item.id, "module", colorFor(item.id), false);}
13267
+ function empty(a,b){return '<div class="empty"><div>' + esc(a) + '<br>' + esc(b) + '</div></div>';}
13268
+ function emptyCycleState(){return '<div class="empty"><div><b>No cycles found</b><br>This graph slice has no resolved module loops or symbol recursion.<br><br>A cycle means A calls B, B calls C, and C calls A.</div></div>';}
13269
+ function guideForMode(){
13270
+ if(mode === "Module Overview")return '<div class="guide"><b>How to read</b><span>Module = code area. Symbol = named code item. Counts on caller/callee cards are call edges. Grouped symbol rows collapse repeated indexed ranges.</span></div>';
13271
+ if(mode === "Explore Symbols")return '<div class="guide"><b>How to read</b><span>Focused symbol is in the middle. Left calls it. Right is what it calls.</span></div>';
13272
+ if(mode === "Hotspots")return '<div class="guide"><b>How to read</b><span>Higher score means more incoming plus outgoing call edges, often more load-bearing.</span></div>';
13273
+ if(mode === "Cycles")return '<div class="guide"><b>How to read</b><span>Cycles include module dependency loops and direct or short symbol recursion.</span></div>';
13274
+ return '<div class="guide"><b>How to read</b><span>Recent Git movement is shown first; open a change to see affected module, churn, and call context.</span></div>';
13275
+ }
13276
+ function groupedSymbolCards(edgeList, pickId, limit){
13277
+ const groups = new Map();
13278
+ edgeList.forEach(function(edge){const id = pickId(edge); const node = nodeById(id); if(!node)return; const key = node.name + "|" + node.kind + "|" + node.filePath; const group = groups.get(key) || { id:id, count:0 }; group.count += edgeWeight(edge); groups.set(key, group);});
13279
+ return [...groups.values()].sort(function(a,b){return b.count-a.count;}).slice(0, limit || groups.size).map(function(group){return symbolCard(group.id, group.count);}).join("");
13280
+ }
13281
+ function groupedModuleSymbolRows(symbols){
13282
+ const groups = new Map();
13283
+ symbols.forEach(function(node){
13284
+ const key = node.name + "|" + node.kind + "|" + node.filePath;
13285
+ const group = groups.get(key) || { id:node.id, node:node, count:0, minLine:node.line, maxLine:node.line };
13286
+ group.count += 1;
13287
+ group.minLine = Math.min(group.minLine, node.line);
13288
+ group.maxLine = Math.max(group.maxLine, node.line);
13289
+ groups.set(key, group);
13290
+ });
13291
+ return [...groups.values()].map(function(group){
13292
+ const lineText = group.minLine === group.maxLine ? "line " + group.minLine : "lines " + group.minLine + "-" + group.maxLine;
13293
+ const countText = group.count > 1 ? '<span>x' + group.count + '</span>' : "";
13294
+ return '<div class="row" data-id="' + esc(group.id) + '" data-type="symbol"><div class="name">' + esc(group.node.name) + '</div><div class="meta">' + badge(group.node.kind) + '<span>' + esc(shortPath(group.node.filePath)) + '</span><span>' + esc(lineText) + '</span>' + countText + '</div></div>';
13295
+ }).join("");
13296
+ }
13297
+ function findSymbolCycles(limit){
13298
+ const outgoing = new Map();
13299
+ edges.forEach(function(edge){if(edge.source === edge.target)return; const list = outgoing.get(edge.source) || []; list.push(edge.target); outgoing.set(edge.source, list);});
13300
+ const cycles = [];
13301
+ const seen = new Set();
13302
+ edges.filter(function(edge){return edge.source === edge.target;}).forEach(function(edge){
13303
+ if(!nodeById(edge.source))return;
13304
+ seen.add(edge.source + ">" + edge.source);
13305
+ cycles.push({ ids:[edge.source], kind:"direct recursion" });
13306
+ });
13307
+ nodes.some(function(start){
13308
+ const stack = [{ id:start.id, path:[start.id] }];
13309
+ while(stack.length > 0 && cycles.length < limit){
13310
+ const current = stack.pop();
13311
+ (outgoing.get(current.id) || []).forEach(function(next){
13312
+ if(cycles.length >= limit)return;
13313
+ if(next === start.id && current.path.length > 1){
13314
+ const key = current.path.slice().sort().join(">");
13315
+ if(!seen.has(key)){seen.add(key); cycles.push({ ids:current.path.slice(), kind:"symbol loop" });}
13316
+ } else if(current.path.length < 5 && current.path.indexOf(next) === -1) {
13317
+ stack.push({ id:next, path:current.path.concat(next) });
13318
+ }
13319
+ });
13320
+ }
13321
+ return cycles.length >= limit;
13322
+ });
13323
+ return cycles.slice(0, limit);
13324
+ }
13325
+
13326
+ function renderTabs(){
13327
+ document.getElementById("tabs").innerHTML = modes.map(function(item){return '<button class="tab ' + (item === mode ? 'active' : '') + '" data-mode="' + esc(item) + '">' + esc(item) + '</button>';}).join("");
13328
+ document.querySelectorAll("[data-mode]").forEach(function(button){button.onclick = function(){mode = button.dataset.mode || "Changes"; render();};});
13329
+ document.getElementById("modeName").textContent = mode;
13330
+ document.getElementById("nodeCount").textContent = String(nodes.length);
13331
+ document.getElementById("edgeCount").textContent = String(edges.length);
13332
+ document.getElementById("changeCount").textContent = String(changes.length);
13333
+ }
13334
+ function renderResults(){
13335
+ const q = query.toLowerCase();
13336
+ const items = changes.map(function(change){return {type:"change",id:change.id,title:change.title,meta:change.source + " | " + change.when + " | " + change.intent,color:change.kind === "risk" ? "#ce6764" : "#8b6cf6"};})
13337
+ .concat(modules.map(function(item){return {type:"module",id:item.id,title:item.label,meta:item.symbolCount + " symbols | " + item.category,color:colorFor(item.id)};}))
13338
+ .concat(nodes.map(function(node){return {type:"symbol",id:node.id,title:node.name,meta:node.kind + " | " + shortPath(node.filePath),color:colorFor(node.id)};}))
13339
+ .filter(function(item){return !q || (item.title + " " + item.meta + " " + item.id).toLowerCase().includes(q);})
13340
+ .slice(0, 24);
13341
+ document.getElementById("results").innerHTML = items.map(function(item){return '<div class="result" data-id="' + esc(item.id) + '" data-type="' + esc(item.type) + '" style="border-color:' + esc(item.color) + '"><div class="name">' + esc(item.title) + '</div><div class="meta">' + esc(item.meta) + '</div></div>';}).join("") || empty("No matching changes, symbols, or modules", "Try a file, symbol, intent, or risk term");
13342
+ }
13343
+ function renderChanges(){
13344
+ const active = changes.find(function(change){return change.id === selectedChange;}) || changes[0];
13345
+ if(!active){renderHotspots(); return;}
13346
+ selectedChange = active.id;
13347
+ const focus = active.focusNodeId ? nodeById(active.focusNodeId) : undefined;
13348
+ document.getElementById("stage").className = "change-grid";
13349
+ document.getElementById("stage").innerHTML = '<section class="change-list"><h2 class="title">Moving lately</h2>' + changes.map(function(change){
13350
+ return '<div class="change ' + (change.id === active.id ? 'active' : '') + '" data-change="' + esc(change.id) + '"><div class="change-top"><span class="pill ' + esc(change.kind) + '">' + esc(change.kind) + '</span><span class="meta">' + esc(change.source) + ' | ' + esc(change.when) + '</span></div><div class="name">' + esc(change.title) + '</div><div class="meta">' + esc(change.summary) + '</div></div>';
13351
+ }).join("") + '</section><section class="why"><div class="why-card"><div class="change-top"><span class="pill ' + esc(active.kind) + '">' + esc(active.intent) + '</span><span class="meta">' + esc(active.source) + ' | ' + esc(active.when) + '</span></div><h3>' + esc(active.title) + '</h3><p>' + esc(active.why) + '</p><div class="impact"><div><b>' + active.calls + '</b><span>call edges</span></div><div><b>' + active.churn + '</b><span>churn</span></div><div><b>' + esc(active.risk) + '</b><span>risk</span></div></div></div><div class="why-card"><h3>Current touchpoint</h3>' + (focus ? nodeCard(focus.name, badge(focus.kind) + '<span>' + esc(shortPath(focus.filePath)) + '</span>', focus.id, "symbol", colorFor(focus.id), false) : moduleCard(active.moduleId, active.calls)) + '</div><div class="why-card"><h3>Nearest call context</h3><div class="list">' + (focus ? (groupedSymbolCards(incomingSymbol(focus.id), function(edge){return edge.source;}, 3) + groupedSymbolCards(outgoingSymbol(focus.id), function(edge){return edge.target;}, 3)) : empty("No focused symbol", "Open Module Overview")) + '</div></div></section>';
13352
+ document.getElementById("hint").textContent = "Temporal default: what changed, why, and which call path it affects.";
13353
+ document.getElementById("edges").innerHTML = "";
13354
+ document.querySelectorAll("[data-change]").forEach(function(el){el.onclick = function(){selectedChange = el.dataset.change; render();};});
13355
+ }
13356
+ function renderSymbol(){
13357
+ const symbol = selected.type === "symbol" ? nodeById(selected.id) : nodes.find(function(node){return node.moduleId === selected.id;}) || nodes[0];
13358
+ if(!symbol){document.getElementById("stage").innerHTML = empty("No symbols", "Run index_codebase first"); return;}
13359
+ selected = {type:"symbol", id:symbol.id};
13360
+ const callers = incomingSymbol(symbol.id);
13361
+ const callees = outgoingSymbol(symbol.id);
13362
+ document.getElementById("stage").className = "flow";
13363
+ document.getElementById("stage").innerHTML = '<section class="lane" id="callers"><h3>Callers</h3>' + (groupedSymbolCards(callers, function(edge){return edge.source;}) || empty("No callers", "Entry-level symbol")) + '</section><section class="center">' + nodeCard(symbol.name, badge(symbol.kind) + '<span>' + esc(shortPath(symbol.filePath)) + '</span>', symbol.id, "symbol", colorFor(symbol.id), true) + '</section><section class="lane" id="callees"><h3>Callees</h3>' + (groupedSymbolCards(callees, function(edge){return edge.target;}) || empty("No callees", "Leaf-level symbol")) + '</section>';
13364
+ document.getElementById("hint").textContent = "Explore mode: clustered symbol relationships. Focused module view uses the same one-hop navigation.";
13365
+ requestAnimationFrame(drawEdges);
13366
+ }
13367
+ function renderModule(){
13368
+ const item = selected.type === "module" ? moduleById(selected.id) : moduleById((nodeById(selected.id) || {}).moduleId) || modules[0];
13369
+ if(!item){document.getElementById("stage").innerHTML = empty("No modules", "Run index_codebase first"); return;}
13370
+ selected = {type:"module", id:item.id};
13371
+ const callers = incomingModule(item.id);
13372
+ const callees = outgoingModule(item.id);
13373
+ const internalEdges = edges.filter(function(edge){return nodeById(edge.source)?.moduleId === item.id && nodeById(edge.target)?.moduleId === item.id;});
13374
+ const moduleSymbols = nodes.filter(function(node){return node.moduleId === item.id;});
13375
+ const leftTitle = callers.length > 0 ? "Incoming modules" : "Internal callers";
13376
+ const rightTitle = callees.length > 0 ? "Outgoing modules" : "Internal callees";
13377
+ const groupedModuleSymbols = groupedModuleSymbolRows(moduleSymbols);
13378
+ const uniqueModuleSymbolCount = (groupedModuleSymbols.match(/class="row"/g) || []).length;
13379
+ const leftCards = callers.length > 0 ? callers.map(function(edge){return moduleCard(edge.source, edge.weight);}).join("") : groupedSymbolCards(internalEdges, function(edge){return edge.source;}, 12);
13380
+ const rightCards = callees.length > 0 ? callees.map(function(edge){return moduleCard(edge.target, edge.weight);}).join("") : groupedSymbolCards(internalEdges, function(edge){return edge.target;}, 12);
13381
+ document.getElementById("stage").className = "module-board";
13382
+ document.getElementById("stage").innerHTML = '<section class="module-summary">' + nodeCard(item.label, '<span>' + item.symbolCount + ' indexed symbols</span><span>' + uniqueModuleSymbolCount + ' unique rows</span><span>' + esc(item.category) + '</span>', item.id, "module", colorFor(item.id), true) + '</section><section class="module-lanes"><div class="lane" id="callers"><h3>' + leftTitle + '</h3>' + (leftCards || empty("This slice only has intra-module calls.", "Selected from module list")) + '</div><div class="lane" id="callees"><h3>' + rightTitle + '</h3>' + (rightCards || empty("No calls in this module.", "Selected from module list")) + '</div></section><section class="module-symbols"><h3>Symbols in module (' + uniqueModuleSymbolCount + ' unique, ' + moduleSymbols.length + ' indexed)</h3><div class="list">' + groupedModuleSymbols + '</div></section>';
13383
+ document.getElementById("hint").textContent = callers.length + callees.length > 0 ? "Module Overview. Focused module view shows strongest incoming and outgoing module edges." : "Module Overview. This repo slice has no resolved cross-module edges, so this view shows intra-module hotspots.";
13384
+ requestAnimationFrame(drawEdges);
13385
+ }
13386
+ function renderHotspots(){
13387
+ const ranked = nodes.map(function(node){return {node:node, score:incomingSymbol(node.id).length + outgoingSymbol(node.id).length};}).sort(function(a,b){return b.score-a.score;}).slice(0, 20);
13388
+ document.getElementById("stage").className = "grid";
13389
+ document.getElementById("stage").innerHTML = ranked.map(function(item,index){return '<div class="row" data-id="' + esc(item.node.id) + '" data-type="symbol"><div class="meta"><span class="rank">' + (index + 1) + '</span><span class="weight">' + item.score + ' call edges</span></div><div class="name">' + esc(item.node.name) + '</div><div class="meta">' + badge(item.node.kind) + '<span>' + esc(shortPath(item.node.filePath)) + '</span></div></div>';}).join("") || empty("No hotspots", "No call edges in this slice");
13390
+ document.getElementById("hint").textContent = "Hotspots rank symbols by incoming plus outgoing call edges.";
13391
+ document.getElementById("edges").innerHTML = "";
13392
+ }
13393
+ function renderCycles(){
13394
+ const cycles = moduleEdges.filter(function(edge){return moduleEdges.some(function(other){return other.source === edge.target && other.target === edge.source;});});
13395
+ const symbolCycles = findSymbolCycles(20);
13396
+ document.getElementById("stage").className = "grid";
13397
+ const moduleHtml = cycles.map(function(edge){return '<div class="row" data-id="' + esc(edge.source) + '" data-type="module"><div class="name">' + esc(labelOf(edge.source)) + ' -> ' + esc(labelOf(edge.target)) + '</div><div class="meta"><span>module dependency loop candidate</span><span class="weight">' + edge.weight + ' calls</span></div></div>';}).join("");
13398
+ const symbolHtml = symbolCycles.map(function(cycle){const start = cycle.ids[0]; const node = nodeById(start); const path = cycle.ids.map(function(id){return labelOf(id);}).join(" -> ") + " -> " + labelOf(start); const countLabel = cycle.ids.length === 1 ? "1 symbol" : cycle.ids.length + " symbols"; return '<div class="row" data-id="' + esc(start) + '" data-type="symbol"><div class="name">' + esc(path) + '</div><div class="meta"><span>' + esc(cycle.kind) + '</span>' + (node ? '<span>' + esc(shortPath(node.filePath)) + '</span>' : '') + '<span class="weight">' + countLabel + '</span></div></div>';}).join("");
13399
+ document.getElementById("stage").innerHTML = moduleHtml + symbolHtml || emptyCycleState();
13400
+ document.getElementById("hint").textContent = "Cycle mode surfaces module loops plus direct or short symbol recursion.";
13401
+ document.getElementById("edges").innerHTML = "";
13402
+ }
13403
+ function renderDetails(){
13404
+ if(mode === "Changes"){
13405
+ const active = changes.find(function(change){return change.id === selectedChange;});
13406
+ if(active){document.getElementById("details").innerHTML = guideForMode() + '<div class="name">' + esc(active.title) + '</div><div class="kv"><span>Source</span><b>' + esc(active.source) + '</b></div><div class="kv"><span>Changed</span><b>' + esc(active.when) + '</b></div><div class="kv"><span>Intent</span><b>' + esc(active.intent) + '</b></div><div class="kv"><span>Risk</span><b>' + esc(active.risk) + '</b></div><h2 class="title">Onboarding read</h2><p style="margin:0;color:#6f83a4;font-size:13px;line-height:1.6">' + esc(active.summary) + '</p>'; return;}
13407
+ }
13408
+ const isSymbol = selected.type === "symbol";
13409
+ const item = isSymbol ? nodeById(selected.id) : moduleById(selected.id);
13410
+ if(!item){document.getElementById("details").innerHTML = guideForMode(); return;}
13411
+ const incoming = isSymbol ? incomingSymbol(selected.id) : incomingModule(selected.id);
13412
+ const outgoing = isSymbol ? outgoingSymbol(selected.id) : outgoingModule(selected.id);
13413
+ if(!isSymbol){
13414
+ const internal = edges.filter(function(edge){const source = nodeById(edge.source); const target = nodeById(edge.target); return source && target && source.moduleId === selected.id && target.moduleId === selected.id;});
13415
+ const hasCrossModuleEdges = incoming.length + outgoing.length > 0;
13416
+ const edgeCards = hasCrossModuleEdges ? incoming.concat(outgoing).map(function(edge){return moduleCard(edge.source === selected.id ? edge.target : edge.source, edge.weight);}).join("") : groupedSymbolCards(internal, function(edge){return edge.source;}, 4) + groupedSymbolCards(internal, function(edge){return edge.target;}, 4);
13417
+ document.getElementById("details").innerHTML = guideForMode() + '<div class="name">' + esc(item.label) + '</div><div class="kv"><span>Kind</span><b>' + esc(item.category) + '</b></div><div class="kv"><span>File/module</span><b>' + esc(item.label) + '</b></div><div class="kv"><span>Symbols</span><b>' + item.symbolCount + '</b></div><div class="kv"><span>' + (hasCrossModuleEdges ? 'Cross-module edges' : 'Internal call edges') + '</span><b>' + (incoming.length + outgoing.length || internal.length) + '</b></div><h2 class="title">Strongest edges</h2><div class="list">' + (edgeCards || empty("No calls in this module.", "Selected from module list")) + '</div>';
13418
+ return;
13419
+ }
13420
+ document.getElementById("details").innerHTML = guideForMode() + '<div class="name">' + esc(item.name) + '</div><div class="kv"><span>Kind</span><b>' + esc(item.kind) + '</b></div><div class="kv"><span>File/module</span><b>' + esc(shortPath(item.filePath)) + '</b></div><div class="kv"><span>Callers</span><b>' + incoming.length + '</b></div><div class="kv"><span>Callees</span><b>' + outgoing.length + '</b></div><h2 class="title">Strongest edges</h2><div class="list">' + groupedSymbolCards(incoming.concat(outgoing), function(edge){return edge.source === selected.id ? edge.target : edge.source;}, 8) + '</div>';
13421
+ }
13422
+ function drawEdges(){
13423
+ const svg = document.getElementById("edges");
13424
+ const stage = document.getElementById("stage");
13425
+ const left = document.getElementById("callers");
13426
+ const right = document.getElementById("callees");
13427
+ const center = document.querySelector(".center .node");
13428
+ svg.innerHTML = "";
13429
+ if(!left || !right || !center || innerWidth < 901)return;
13430
+ const box = stage.getBoundingClientRect();
13431
+ svg.setAttribute("viewBox", "0 0 " + box.width + " " + box.height);
13432
+ function line(from,to){const a=from.getBoundingClientRect();const b=to.getBoundingClientRect();const x1=a.right-box.left;const y1=a.top+a.height/2-box.top;const x2=b.left-box.left;const y2=b.top+b.height/2-box.top;const mid=(x1+x2)/2;const path=document.createElementNS("http://www.w3.org/2000/svg","path");path.setAttribute("d","M"+x1+","+y1+" C"+mid+","+y1+" "+mid+","+y2+" "+x2+","+y2);path.setAttribute("fill","none");path.setAttribute("stroke","#334966");path.setAttribute("stroke-width","1.5");path.setAttribute("opacity",".65");svg.appendChild(path);}
13433
+ left.querySelectorAll(".node").forEach(function(node){line(node, center);});
13434
+ right.querySelectorAll(".node").forEach(function(node){line(center, node);});
13435
+ }
13436
+ function wireClicks(){
13437
+ document.querySelectorAll("[data-id]").forEach(function(el){el.onclick = function(){selected = {type:el.dataset.type, id:el.dataset.id}; if(selected.type === "change"){selectedChange = selected.id; mode = "Changes";} else {mode = selected.type === "module" ? "Module Overview" : "Explore Symbols";} render();};});
13438
+ }
13439
+ function render(){
13440
+ renderTabs();
13441
+ renderResults();
13442
+ if(mode === "Changes")renderChanges(); else if(mode === "Module Overview")renderModule(); else if(mode === "Hotspots")renderHotspots(); else if(mode === "Cycles")renderCycles(); else renderSymbol();
13443
+ renderDetails();
13444
+ wireClicks();
13445
+ }
13446
+ document.getElementById("search").addEventListener("input", function(event){query = event.target.value; renderResults(); wireClicks();});
13447
+ addEventListener("resize", function(){requestAnimationFrame(drawEdges);});
13448
+ render();
13449
+ </script>
13450
+ </body>
13451
+ </html>`;
13452
+ }
13453
+
13454
+ // src/tools/visualize/transform.ts
13455
+ import * as path23 from "path";
13456
+
13457
+ // src/tools/visualize/modules.ts
13458
+ var MAX_MODULES = 18;
13459
+ var KNOWN_ROOTS = ["src", "native", "tests", "commands", "scripts", "docs", "benchmarks"];
13460
+ function normalizeSlashes(input) {
13461
+ return input.replace(/\\/g, "/");
13462
+ }
13463
+ function stripToProjectRelative(filePath) {
13464
+ const normalized = normalizeSlashes(filePath);
13465
+ let bestIndex = Number.POSITIVE_INFINITY;
13466
+ let bestRoot = null;
13467
+ for (const root of KNOWN_ROOTS) {
13468
+ if (normalized === root || normalized.startsWith(`${root}/`)) {
13469
+ return normalized;
13470
+ }
13471
+ const marker = `/${root}/`;
13472
+ const rootIndex = normalized.indexOf(marker);
13473
+ if (rootIndex !== -1 && rootIndex < bestIndex) {
13474
+ bestIndex = rootIndex;
13475
+ bestRoot = root;
13476
+ }
13477
+ if (normalized.endsWith(`/${root}`)) {
13478
+ return root;
13479
+ }
13480
+ }
13481
+ if (bestRoot !== null && Number.isFinite(bestIndex)) {
13482
+ return normalized.slice(bestIndex + 1);
13483
+ }
13484
+ const parts = normalized.split("/").filter(Boolean);
13485
+ return parts.slice(-3).join("/") || normalized;
13486
+ }
13487
+ function modulePrefixFromRelativePath(relativeFilePath) {
13488
+ const parts = relativeFilePath.split("/").filter(Boolean);
13489
+ if (parts.length === 0) return ".";
13490
+ const [root, second, third] = parts;
13491
+ if (root === "src") {
13492
+ if (!second) return "src";
13493
+ const broadBuckets = /* @__PURE__ */ new Set([
13494
+ "config",
13495
+ "embeddings",
13496
+ "eval",
13497
+ "git",
13498
+ "indexer",
13499
+ "native",
13500
+ "rerank",
13501
+ "tools",
13502
+ "utils",
13503
+ "watcher"
13504
+ ]);
13505
+ if (broadBuckets.has(second)) return `src/${second}`;
13506
+ return second ? `src/${second}` : "src";
13507
+ }
13508
+ if (root === "tests") {
13509
+ if (second === "fixtures" && third) return `tests/fixtures/${third}`;
13510
+ return second ? `tests/${second}` : "tests";
13511
+ }
13512
+ if (root === "native") {
13513
+ if (second === "src") return "native";
13514
+ return second ? `native/${second}` : "native";
13515
+ }
13516
+ if (root === "commands") return "commands";
13517
+ if (root === "scripts") return "scripts";
13518
+ if (root === "docs") return second ? `docs/${second}` : "docs";
13519
+ if (root === "benchmarks") return second ? `benchmarks/${second}` : "benchmarks";
13520
+ return root;
13521
+ }
13522
+ function parentModulePrefix(prefix) {
13523
+ const parts = prefix.split("/");
13524
+ if (parts.length <= 1) return null;
13525
+ return parts.slice(0, -1).join("/");
13526
+ }
13527
+ function shortLabel(prefix, allPrefixes) {
13528
+ const parts = prefix.split("/");
13529
+ for (let i = 1; i <= parts.length; i++) {
13530
+ const suffix = parts.slice(parts.length - i).join("/");
13531
+ const ambiguous = [...allPrefixes].some((candidate) => {
13532
+ if (candidate === prefix) return false;
13533
+ return candidate.endsWith(`/${suffix}`) || candidate === suffix;
13534
+ });
13535
+ if (!ambiguous) return suffix;
13536
+ }
13537
+ return prefix;
13538
+ }
13539
+ function classifyModulePrefix(prefix) {
13540
+ if (prefix.startsWith("tests/fixtures")) return "fixture";
13541
+ if (prefix.startsWith("tests")) return "test";
13542
+ if (prefix === "native" || prefix.startsWith("native/")) return "native";
13543
+ if (prefix === "commands" || prefix.startsWith("commands/")) return "command";
13544
+ if (prefix === "scripts" || prefix.startsWith("scripts/")) return "script";
13545
+ if (prefix === "docs" || prefix.startsWith("docs/")) return "doc";
13546
+ if (prefix === "benchmarks" || prefix.startsWith("benchmarks/")) return "benchmark";
13547
+ if (prefix === "src" || prefix.startsWith("src/")) return "source";
13548
+ return "other";
13549
+ }
13550
+ function displayLabelForPrefix(prefix, allPrefixes) {
13551
+ const short = shortLabel(prefix, allPrefixes);
13552
+ if (prefix.startsWith("tests/fixtures/")) {
13553
+ const fixtureName = prefix.slice("tests/fixtures/".length);
13554
+ return `fixture: ${fixtureName}`;
13555
+ }
13556
+ if (prefix === "tests/fixtures") {
13557
+ return "fixtures";
13558
+ }
13559
+ if (prefix.startsWith("tests/")) {
13560
+ const testArea = prefix.slice("tests/".length);
13561
+ return `tests: ${testArea}`;
13562
+ }
13563
+ if (prefix === "tests") {
13564
+ return "tests";
13565
+ }
13566
+ if (prefix === "native") {
13567
+ return "native";
13568
+ }
13569
+ return short;
13570
+ }
13571
+ function compactModules(prefixToNodes) {
13572
+ const grouped = new Map(prefixToNodes);
13573
+ while (grouped.size > MAX_MODULES) {
13574
+ const smallest = [...grouped.entries()].sort((a, b) => a[1].length - b[1].length)[0];
13575
+ if (!smallest) break;
13576
+ const [prefix, members] = smallest;
13577
+ const parent = parentModulePrefix(prefix);
13578
+ if (!parent) break;
13579
+ grouped.delete(prefix);
13580
+ if (!grouped.has(parent)) grouped.set(parent, []);
13581
+ grouped.get(parent)?.push(...members);
13582
+ }
13583
+ return grouped;
13584
+ }
13585
+ function deriveModules(nodes) {
13586
+ const initial = /* @__PURE__ */ new Map();
13587
+ for (const node of nodes) {
13588
+ const relative10 = stripToProjectRelative(node.filePath);
13589
+ const prefix = modulePrefixFromRelativePath(relative10);
13590
+ if (!initial.has(prefix)) initial.set(prefix, []);
13591
+ initial.get(prefix)?.push(node);
13592
+ }
13593
+ const nonFixturePrefixes = [...initial.keys()].filter((prefix) => !prefix.startsWith("tests/fixtures/"));
13594
+ if (nonFixturePrefixes.length > 0) {
13595
+ const fixtureEntries = [...initial.entries()].filter(([prefix]) => prefix.startsWith("tests/fixtures/"));
13596
+ if (fixtureEntries.length > 1) {
13597
+ const mergedFixtureNodes = fixtureEntries.flatMap(([, members]) => members);
13598
+ for (const [prefix] of fixtureEntries) {
13599
+ initial.delete(prefix);
13600
+ }
13601
+ initial.set("tests/fixtures", mergedFixtureNodes);
13602
+ }
13603
+ }
13604
+ const grouped = compactModules(initial);
13605
+ const allPrefixes = new Set(grouped.keys());
13606
+ const modules = [...grouped.entries()].map(([prefix, members]) => {
13607
+ const kinds = {};
13608
+ for (const node of members) {
13609
+ kinds[node.kind] = (kinds[node.kind] ?? 0) + 1;
13610
+ }
13611
+ const label = displayLabelForPrefix(prefix, allPrefixes);
13612
+ const category = classifyModulePrefix(prefix);
13613
+ const id = `module-${prefix.replace(/[^a-zA-Z0-9]+/g, "-")}`;
13614
+ for (const node of members) {
13615
+ node.moduleId = id;
13616
+ node.moduleLabel = label;
13617
+ }
13618
+ return {
13619
+ id,
13620
+ label,
13621
+ pathPrefix: prefix,
13622
+ category,
13623
+ symbolCount: members.length,
13624
+ symbols: members.map((node) => node.id),
13625
+ kinds
13626
+ };
13627
+ }).sort((a, b) => b.symbolCount - a.symbolCount || a.label.localeCompare(b.label));
13628
+ return modules;
13629
+ }
13630
+ function deriveModuleEdges(nodes, edges) {
13631
+ const nodeMap = new Map(nodes.map((node) => [node.id, node]));
13632
+ const aggregate = /* @__PURE__ */ new Map();
13633
+ for (const edge of edges) {
13634
+ const source = nodeMap.get(edge.source);
13635
+ if (!source || !source.moduleId) continue;
13636
+ const target = nodeMap.get(edge.target);
13637
+ const targetModuleId = target?.moduleId;
13638
+ if (!targetModuleId) continue;
13639
+ if (source.moduleId === targetModuleId) continue;
13640
+ const key = `${source.moduleId}__${targetModuleId}`;
13641
+ if (!aggregate.has(key)) {
13642
+ aggregate.set(key, {
13643
+ source: source.moduleId,
13644
+ target: targetModuleId,
13645
+ weight: 0,
13646
+ callTypes: {}
13647
+ });
13648
+ }
13649
+ const moduleEdge = aggregate.get(key);
13650
+ moduleEdge.weight += 1;
13651
+ moduleEdge.callTypes[edge.callType] = (moduleEdge.callTypes[edge.callType] ?? 0) + 1;
13652
+ }
13653
+ return [...aggregate.values()].sort((a, b) => b.weight - a.weight);
13654
+ }
13655
+
13656
+ // src/tools/visualize/transform.ts
13657
+ function transformForVisualization(symbols, edges, options = {}) {
13658
+ const { includeOrphans = false, directory, maxNodes = 5e3 } = options;
13659
+ let filteredSymbols = symbols;
13660
+ if (directory) {
13661
+ const normalizedDir = directory.replace(/\/$/, "");
13662
+ const normalizedDirWithSlash = `${normalizedDir}/`;
13663
+ const normalizedAbsoluteSuffix = `/${normalizedDirWithSlash}`;
13664
+ filteredSymbols = symbols.filter(
13665
+ (s) => {
13666
+ const normalizedPath = s.filePath.replace(/\\/g, "/");
13667
+ return normalizedPath === normalizedDir || normalizedPath.startsWith(normalizedDirWithSlash) || normalizedPath.endsWith(`/${normalizedDir}`) || normalizedPath.includes(normalizedAbsoluteSuffix);
13668
+ }
13669
+ );
13670
+ }
13671
+ const symbolIdSet = new Set(filteredSymbols.map((s) => s.id));
13672
+ const filteredEdges = [];
13673
+ for (const edge of edges) {
13674
+ if (!edge.isResolved || !edge.toSymbolId) continue;
13675
+ if (!symbolIdSet.has(edge.fromSymbolId)) continue;
13676
+ if (!symbolIdSet.has(edge.toSymbolId)) continue;
13677
+ filteredEdges.push({
13678
+ source: edge.fromSymbolId,
13679
+ target: edge.toSymbolId,
13680
+ callType: edge.callType,
13681
+ confidence: edge.confidence,
13682
+ line: edge.line
13683
+ });
13684
+ }
13685
+ let finalSymbols = filteredSymbols;
13686
+ if (!includeOrphans) {
13687
+ const connectedIds = /* @__PURE__ */ new Set();
13688
+ for (const edge of filteredEdges) {
13689
+ connectedIds.add(edge.source);
13690
+ connectedIds.add(edge.target);
13691
+ }
13692
+ finalSymbols = filteredSymbols.filter((s) => connectedIds.has(s.id));
13693
+ }
13694
+ const truncated = finalSymbols.length > maxNodes;
13695
+ if (truncated) {
13696
+ const connectionCount = /* @__PURE__ */ new Map();
13697
+ for (const edge of filteredEdges) {
13698
+ connectionCount.set(edge.source, (connectionCount.get(edge.source) ?? 0) + 1);
13699
+ connectionCount.set(edge.target, (connectionCount.get(edge.target) ?? 0) + 1);
13700
+ }
13701
+ finalSymbols = finalSymbols.sort((a, b) => (connectionCount.get(b.id) ?? 0) - (connectionCount.get(a.id) ?? 0)).slice(0, maxNodes);
13702
+ const remainingIds = new Set(finalSymbols.map((s) => s.id));
13703
+ filteredEdges.splice(
13704
+ 0,
13705
+ filteredEdges.length,
13706
+ ...filteredEdges.filter((e) => remainingIds.has(e.source) && remainingIds.has(e.target))
13707
+ );
13708
+ }
13709
+ const nodes = finalSymbols.map((s) => ({
13710
+ id: s.id,
13711
+ name: s.name,
13712
+ filePath: s.filePath,
13713
+ kind: s.kind,
13714
+ line: s.startLine,
13715
+ directory: path23.dirname(s.filePath),
13716
+ moduleId: "",
13717
+ moduleLabel: ""
13718
+ }));
13719
+ const modules = deriveModules(nodes);
13720
+ const moduleEdges = deriveModuleEdges(nodes, filteredEdges);
13721
+ return {
13722
+ nodes,
13723
+ edges: filteredEdges,
13724
+ modules,
13725
+ moduleEdges,
13726
+ metadata: {
13727
+ totalSymbols: symbols.length,
13728
+ totalEdges: edges.length,
13729
+ truncated,
13730
+ directory,
13731
+ moduleCount: modules.length
13732
+ }
13733
+ };
13734
+ }
13735
+
13736
+ // src/cli.ts
13737
+ function parseArgs(argv) {
13738
+ let project = process.cwd();
13739
+ let config;
13740
+ let host = "opencode";
13741
+ for (let i = 2; i < argv.length; i++) {
13742
+ if (argv[i] === "--project" && argv[i + 1]) {
13743
+ project = path24.resolve(argv[++i]);
13744
+ } else if (argv[i] === "--config" && argv[i + 1]) {
13745
+ config = path24.resolve(argv[++i]);
13746
+ } else if (argv[i] === "--host" && argv[i + 1]) {
13747
+ host = parseHostMode(argv[++i]);
13748
+ } else if (argv[i] === "--host") {
13749
+ host = parseHostMode(void 0);
13750
+ }
13751
+ }
13752
+ return { project, config, host };
13753
+ }
13754
+ function loadCliRawConfig(args) {
13755
+ return args.config ? loadConfigFile(args.config) : loadMergedConfig(args.project, args.host);
13756
+ }
13757
+ function parseVisualizeArgs(argv, cwd) {
13758
+ let project = cwd;
13759
+ let directory;
13760
+ let includeOrphans = false;
13761
+ let maxNodes = 5e3;
13762
+ for (let i = 0; i < argv.length; i++) {
13763
+ const arg = argv[i];
13764
+ if (arg === "--project" && argv[i + 1]) {
13765
+ project = path24.resolve(argv[++i]);
13766
+ } else if (arg === "--max" && argv[i + 1]) {
13767
+ maxNodes = Number(argv[++i]);
13768
+ } else if (arg.startsWith("--max=") || arg.startsWith("max=")) {
13769
+ maxNodes = Number(arg.split("=")[1]);
13770
+ } else if (arg === "--orphans" || arg === "orphans" || arg === "--include-orphans" || arg === "include-orphans") {
13771
+ includeOrphans = true;
13772
+ } else if (!arg.startsWith("-") && directory === void 0) {
13773
+ directory = arg;
13774
+ }
13775
+ }
13776
+ if (!Number.isFinite(maxNodes) || maxNodes < 1) {
13777
+ throw new Error("max must be a positive number");
13778
+ }
13779
+ return { directory, includeOrphans, maxNodes, project };
13780
+ }
13781
+ async function handleVisualizeCommand(argv, cwd) {
13782
+ try {
13783
+ const args = parseVisualizeArgs(argv, cwd);
13784
+ const config = parseConfig(loadMergedConfig(args.project));
13785
+ const indexer = new Indexer(args.project, config);
13786
+ const rawData = await indexer.getVisualizationData({
13787
+ directory: args.directory
13788
+ });
13789
+ if (rawData.symbols.length === 0) {
13790
+ console.error("No call graph data found. Run /index in OpenCode first, then retry npm run visualize.");
13791
+ return 1;
13792
+ }
13793
+ const vizData = attachRecentActivity(transformForVisualization(rawData.symbols, rawData.edges, {
13794
+ includeOrphans: args.includeOrphans,
13795
+ directory: args.directory,
13796
+ maxNodes: args.maxNodes
13797
+ }), args.project);
13798
+ if (vizData.nodes.length === 0) {
13799
+ console.error("No connected symbols found. Retry with: npm run visualize -- orphans");
13800
+ return 1;
13801
+ }
13802
+ const outputPath = path24.join(os5.tmpdir(), `call-graph-${Date.now()}.html`);
13803
+ writeFileSync6(outputPath, generateVisualizationHtml(vizData), "utf-8");
13804
+ console.log(`Temporal call graph visualization generated: ${outputPath}`);
13805
+ console.log(`Nodes: ${vizData.nodes.length} | Edges: ${vizData.edges.length}`);
13806
+ console.log(`Recent change lenses: ${vizData.changes?.length ?? 0}`);
13807
+ if (vizData.metadata.truncated) {
13808
+ console.log(`Graph truncated to ${args.maxNodes} most-connected nodes.`);
13809
+ }
13810
+ return 0;
13811
+ } catch {
13812
+ console.error("Failed to generate visualization. Check the project, config, and arguments, then retry.");
13813
+ return 1;
13814
+ }
13815
+ }
13816
+ async function main() {
13817
+ if (process.argv[2] === "eval") {
13818
+ const exitCode = await handleEvalCommand(process.argv.slice(3), process.cwd());
13819
+ process.exit(exitCode);
13820
+ }
13821
+ if (process.argv[2] === "visualize") {
13822
+ const exitCode = await handleVisualizeCommand(process.argv.slice(3), process.cwd());
13823
+ process.exit(exitCode);
13824
+ }
13825
+ const args = parseArgs(process.argv);
13826
+ const rawConfig = loadCliRawConfig(args);
13827
+ const config = parseConfig(rawConfig);
13828
+ const server = createMcpServer(args.project, config, args.host);
13829
+ const transport = new StdioServerTransport();
13830
+ await server.connect(transport);
13831
+ let watcher = null;
13832
+ const isHomeDir = path24.resolve(args.project) === path24.resolve(os5.homedir());
13833
+ const isValidProject = !isHomeDir && (!config.indexing.requireProjectMarker || hasProjectMarker(args.project));
13834
+ if (config.indexing.autoIndex && isValidProject) {
13835
+ const indexer = getIndexerForProject(args.project, args.host);
13836
+ startAutoIndex(indexer, args.project);
13837
+ }
13838
+ if (config.indexing.watchFiles && isValidProject) {
13839
+ watcher = createWatcherWithIndexer(
13840
+ () => getIndexerForProject(args.project, args.host),
13841
+ args.project,
13842
+ config,
13843
+ args.host,
13844
+ args.config ? { configPath: args.config } : {}
13845
+ );
13846
+ }
13847
+ const shutdown = () => {
13848
+ watcher?.stop();
13849
+ server.close().catch(() => {
10543
13850
  });
10544
13851
  process.exit(0);
10545
13852
  };
10546
13853
  process.on("SIGINT", shutdown);
10547
13854
  process.on("SIGTERM", shutdown);
10548
13855
  }
10549
- main().catch((_error) => {
10550
- console.error("Fatal: failed to start MCP server (check config and network)");
13856
+ function handleMainError(error) {
13857
+ if (error instanceof Error && error.message.startsWith("Invalid host mode")) {
13858
+ console.error(`Invalid host mode. Allowed values: ${HOST_MODES.join(", ")}.`);
13859
+ process.exit(1);
13860
+ }
13861
+ if (error instanceof Error) {
13862
+ console.error("Failed to start MCP server. Check config and network.");
13863
+ process.exit(1);
13864
+ }
13865
+ console.error("Fatal: failed to start MCP server");
10551
13866
  process.exit(1);
10552
- });
13867
+ }
13868
+ if (process.argv[1] && import.meta.url === pathToFileURL(path24.resolve(process.argv[1])).href) {
13869
+ main().catch(handleMainError);
13870
+ }
13871
+ export {
13872
+ loadCliRawConfig,
13873
+ parseArgs
13874
+ };
13875
+ /*! Bundled license information:
13876
+
13877
+ chokidar/index.js:
13878
+ (*! chokidar - MIT License (c) 2012 Paul Miller (paulmillr.com) *)
13879
+ */
10553
13880
  //# sourceMappingURL=cli.js.map