opencode-codebase-index 0.1.11 → 0.2.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/index.cjs CHANGED
@@ -491,7 +491,7 @@ var require_ignore = __commonJS({
491
491
  // path matching.
492
492
  // - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
493
493
  // @returns {TestResult} true if a file is ignored
494
- test(path7, checkUnignored, mode) {
494
+ test(path8, 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(path7);
503
+ const matched = rule[mode].test(path8);
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 = (path7, originalPath, doThrow) => {
525
- if (!isString(path7)) {
524
+ var checkPath = (path8, originalPath, doThrow) => {
525
+ if (!isString(path8)) {
526
526
  return doThrow(
527
527
  `path must be a string, but got \`${originalPath}\``,
528
528
  TypeError
529
529
  );
530
530
  }
531
- if (!path7) {
531
+ if (!path8) {
532
532
  return doThrow(`path must not be empty`, TypeError);
533
533
  }
534
- if (checkPath.isNotRelative(path7)) {
534
+ if (checkPath.isNotRelative(path8)) {
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 = (path7) => REGEX_TEST_INVALID_PATH.test(path7);
543
+ var isNotRelative = (path8) => REGEX_TEST_INVALID_PATH.test(path8);
544
544
  checkPath.isNotRelative = isNotRelative;
545
545
  checkPath.convert = (p) => p;
546
546
  var Ignore2 = class {
@@ -570,19 +570,19 @@ var require_ignore = __commonJS({
570
570
  }
571
571
  // @returns {TestResult}
572
572
  _test(originalPath, cache, checkUnignored, slices) {
573
- const path7 = originalPath && checkPath.convert(originalPath);
573
+ const path8 = originalPath && checkPath.convert(originalPath);
574
574
  checkPath(
575
- path7,
575
+ path8,
576
576
  originalPath,
577
577
  this._strictPathCheck ? throwError : RETURN_FALSE
578
578
  );
579
- return this._t(path7, cache, checkUnignored, slices);
579
+ return this._t(path8, cache, checkUnignored, slices);
580
580
  }
581
- checkIgnore(path7) {
582
- if (!REGEX_TEST_TRAILING_SLASH.test(path7)) {
583
- return this.test(path7);
581
+ checkIgnore(path8) {
582
+ if (!REGEX_TEST_TRAILING_SLASH.test(path8)) {
583
+ return this.test(path8);
584
584
  }
585
- const slices = path7.split(SLASH2).filter(Boolean);
585
+ const slices = path8.split(SLASH2).filter(Boolean);
586
586
  slices.pop();
587
587
  if (slices.length) {
588
588
  const parent = this._t(
@@ -595,18 +595,18 @@ var require_ignore = __commonJS({
595
595
  return parent;
596
596
  }
597
597
  }
598
- return this._rules.test(path7, false, MODE_CHECK_IGNORE);
598
+ return this._rules.test(path8, false, MODE_CHECK_IGNORE);
599
599
  }
600
- _t(path7, cache, checkUnignored, slices) {
601
- if (path7 in cache) {
602
- return cache[path7];
600
+ _t(path8, cache, checkUnignored, slices) {
601
+ if (path8 in cache) {
602
+ return cache[path8];
603
603
  }
604
604
  if (!slices) {
605
- slices = path7.split(SLASH2).filter(Boolean);
605
+ slices = path8.split(SLASH2).filter(Boolean);
606
606
  }
607
607
  slices.pop();
608
608
  if (!slices.length) {
609
- return cache[path7] = this._rules.test(path7, checkUnignored, MODE_IGNORE);
609
+ return cache[path8] = this._rules.test(path8, checkUnignored, MODE_IGNORE);
610
610
  }
611
611
  const parent = this._t(
612
612
  slices.join(SLASH2) + SLASH2,
@@ -614,29 +614,29 @@ var require_ignore = __commonJS({
614
614
  checkUnignored,
615
615
  slices
616
616
  );
617
- return cache[path7] = parent.ignored ? parent : this._rules.test(path7, checkUnignored, MODE_IGNORE);
617
+ return cache[path8] = parent.ignored ? parent : this._rules.test(path8, checkUnignored, MODE_IGNORE);
618
618
  }
619
- ignores(path7) {
620
- return this._test(path7, this._ignoreCache, false).ignored;
619
+ ignores(path8) {
620
+ return this._test(path8, this._ignoreCache, false).ignored;
621
621
  }
622
622
  createFilter() {
623
- return (path7) => !this.ignores(path7);
623
+ return (path8) => !this.ignores(path8);
624
624
  }
625
625
  filter(paths) {
626
626
  return makeArray(paths).filter(this.createFilter());
627
627
  }
628
628
  // @returns {TestResult}
629
- test(path7) {
630
- return this._test(path7, this._testCache, true);
629
+ test(path8) {
630
+ return this._test(path8, this._testCache, true);
631
631
  }
632
632
  };
633
633
  var factory = (options) => new Ignore2(options);
634
- var isPathValid = (path7) => checkPath(path7 && checkPath.convert(path7), path7, RETURN_FALSE);
634
+ var isPathValid = (path8) => checkPath(path8 && checkPath.convert(path8), path8, 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 = (path7) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path7) || isNotRelative(path7);
639
+ checkPath.isNotRelative = (path8) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path8) || isNotRelative(path8);
640
640
  };
641
641
  if (
642
642
  // Detect `process` so that it can run in browsers.
@@ -657,8 +657,8 @@ __export(index_exports, {
657
657
  default: () => index_default
658
658
  });
659
659
  module.exports = __toCommonJS(index_exports);
660
- var import_fs4 = require("fs");
661
- var path6 = __toESM(require("path"), 1);
660
+ var import_fs5 = require("fs");
661
+ var path7 = __toESM(require("path"), 1);
662
662
 
663
663
  // src/config/schema.ts
664
664
  var DEFAULT_INCLUDE = [
@@ -811,8 +811,8 @@ function getDefaultModelForProvider(provider) {
811
811
  }
812
812
 
813
813
  // src/indexer/index.ts
814
- var import_fs3 = require("fs");
815
- var path4 = __toESM(require("path"), 1);
814
+ var import_fs4 = require("fs");
815
+ var path5 = __toESM(require("path"), 1);
816
816
 
817
817
  // node_modules/eventemitter3/index.mjs
818
818
  var import_index = __toESM(require_eventemitter3(), 1);
@@ -2193,7 +2193,10 @@ function shouldIncludeFile(filePath, projectRoot, includePatterns, excludePatter
2193
2193
  return false;
2194
2194
  }
2195
2195
  function matchGlob(filePath, pattern) {
2196
- const regexPattern = pattern.replace(/\*\*/g, "<<<DOUBLESTAR>>>").replace(/\*/g, "[^/]*").replace(/<<<DOUBLESTAR>>>/g, ".*").replace(/\?/g, ".").replace(/\{([^}]+)\}/g, (_, p1) => `(${p1.split(",").join("|")})`);
2196
+ let regexPattern = pattern.replace(/\*\*/g, "<<<DOUBLESTAR>>>").replace(/\*/g, "[^/]*").replace(/<<<DOUBLESTAR>>>/g, ".*").replace(/\?/g, ".").replace(/\{([^}]+)\}/g, (_, p1) => `(${p1.split(",").join("|")})`);
2197
+ if (regexPattern.startsWith(".*/")) {
2198
+ regexPattern = `(.*\\/)?${regexPattern.slice(3)}`;
2199
+ }
2197
2200
  const regex = new RegExp(`^${regexPattern}$`);
2198
2201
  return regex.test(filePath);
2199
2202
  }
@@ -2665,37 +2668,181 @@ var InvertedIndex = class {
2665
2668
  return this.inner.documentCount();
2666
2669
  }
2667
2670
  };
2671
+ var Database = class {
2672
+ inner;
2673
+ constructor(dbPath) {
2674
+ this.inner = new native.Database(dbPath);
2675
+ }
2676
+ embeddingExists(contentHash) {
2677
+ return this.inner.embeddingExists(contentHash);
2678
+ }
2679
+ getEmbedding(contentHash) {
2680
+ return this.inner.getEmbedding(contentHash) ?? null;
2681
+ }
2682
+ upsertEmbedding(contentHash, embedding, chunkText, model) {
2683
+ this.inner.upsertEmbedding(contentHash, embedding, chunkText, model);
2684
+ }
2685
+ getMissingEmbeddings(contentHashes) {
2686
+ return this.inner.getMissingEmbeddings(contentHashes);
2687
+ }
2688
+ upsertChunk(chunk) {
2689
+ this.inner.upsertChunk(chunk);
2690
+ }
2691
+ getChunk(chunkId) {
2692
+ return this.inner.getChunk(chunkId) ?? null;
2693
+ }
2694
+ getChunksByFile(filePath) {
2695
+ return this.inner.getChunksByFile(filePath);
2696
+ }
2697
+ deleteChunksByFile(filePath) {
2698
+ return this.inner.deleteChunksByFile(filePath);
2699
+ }
2700
+ addChunksToBranch(branch, chunkIds) {
2701
+ this.inner.addChunksToBranch(branch, chunkIds);
2702
+ }
2703
+ clearBranch(branch) {
2704
+ return this.inner.clearBranch(branch);
2705
+ }
2706
+ getBranchChunkIds(branch) {
2707
+ return this.inner.getBranchChunkIds(branch);
2708
+ }
2709
+ getBranchDelta(branch, baseBranch) {
2710
+ return this.inner.getBranchDelta(branch, baseBranch);
2711
+ }
2712
+ chunkExistsOnBranch(branch, chunkId) {
2713
+ return this.inner.chunkExistsOnBranch(branch, chunkId);
2714
+ }
2715
+ getAllBranches() {
2716
+ return this.inner.getAllBranches();
2717
+ }
2718
+ getMetadata(key) {
2719
+ return this.inner.getMetadata(key) ?? null;
2720
+ }
2721
+ setMetadata(key, value) {
2722
+ this.inner.setMetadata(key, value);
2723
+ }
2724
+ deleteMetadata(key) {
2725
+ return this.inner.deleteMetadata(key);
2726
+ }
2727
+ gcOrphanEmbeddings() {
2728
+ return this.inner.gcOrphanEmbeddings();
2729
+ }
2730
+ gcOrphanChunks() {
2731
+ return this.inner.gcOrphanChunks();
2732
+ }
2733
+ getStats() {
2734
+ return this.inner.getStats();
2735
+ }
2736
+ };
2737
+
2738
+ // src/git/index.ts
2739
+ var import_fs3 = require("fs");
2740
+ var path4 = __toESM(require("path"), 1);
2741
+ var import_child_process = require("child_process");
2742
+ function isGitRepo(dir) {
2743
+ return (0, import_fs3.existsSync)(path4.join(dir, ".git"));
2744
+ }
2745
+ function getCurrentBranch(repoRoot) {
2746
+ const headPath = path4.join(repoRoot, ".git", "HEAD");
2747
+ if (!(0, import_fs3.existsSync)(headPath)) {
2748
+ return null;
2749
+ }
2750
+ try {
2751
+ const headContent = (0, import_fs3.readFileSync)(headPath, "utf-8").trim();
2752
+ const match = headContent.match(/^ref: refs\/heads\/(.+)$/);
2753
+ if (match) {
2754
+ return match[1];
2755
+ }
2756
+ if (/^[0-9a-f]{40}$/i.test(headContent)) {
2757
+ return headContent.slice(0, 7);
2758
+ }
2759
+ return null;
2760
+ } catch {
2761
+ return null;
2762
+ }
2763
+ }
2764
+ function getBaseBranch(repoRoot) {
2765
+ const candidates = ["main", "master", "develop", "trunk"];
2766
+ for (const candidate of candidates) {
2767
+ const refPath = path4.join(repoRoot, ".git", "refs", "heads", candidate);
2768
+ if ((0, import_fs3.existsSync)(refPath)) {
2769
+ return candidate;
2770
+ }
2771
+ const packedRefsPath = path4.join(repoRoot, ".git", "packed-refs");
2772
+ if ((0, import_fs3.existsSync)(packedRefsPath)) {
2773
+ try {
2774
+ const content = (0, import_fs3.readFileSync)(packedRefsPath, "utf-8");
2775
+ if (content.includes(`refs/heads/${candidate}`)) {
2776
+ return candidate;
2777
+ }
2778
+ } catch {
2779
+ }
2780
+ }
2781
+ }
2782
+ try {
2783
+ const result = (0, import_child_process.execSync)("git remote show origin", {
2784
+ cwd: repoRoot,
2785
+ encoding: "utf-8",
2786
+ stdio: ["pipe", "pipe", "pipe"]
2787
+ });
2788
+ const match = result.match(/HEAD branch: (.+)/);
2789
+ if (match) {
2790
+ return match[1].trim();
2791
+ }
2792
+ } catch {
2793
+ }
2794
+ return getCurrentBranch(repoRoot) ?? "main";
2795
+ }
2796
+ function getBranchOrDefault(repoRoot) {
2797
+ if (!isGitRepo(repoRoot)) {
2798
+ return "default";
2799
+ }
2800
+ return getCurrentBranch(repoRoot) ?? "default";
2801
+ }
2802
+ function getHeadPath(repoRoot) {
2803
+ return path4.join(repoRoot, ".git", "HEAD");
2804
+ }
2668
2805
 
2669
2806
  // src/indexer/index.ts
2807
+ function float32ArrayToBuffer(arr) {
2808
+ const float32 = new Float32Array(arr);
2809
+ return Buffer.from(float32.buffer);
2810
+ }
2811
+ function bufferToFloat32Array(buf) {
2812
+ return new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4);
2813
+ }
2670
2814
  var Indexer = class {
2671
2815
  config;
2672
2816
  projectRoot;
2673
2817
  indexPath;
2674
2818
  store = null;
2675
2819
  invertedIndex = null;
2820
+ database = null;
2676
2821
  provider = null;
2677
2822
  detectedProvider = null;
2678
2823
  fileHashCache = /* @__PURE__ */ new Map();
2679
2824
  fileHashCachePath = "";
2680
2825
  failedBatchesPath = "";
2826
+ currentBranch = "default";
2827
+ baseBranch = "main";
2681
2828
  constructor(projectRoot, config) {
2682
2829
  this.projectRoot = projectRoot;
2683
2830
  this.config = config;
2684
2831
  this.indexPath = this.getIndexPath();
2685
- this.fileHashCachePath = path4.join(this.indexPath, "file-hashes.json");
2686
- this.failedBatchesPath = path4.join(this.indexPath, "failed-batches.json");
2832
+ this.fileHashCachePath = path5.join(this.indexPath, "file-hashes.json");
2833
+ this.failedBatchesPath = path5.join(this.indexPath, "failed-batches.json");
2687
2834
  }
2688
2835
  getIndexPath() {
2689
2836
  if (this.config.scope === "global") {
2690
2837
  const homeDir = process.env.HOME || process.env.USERPROFILE || "";
2691
- return path4.join(homeDir, ".opencode", "global-index");
2838
+ return path5.join(homeDir, ".opencode", "global-index");
2692
2839
  }
2693
- return path4.join(this.projectRoot, ".opencode", "index");
2840
+ return path5.join(this.projectRoot, ".opencode", "index");
2694
2841
  }
2695
2842
  loadFileHashCache() {
2696
2843
  try {
2697
- if ((0, import_fs3.existsSync)(this.fileHashCachePath)) {
2698
- const data = (0, import_fs3.readFileSync)(this.fileHashCachePath, "utf-8");
2844
+ if ((0, import_fs4.existsSync)(this.fileHashCachePath)) {
2845
+ const data = (0, import_fs4.readFileSync)(this.fileHashCachePath, "utf-8");
2699
2846
  const parsed = JSON.parse(data);
2700
2847
  this.fileHashCache = new Map(Object.entries(parsed));
2701
2848
  }
@@ -2708,12 +2855,12 @@ var Indexer = class {
2708
2855
  for (const [k, v] of this.fileHashCache) {
2709
2856
  obj[k] = v;
2710
2857
  }
2711
- (0, import_fs3.writeFileSync)(this.fileHashCachePath, JSON.stringify(obj));
2858
+ (0, import_fs4.writeFileSync)(this.fileHashCachePath, JSON.stringify(obj));
2712
2859
  }
2713
2860
  loadFailedBatches() {
2714
2861
  try {
2715
- if ((0, import_fs3.existsSync)(this.failedBatchesPath)) {
2716
- const data = (0, import_fs3.readFileSync)(this.failedBatchesPath, "utf-8");
2862
+ if ((0, import_fs4.existsSync)(this.failedBatchesPath)) {
2863
+ const data = (0, import_fs4.readFileSync)(this.failedBatchesPath, "utf-8");
2717
2864
  return JSON.parse(data);
2718
2865
  }
2719
2866
  } catch {
@@ -2723,13 +2870,13 @@ var Indexer = class {
2723
2870
  }
2724
2871
  saveFailedBatches(batches) {
2725
2872
  if (batches.length === 0) {
2726
- if ((0, import_fs3.existsSync)(this.failedBatchesPath)) {
2727
- import_fs3.promises.unlink(this.failedBatchesPath).catch(() => {
2873
+ if ((0, import_fs4.existsSync)(this.failedBatchesPath)) {
2874
+ import_fs4.promises.unlink(this.failedBatchesPath).catch(() => {
2728
2875
  });
2729
2876
  }
2730
2877
  return;
2731
2878
  }
2732
- (0, import_fs3.writeFileSync)(this.failedBatchesPath, JSON.stringify(batches, null, 2));
2879
+ (0, import_fs4.writeFileSync)(this.failedBatchesPath, JSON.stringify(batches, null, 2));
2733
2880
  }
2734
2881
  addFailedBatch(batch, error) {
2735
2882
  const existing = this.loadFailedBatches();
@@ -2752,27 +2899,68 @@ var Indexer = class {
2752
2899
  this.detectedProvider.credentials,
2753
2900
  this.detectedProvider.modelInfo
2754
2901
  );
2755
- await import_fs3.promises.mkdir(this.indexPath, { recursive: true });
2902
+ await import_fs4.promises.mkdir(this.indexPath, { recursive: true });
2756
2903
  const dimensions = this.detectedProvider.modelInfo.dimensions;
2757
- const storePath = path4.join(this.indexPath, "vectors");
2904
+ const storePath = path5.join(this.indexPath, "vectors");
2758
2905
  this.store = new VectorStore(storePath, dimensions);
2759
- const indexFilePath = path4.join(this.indexPath, "vectors.usearch");
2760
- if ((0, import_fs3.existsSync)(indexFilePath)) {
2906
+ const indexFilePath = path5.join(this.indexPath, "vectors.usearch");
2907
+ if ((0, import_fs4.existsSync)(indexFilePath)) {
2761
2908
  this.store.load();
2762
2909
  }
2763
- const invertedIndexPath = path4.join(this.indexPath, "inverted-index.json");
2910
+ const invertedIndexPath = path5.join(this.indexPath, "inverted-index.json");
2764
2911
  this.invertedIndex = new InvertedIndex(invertedIndexPath);
2765
- this.invertedIndex.load();
2912
+ try {
2913
+ this.invertedIndex.load();
2914
+ } catch {
2915
+ if ((0, import_fs4.existsSync)(invertedIndexPath)) {
2916
+ await import_fs4.promises.unlink(invertedIndexPath);
2917
+ }
2918
+ this.invertedIndex = new InvertedIndex(invertedIndexPath);
2919
+ }
2920
+ const dbPath = path5.join(this.indexPath, "codebase.db");
2921
+ const dbIsNew = !(0, import_fs4.existsSync)(dbPath);
2922
+ this.database = new Database(dbPath);
2923
+ if (dbIsNew && this.store.count() > 0) {
2924
+ this.migrateFromLegacyIndex();
2925
+ }
2926
+ if (isGitRepo(this.projectRoot)) {
2927
+ this.currentBranch = getBranchOrDefault(this.projectRoot);
2928
+ this.baseBranch = getBaseBranch(this.projectRoot);
2929
+ } else {
2930
+ this.currentBranch = "default";
2931
+ this.baseBranch = "default";
2932
+ }
2933
+ }
2934
+ migrateFromLegacyIndex() {
2935
+ if (!this.store || !this.database) return;
2936
+ const allMetadata = this.store.getAllMetadata();
2937
+ const chunkIds = [];
2938
+ for (const { key, metadata } of allMetadata) {
2939
+ const chunkData = {
2940
+ chunkId: key,
2941
+ contentHash: metadata.hash,
2942
+ filePath: metadata.filePath,
2943
+ startLine: metadata.startLine,
2944
+ endLine: metadata.endLine,
2945
+ nodeType: metadata.chunkType,
2946
+ name: metadata.name,
2947
+ language: metadata.language
2948
+ };
2949
+ this.database.upsertChunk(chunkData);
2950
+ chunkIds.push(key);
2951
+ }
2952
+ this.database.addChunksToBranch(this.currentBranch || "default", chunkIds);
2766
2953
  }
2767
2954
  async ensureInitialized() {
2768
- if (!this.store || !this.provider || !this.invertedIndex || !this.detectedProvider) {
2955
+ if (!this.store || !this.provider || !this.invertedIndex || !this.detectedProvider || !this.database) {
2769
2956
  await this.initialize();
2770
2957
  }
2771
2958
  return {
2772
2959
  store: this.store,
2773
2960
  provider: this.provider,
2774
2961
  invertedIndex: this.invertedIndex,
2775
- detectedProvider: this.detectedProvider
2962
+ detectedProvider: this.detectedProvider,
2963
+ database: this.database
2776
2964
  };
2777
2965
  }
2778
2966
  async estimateCost() {
@@ -2786,7 +2974,7 @@ var Indexer = class {
2786
2974
  return createCostEstimate(files, detectedProvider);
2787
2975
  }
2788
2976
  async index(onProgress) {
2789
- const { store, provider, invertedIndex } = await this.ensureInitialized();
2977
+ const { store, provider, invertedIndex, database, detectedProvider } = await this.ensureInitialized();
2790
2978
  const startTime = Date.now();
2791
2979
  const stats = {
2792
2980
  totalFiles: 0,
@@ -2825,7 +3013,7 @@ var Indexer = class {
2825
3013
  if (this.fileHashCache.get(f.path) === currentHash) {
2826
3014
  unchangedFilePaths.add(f.path);
2827
3015
  } else {
2828
- const content = await import_fs3.promises.readFile(f.path, "utf-8");
3016
+ const content = await import_fs4.promises.readFile(f.path, "utf-8");
2829
3017
  changedFiles.push({ path: f.path, content, hash: currentHash });
2830
3018
  }
2831
3019
  }
@@ -2860,7 +3048,7 @@ var Indexer = class {
2860
3048
  for (const parsed of parsedFiles) {
2861
3049
  currentFilePaths.add(parsed.path);
2862
3050
  if (parsed.chunks.length === 0) {
2863
- const relativePath = path4.relative(this.projectRoot, parsed.path);
3051
+ const relativePath = path5.relative(this.projectRoot, parsed.path);
2864
3052
  stats.parseFailures.push(relativePath);
2865
3053
  }
2866
3054
  let fileChunkCount = 0;
@@ -2874,6 +3062,17 @@ var Indexer = class {
2874
3062
  const id = generateChunkId(parsed.path, chunk);
2875
3063
  const contentHash = generateChunkHash(chunk);
2876
3064
  currentChunkIds.add(id);
3065
+ const chunkData = {
3066
+ chunkId: id,
3067
+ contentHash,
3068
+ filePath: parsed.path,
3069
+ startLine: chunk.startLine,
3070
+ endLine: chunk.endLine,
3071
+ nodeType: chunk.chunkType,
3072
+ name: chunk.name,
3073
+ language: chunk.language
3074
+ };
3075
+ database.upsertChunk(chunkData);
2877
3076
  if (existingChunks.get(id) === contentHash) {
2878
3077
  fileChunkCount++;
2879
3078
  continue;
@@ -2888,7 +3087,7 @@ var Indexer = class {
2888
3087
  language: chunk.language,
2889
3088
  hash: contentHash
2890
3089
  };
2891
- pendingChunks.push({ id, text, content: chunk.content, metadata });
3090
+ pendingChunks.push({ id, text, content: chunk.content, contentHash, metadata });
2892
3091
  fileChunkCount++;
2893
3092
  }
2894
3093
  }
@@ -2904,6 +3103,8 @@ var Indexer = class {
2904
3103
  stats.existingChunks = currentChunkIds.size - pendingChunks.length;
2905
3104
  stats.removedChunks = removedCount;
2906
3105
  if (pendingChunks.length === 0 && removedCount === 0) {
3106
+ database.clearBranch(this.currentBranch);
3107
+ database.addChunksToBranch(this.currentBranch, Array.from(currentChunkIds));
2907
3108
  this.fileHashCache = currentFileHashes;
2908
3109
  this.saveFileHashCache();
2909
3110
  stats.durationMs = Date.now() - startTime;
@@ -2917,6 +3118,8 @@ var Indexer = class {
2917
3118
  return stats;
2918
3119
  }
2919
3120
  if (pendingChunks.length === 0) {
3121
+ database.clearBranch(this.currentBranch);
3122
+ database.addChunksToBranch(this.currentBranch, Array.from(currentChunkIds));
2920
3123
  store.save();
2921
3124
  invertedIndex.save();
2922
3125
  this.fileHashCache = currentFileHashes;
@@ -2938,8 +3141,22 @@ var Indexer = class {
2938
3141
  chunksProcessed: 0,
2939
3142
  totalChunks: pendingChunks.length
2940
3143
  });
3144
+ const allContentHashes = pendingChunks.map((c) => c.contentHash);
3145
+ const missingHashes = new Set(database.getMissingEmbeddings(allContentHashes));
3146
+ const chunksNeedingEmbedding = pendingChunks.filter((c) => missingHashes.has(c.contentHash));
3147
+ const chunksWithExistingEmbedding = pendingChunks.filter((c) => !missingHashes.has(c.contentHash));
3148
+ for (const chunk of chunksWithExistingEmbedding) {
3149
+ const embeddingBuffer = database.getEmbedding(chunk.contentHash);
3150
+ if (embeddingBuffer) {
3151
+ const vector = bufferToFloat32Array(embeddingBuffer);
3152
+ store.add(chunk.id, Array.from(vector), chunk.metadata);
3153
+ invertedIndex.removeChunk(chunk.id);
3154
+ invertedIndex.addChunk(chunk.id, chunk.content);
3155
+ stats.indexedChunks++;
3156
+ }
3157
+ }
2941
3158
  const queue = new PQueue({ concurrency: 3 });
2942
- const dynamicBatches = createDynamicBatches(pendingChunks);
3159
+ const dynamicBatches = createDynamicBatches(chunksNeedingEmbedding);
2943
3160
  for (const batch of dynamicBatches) {
2944
3161
  queue.add(async () => {
2945
3162
  try {
@@ -2964,7 +3181,15 @@ var Indexer = class {
2964
3181
  metadata: chunk.metadata
2965
3182
  }));
2966
3183
  store.addBatch(items);
2967
- for (const chunk of batch) {
3184
+ for (let i = 0; i < batch.length; i++) {
3185
+ const chunk = batch[i];
3186
+ const embedding = result.embeddings[i];
3187
+ database.upsertEmbedding(
3188
+ chunk.contentHash,
3189
+ float32ArrayToBuffer(embedding),
3190
+ chunk.text,
3191
+ detectedProvider.modelInfo.model
3192
+ );
2968
3193
  invertedIndex.removeChunk(chunk.id);
2969
3194
  invertedIndex.addChunk(chunk.id, chunk.content);
2970
3195
  }
@@ -2992,6 +3217,8 @@ var Indexer = class {
2992
3217
  chunksProcessed: stats.indexedChunks,
2993
3218
  totalChunks: pendingChunks.length
2994
3219
  });
3220
+ database.clearBranch(this.currentBranch);
3221
+ database.addChunksToBranch(this.currentBranch, Array.from(currentChunkIds));
2995
3222
  store.save();
2996
3223
  invertedIndex.save();
2997
3224
  this.fileHashCache = currentFileHashes;
@@ -3010,18 +3237,24 @@ var Indexer = class {
3010
3237
  return stats;
3011
3238
  }
3012
3239
  async search(query, limit, options) {
3013
- const { store, provider } = await this.ensureInitialized();
3240
+ const { store, provider, database } = await this.ensureInitialized();
3014
3241
  if (store.count() === 0) {
3015
3242
  return [];
3016
3243
  }
3017
3244
  const maxResults = limit ?? this.config.search.maxResults;
3018
3245
  const hybridWeight = options?.hybridWeight ?? this.config.search.hybridWeight;
3246
+ const filterByBranch = options?.filterByBranch ?? true;
3019
3247
  const { embedding } = await provider.embed(query);
3020
3248
  const semanticResults = store.search(embedding, maxResults * 4);
3021
3249
  const keywordResults = await this.keywordSearch(query, maxResults * 4);
3022
3250
  const combined = this.fuseResults(semanticResults, keywordResults, hybridWeight, maxResults * 4);
3251
+ let branchChunkIds = null;
3252
+ if (filterByBranch && this.currentBranch !== "default") {
3253
+ branchChunkIds = new Set(database.getBranchChunkIds(this.currentBranch));
3254
+ }
3023
3255
  const filtered = combined.filter((r) => {
3024
3256
  if (r.score < this.config.search.minScore) return false;
3257
+ if (branchChunkIds && !branchChunkIds.has(r.id)) return false;
3025
3258
  if (options?.fileType) {
3026
3259
  const ext = r.metadata.filePath.split(".").pop()?.toLowerCase();
3027
3260
  if (ext !== options.fileType.toLowerCase().replace(/^\./, "")) return false;
@@ -3042,7 +3275,7 @@ var Indexer = class {
3042
3275
  let contextEndLine = r.metadata.endLine;
3043
3276
  if (this.config.search.includeContext) {
3044
3277
  try {
3045
- const fileContent = await import_fs3.promises.readFile(
3278
+ const fileContent = await import_fs4.promises.readFile(
3046
3279
  r.metadata.filePath,
3047
3280
  "utf-8"
3048
3281
  );
@@ -3123,7 +3356,9 @@ var Indexer = class {
3123
3356
  vectorCount: store.count(),
3124
3357
  provider: detectedProvider.provider,
3125
3358
  model: detectedProvider.modelInfo.model,
3126
- indexPath: this.indexPath
3359
+ indexPath: this.indexPath,
3360
+ currentBranch: this.currentBranch,
3361
+ baseBranch: this.baseBranch
3127
3362
  };
3128
3363
  }
3129
3364
  async clearIndex() {
@@ -3134,7 +3369,7 @@ var Indexer = class {
3134
3369
  invertedIndex.save();
3135
3370
  }
3136
3371
  async healthCheck() {
3137
- const { store, invertedIndex } = await this.ensureInitialized();
3372
+ const { store, invertedIndex, database } = await this.ensureInitialized();
3138
3373
  const allMetadata = store.getAllMetadata();
3139
3374
  const filePathsToChunkKeys = /* @__PURE__ */ new Map();
3140
3375
  for (const { key, metadata } of allMetadata) {
@@ -3145,12 +3380,13 @@ var Indexer = class {
3145
3380
  const removedFilePaths = [];
3146
3381
  let removedCount = 0;
3147
3382
  for (const [filePath, chunkKeys] of filePathsToChunkKeys) {
3148
- if (!(0, import_fs3.existsSync)(filePath)) {
3383
+ if (!(0, import_fs4.existsSync)(filePath)) {
3149
3384
  for (const key of chunkKeys) {
3150
3385
  store.remove(key);
3151
3386
  invertedIndex.removeChunk(key);
3152
3387
  removedCount++;
3153
3388
  }
3389
+ database.deleteChunksByFile(filePath);
3154
3390
  removedFilePaths.push(filePath);
3155
3391
  }
3156
3392
  }
@@ -3158,7 +3394,9 @@ var Indexer = class {
3158
3394
  store.save();
3159
3395
  invertedIndex.save();
3160
3396
  }
3161
- return { removed: removedCount, filePaths: removedFilePaths };
3397
+ const gcOrphanEmbeddings = database.gcOrphanEmbeddings();
3398
+ const gcOrphanChunks = database.gcOrphanChunks();
3399
+ return { removed: removedCount, filePaths: removedFilePaths, gcOrphanEmbeddings, gcOrphanChunks };
3162
3400
  }
3163
3401
  async retryFailedBatches() {
3164
3402
  const { store, provider, invertedIndex } = await this.ensureInitialized();
@@ -3212,6 +3450,22 @@ var Indexer = class {
3212
3450
  getFailedBatchesCount() {
3213
3451
  return this.loadFailedBatches().length;
3214
3452
  }
3453
+ getCurrentBranch() {
3454
+ return this.currentBranch;
3455
+ }
3456
+ getBaseBranch() {
3457
+ return this.baseBranch;
3458
+ }
3459
+ refreshBranchInfo() {
3460
+ if (isGitRepo(this.projectRoot)) {
3461
+ this.currentBranch = getBranchOrDefault(this.projectRoot);
3462
+ this.baseBranch = getBaseBranch(this.projectRoot);
3463
+ }
3464
+ }
3465
+ async getDatabaseStats() {
3466
+ const { database } = await this.ensureInitialized();
3467
+ return database.getStats();
3468
+ }
3215
3469
  };
3216
3470
 
3217
3471
  // node_modules/chokidar/index.js
@@ -3304,7 +3558,7 @@ var ReaddirpStream = class extends import_node_stream.Readable {
3304
3558
  this._directoryFilter = normalizeFilter(opts.directoryFilter);
3305
3559
  const statMethod = opts.lstat ? import_promises.lstat : import_promises.stat;
3306
3560
  if (wantBigintFsStats) {
3307
- this._stat = (path7) => statMethod(path7, { bigint: true });
3561
+ this._stat = (path8) => statMethod(path8, { bigint: true });
3308
3562
  } else {
3309
3563
  this._stat = statMethod;
3310
3564
  }
@@ -3329,8 +3583,8 @@ var ReaddirpStream = class extends import_node_stream.Readable {
3329
3583
  const par = this.parent;
3330
3584
  const fil = par && par.files;
3331
3585
  if (fil && fil.length > 0) {
3332
- const { path: path7, depth } = par;
3333
- const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path7));
3586
+ const { path: path8, depth } = par;
3587
+ const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path8));
3334
3588
  const awaited = await Promise.all(slice);
3335
3589
  for (const entry of awaited) {
3336
3590
  if (!entry)
@@ -3370,20 +3624,20 @@ var ReaddirpStream = class extends import_node_stream.Readable {
3370
3624
  this.reading = false;
3371
3625
  }
3372
3626
  }
3373
- async _exploreDir(path7, depth) {
3627
+ async _exploreDir(path8, depth) {
3374
3628
  let files;
3375
3629
  try {
3376
- files = await (0, import_promises.readdir)(path7, this._rdOptions);
3630
+ files = await (0, import_promises.readdir)(path8, this._rdOptions);
3377
3631
  } catch (error) {
3378
3632
  this._onError(error);
3379
3633
  }
3380
- return { files, depth, path: path7 };
3634
+ return { files, depth, path: path8 };
3381
3635
  }
3382
- async _formatEntry(dirent, path7) {
3636
+ async _formatEntry(dirent, path8) {
3383
3637
  let entry;
3384
3638
  const basename3 = this._isDirent ? dirent.name : dirent;
3385
3639
  try {
3386
- const fullPath = (0, import_node_path.resolve)((0, import_node_path.join)(path7, basename3));
3640
+ const fullPath = (0, import_node_path.resolve)((0, import_node_path.join)(path8, basename3));
3387
3641
  entry = { path: (0, import_node_path.relative)(this._root, fullPath), fullPath, basename: basename3 };
3388
3642
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
3389
3643
  } catch (err) {
@@ -3783,16 +4037,16 @@ var delFromSet = (main, prop, item) => {
3783
4037
  };
3784
4038
  var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
3785
4039
  var FsWatchInstances = /* @__PURE__ */ new Map();
3786
- function createFsWatchInstance(path7, options, listener, errHandler, emitRaw) {
4040
+ function createFsWatchInstance(path8, options, listener, errHandler, emitRaw) {
3787
4041
  const handleEvent = (rawEvent, evPath) => {
3788
- listener(path7);
3789
- emitRaw(rawEvent, evPath, { watchedPath: path7 });
3790
- if (evPath && path7 !== evPath) {
3791
- fsWatchBroadcast(sp.resolve(path7, evPath), KEY_LISTENERS, sp.join(path7, evPath));
4042
+ listener(path8);
4043
+ emitRaw(rawEvent, evPath, { watchedPath: path8 });
4044
+ if (evPath && path8 !== evPath) {
4045
+ fsWatchBroadcast(sp.resolve(path8, evPath), KEY_LISTENERS, sp.join(path8, evPath));
3792
4046
  }
3793
4047
  };
3794
4048
  try {
3795
- return (0, import_node_fs.watch)(path7, {
4049
+ return (0, import_node_fs.watch)(path8, {
3796
4050
  persistent: options.persistent
3797
4051
  }, handleEvent);
3798
4052
  } catch (error) {
@@ -3808,12 +4062,12 @@ var fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
3808
4062
  listener(val1, val2, val3);
3809
4063
  });
3810
4064
  };
3811
- var setFsWatchListener = (path7, fullPath, options, handlers) => {
4065
+ var setFsWatchListener = (path8, fullPath, options, handlers) => {
3812
4066
  const { listener, errHandler, rawEmitter } = handlers;
3813
4067
  let cont = FsWatchInstances.get(fullPath);
3814
4068
  let watcher;
3815
4069
  if (!options.persistent) {
3816
- watcher = createFsWatchInstance(path7, options, listener, errHandler, rawEmitter);
4070
+ watcher = createFsWatchInstance(path8, options, listener, errHandler, rawEmitter);
3817
4071
  if (!watcher)
3818
4072
  return;
3819
4073
  return watcher.close.bind(watcher);
@@ -3824,7 +4078,7 @@ var setFsWatchListener = (path7, fullPath, options, handlers) => {
3824
4078
  addAndConvert(cont, KEY_RAW, rawEmitter);
3825
4079
  } else {
3826
4080
  watcher = createFsWatchInstance(
3827
- path7,
4081
+ path8,
3828
4082
  options,
3829
4083
  fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS),
3830
4084
  errHandler,
@@ -3839,7 +4093,7 @@ var setFsWatchListener = (path7, fullPath, options, handlers) => {
3839
4093
  cont.watcherUnusable = true;
3840
4094
  if (isWindows && error.code === "EPERM") {
3841
4095
  try {
3842
- const fd = await (0, import_promises2.open)(path7, "r");
4096
+ const fd = await (0, import_promises2.open)(path8, "r");
3843
4097
  await fd.close();
3844
4098
  broadcastErr(error);
3845
4099
  } catch (err) {
@@ -3870,7 +4124,7 @@ var setFsWatchListener = (path7, fullPath, options, handlers) => {
3870
4124
  };
3871
4125
  };
3872
4126
  var FsWatchFileInstances = /* @__PURE__ */ new Map();
3873
- var setFsWatchFileListener = (path7, fullPath, options, handlers) => {
4127
+ var setFsWatchFileListener = (path8, fullPath, options, handlers) => {
3874
4128
  const { listener, rawEmitter } = handlers;
3875
4129
  let cont = FsWatchFileInstances.get(fullPath);
3876
4130
  const copts = cont && cont.options;
@@ -3892,7 +4146,7 @@ var setFsWatchFileListener = (path7, fullPath, options, handlers) => {
3892
4146
  });
3893
4147
  const currmtime = curr.mtimeMs;
3894
4148
  if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
3895
- foreach(cont.listeners, (listener2) => listener2(path7, curr));
4149
+ foreach(cont.listeners, (listener2) => listener2(path8, curr));
3896
4150
  }
3897
4151
  })
3898
4152
  };
@@ -3922,13 +4176,13 @@ var NodeFsHandler = class {
3922
4176
  * @param listener on fs change
3923
4177
  * @returns closer for the watcher instance
3924
4178
  */
3925
- _watchWithNodeFs(path7, listener) {
4179
+ _watchWithNodeFs(path8, listener) {
3926
4180
  const opts = this.fsw.options;
3927
- const directory = sp.dirname(path7);
3928
- const basename3 = sp.basename(path7);
4181
+ const directory = sp.dirname(path8);
4182
+ const basename3 = sp.basename(path8);
3929
4183
  const parent = this.fsw._getWatchedDir(directory);
3930
4184
  parent.add(basename3);
3931
- const absolutePath = sp.resolve(path7);
4185
+ const absolutePath = sp.resolve(path8);
3932
4186
  const options = {
3933
4187
  persistent: opts.persistent
3934
4188
  };
@@ -3938,12 +4192,12 @@ var NodeFsHandler = class {
3938
4192
  if (opts.usePolling) {
3939
4193
  const enableBin = opts.interval !== opts.binaryInterval;
3940
4194
  options.interval = enableBin && isBinaryPath(basename3) ? opts.binaryInterval : opts.interval;
3941
- closer = setFsWatchFileListener(path7, absolutePath, options, {
4195
+ closer = setFsWatchFileListener(path8, absolutePath, options, {
3942
4196
  listener,
3943
4197
  rawEmitter: this.fsw._emitRaw
3944
4198
  });
3945
4199
  } else {
3946
- closer = setFsWatchListener(path7, absolutePath, options, {
4200
+ closer = setFsWatchListener(path8, absolutePath, options, {
3947
4201
  listener,
3948
4202
  errHandler: this._boundHandleError,
3949
4203
  rawEmitter: this.fsw._emitRaw
@@ -3965,7 +4219,7 @@ var NodeFsHandler = class {
3965
4219
  let prevStats = stats;
3966
4220
  if (parent.has(basename3))
3967
4221
  return;
3968
- const listener = async (path7, newStats) => {
4222
+ const listener = async (path8, newStats) => {
3969
4223
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
3970
4224
  return;
3971
4225
  if (!newStats || newStats.mtimeMs === 0) {
@@ -3979,11 +4233,11 @@ var NodeFsHandler = class {
3979
4233
  this.fsw._emit(EV.CHANGE, file, newStats2);
3980
4234
  }
3981
4235
  if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats2.ino) {
3982
- this.fsw._closeFile(path7);
4236
+ this.fsw._closeFile(path8);
3983
4237
  prevStats = newStats2;
3984
4238
  const closer2 = this._watchWithNodeFs(file, listener);
3985
4239
  if (closer2)
3986
- this.fsw._addPathCloser(path7, closer2);
4240
+ this.fsw._addPathCloser(path8, closer2);
3987
4241
  } else {
3988
4242
  prevStats = newStats2;
3989
4243
  }
@@ -4015,7 +4269,7 @@ var NodeFsHandler = class {
4015
4269
  * @param item basename of this item
4016
4270
  * @returns true if no more processing is needed for this entry.
4017
4271
  */
4018
- async _handleSymlink(entry, directory, path7, item) {
4272
+ async _handleSymlink(entry, directory, path8, item) {
4019
4273
  if (this.fsw.closed) {
4020
4274
  return;
4021
4275
  }
@@ -4025,7 +4279,7 @@ var NodeFsHandler = class {
4025
4279
  this.fsw._incrReadyCount();
4026
4280
  let linkPath;
4027
4281
  try {
4028
- linkPath = await (0, import_promises2.realpath)(path7);
4282
+ linkPath = await (0, import_promises2.realpath)(path8);
4029
4283
  } catch (e) {
4030
4284
  this.fsw._emitReady();
4031
4285
  return true;
@@ -4035,12 +4289,12 @@ var NodeFsHandler = class {
4035
4289
  if (dir.has(item)) {
4036
4290
  if (this.fsw._symlinkPaths.get(full) !== linkPath) {
4037
4291
  this.fsw._symlinkPaths.set(full, linkPath);
4038
- this.fsw._emit(EV.CHANGE, path7, entry.stats);
4292
+ this.fsw._emit(EV.CHANGE, path8, entry.stats);
4039
4293
  }
4040
4294
  } else {
4041
4295
  dir.add(item);
4042
4296
  this.fsw._symlinkPaths.set(full, linkPath);
4043
- this.fsw._emit(EV.ADD, path7, entry.stats);
4297
+ this.fsw._emit(EV.ADD, path8, entry.stats);
4044
4298
  }
4045
4299
  this.fsw._emitReady();
4046
4300
  return true;
@@ -4070,9 +4324,9 @@ var NodeFsHandler = class {
4070
4324
  return;
4071
4325
  }
4072
4326
  const item = entry.path;
4073
- let path7 = sp.join(directory, item);
4327
+ let path8 = sp.join(directory, item);
4074
4328
  current.add(item);
4075
- if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path7, item)) {
4329
+ if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path8, item)) {
4076
4330
  return;
4077
4331
  }
4078
4332
  if (this.fsw.closed) {
@@ -4081,8 +4335,8 @@ var NodeFsHandler = class {
4081
4335
  }
4082
4336
  if (item === target || !target && !previous.has(item)) {
4083
4337
  this.fsw._incrReadyCount();
4084
- path7 = sp.join(dir, sp.relative(dir, path7));
4085
- this._addToNodeFs(path7, initialAdd, wh, depth + 1);
4338
+ path8 = sp.join(dir, sp.relative(dir, path8));
4339
+ this._addToNodeFs(path8, initialAdd, wh, depth + 1);
4086
4340
  }
4087
4341
  }).on(EV.ERROR, this._boundHandleError);
4088
4342
  return new Promise((resolve4, reject) => {
@@ -4151,13 +4405,13 @@ var NodeFsHandler = class {
4151
4405
  * @param depth Child path actually targeted for watch
4152
4406
  * @param target Child path actually targeted for watch
4153
4407
  */
4154
- async _addToNodeFs(path7, initialAdd, priorWh, depth, target) {
4408
+ async _addToNodeFs(path8, initialAdd, priorWh, depth, target) {
4155
4409
  const ready = this.fsw._emitReady;
4156
- if (this.fsw._isIgnored(path7) || this.fsw.closed) {
4410
+ if (this.fsw._isIgnored(path8) || this.fsw.closed) {
4157
4411
  ready();
4158
4412
  return false;
4159
4413
  }
4160
- const wh = this.fsw._getWatchHelpers(path7);
4414
+ const wh = this.fsw._getWatchHelpers(path8);
4161
4415
  if (priorWh) {
4162
4416
  wh.filterPath = (entry) => priorWh.filterPath(entry);
4163
4417
  wh.filterDir = (entry) => priorWh.filterDir(entry);
@@ -4173,8 +4427,8 @@ var NodeFsHandler = class {
4173
4427
  const follow = this.fsw.options.followSymlinks;
4174
4428
  let closer;
4175
4429
  if (stats.isDirectory()) {
4176
- const absPath = sp.resolve(path7);
4177
- const targetPath = follow ? await (0, import_promises2.realpath)(path7) : path7;
4430
+ const absPath = sp.resolve(path8);
4431
+ const targetPath = follow ? await (0, import_promises2.realpath)(path8) : path8;
4178
4432
  if (this.fsw.closed)
4179
4433
  return;
4180
4434
  closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
@@ -4184,29 +4438,29 @@ var NodeFsHandler = class {
4184
4438
  this.fsw._symlinkPaths.set(absPath, targetPath);
4185
4439
  }
4186
4440
  } else if (stats.isSymbolicLink()) {
4187
- const targetPath = follow ? await (0, import_promises2.realpath)(path7) : path7;
4441
+ const targetPath = follow ? await (0, import_promises2.realpath)(path8) : path8;
4188
4442
  if (this.fsw.closed)
4189
4443
  return;
4190
4444
  const parent = sp.dirname(wh.watchPath);
4191
4445
  this.fsw._getWatchedDir(parent).add(wh.watchPath);
4192
4446
  this.fsw._emit(EV.ADD, wh.watchPath, stats);
4193
- closer = await this._handleDir(parent, stats, initialAdd, depth, path7, wh, targetPath);
4447
+ closer = await this._handleDir(parent, stats, initialAdd, depth, path8, wh, targetPath);
4194
4448
  if (this.fsw.closed)
4195
4449
  return;
4196
4450
  if (targetPath !== void 0) {
4197
- this.fsw._symlinkPaths.set(sp.resolve(path7), targetPath);
4451
+ this.fsw._symlinkPaths.set(sp.resolve(path8), targetPath);
4198
4452
  }
4199
4453
  } else {
4200
4454
  closer = this._handleFile(wh.watchPath, stats, initialAdd);
4201
4455
  }
4202
4456
  ready();
4203
4457
  if (closer)
4204
- this.fsw._addPathCloser(path7, closer);
4458
+ this.fsw._addPathCloser(path8, closer);
4205
4459
  return false;
4206
4460
  } catch (error) {
4207
4461
  if (this.fsw._handleError(error)) {
4208
4462
  ready();
4209
- return path7;
4463
+ return path8;
4210
4464
  }
4211
4465
  }
4212
4466
  }
@@ -4249,24 +4503,24 @@ function createPattern(matcher) {
4249
4503
  }
4250
4504
  return () => false;
4251
4505
  }
4252
- function normalizePath(path7) {
4253
- if (typeof path7 !== "string")
4506
+ function normalizePath(path8) {
4507
+ if (typeof path8 !== "string")
4254
4508
  throw new Error("string expected");
4255
- path7 = sp2.normalize(path7);
4256
- path7 = path7.replace(/\\/g, "/");
4509
+ path8 = sp2.normalize(path8);
4510
+ path8 = path8.replace(/\\/g, "/");
4257
4511
  let prepend = false;
4258
- if (path7.startsWith("//"))
4512
+ if (path8.startsWith("//"))
4259
4513
  prepend = true;
4260
- path7 = path7.replace(DOUBLE_SLASH_RE, "/");
4514
+ path8 = path8.replace(DOUBLE_SLASH_RE, "/");
4261
4515
  if (prepend)
4262
- path7 = "/" + path7;
4263
- return path7;
4516
+ path8 = "/" + path8;
4517
+ return path8;
4264
4518
  }
4265
4519
  function matchPatterns(patterns, testString, stats) {
4266
- const path7 = normalizePath(testString);
4520
+ const path8 = normalizePath(testString);
4267
4521
  for (let index = 0; index < patterns.length; index++) {
4268
4522
  const pattern = patterns[index];
4269
- if (pattern(path7, stats)) {
4523
+ if (pattern(path8, stats)) {
4270
4524
  return true;
4271
4525
  }
4272
4526
  }
@@ -4304,19 +4558,19 @@ var toUnix = (string) => {
4304
4558
  }
4305
4559
  return str;
4306
4560
  };
4307
- var normalizePathToUnix = (path7) => toUnix(sp2.normalize(toUnix(path7)));
4308
- var normalizeIgnored = (cwd = "") => (path7) => {
4309
- if (typeof path7 === "string") {
4310
- return normalizePathToUnix(sp2.isAbsolute(path7) ? path7 : sp2.join(cwd, path7));
4561
+ var normalizePathToUnix = (path8) => toUnix(sp2.normalize(toUnix(path8)));
4562
+ var normalizeIgnored = (cwd = "") => (path8) => {
4563
+ if (typeof path8 === "string") {
4564
+ return normalizePathToUnix(sp2.isAbsolute(path8) ? path8 : sp2.join(cwd, path8));
4311
4565
  } else {
4312
- return path7;
4566
+ return path8;
4313
4567
  }
4314
4568
  };
4315
- var getAbsolutePath = (path7, cwd) => {
4316
- if (sp2.isAbsolute(path7)) {
4317
- return path7;
4569
+ var getAbsolutePath = (path8, cwd) => {
4570
+ if (sp2.isAbsolute(path8)) {
4571
+ return path8;
4318
4572
  }
4319
- return sp2.join(cwd, path7);
4573
+ return sp2.join(cwd, path8);
4320
4574
  };
4321
4575
  var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
4322
4576
  var DirEntry = class {
@@ -4381,10 +4635,10 @@ var WatchHelper = class {
4381
4635
  dirParts;
4382
4636
  followSymlinks;
4383
4637
  statMethod;
4384
- constructor(path7, follow, fsw) {
4638
+ constructor(path8, follow, fsw) {
4385
4639
  this.fsw = fsw;
4386
- const watchPath = path7;
4387
- this.path = path7 = path7.replace(REPLACER_RE, "");
4640
+ const watchPath = path8;
4641
+ this.path = path8 = path8.replace(REPLACER_RE, "");
4388
4642
  this.watchPath = watchPath;
4389
4643
  this.fullWatchPath = sp2.resolve(watchPath);
4390
4644
  this.dirParts = [];
@@ -4524,20 +4778,20 @@ var FSWatcher = class extends import_node_events.EventEmitter {
4524
4778
  this._closePromise = void 0;
4525
4779
  let paths = unifyPaths(paths_);
4526
4780
  if (cwd) {
4527
- paths = paths.map((path7) => {
4528
- const absPath = getAbsolutePath(path7, cwd);
4781
+ paths = paths.map((path8) => {
4782
+ const absPath = getAbsolutePath(path8, cwd);
4529
4783
  return absPath;
4530
4784
  });
4531
4785
  }
4532
- paths.forEach((path7) => {
4533
- this._removeIgnoredPath(path7);
4786
+ paths.forEach((path8) => {
4787
+ this._removeIgnoredPath(path8);
4534
4788
  });
4535
4789
  this._userIgnored = void 0;
4536
4790
  if (!this._readyCount)
4537
4791
  this._readyCount = 0;
4538
4792
  this._readyCount += paths.length;
4539
- Promise.all(paths.map(async (path7) => {
4540
- const res = await this._nodeFsHandler._addToNodeFs(path7, !_internal, void 0, 0, _origAdd);
4793
+ Promise.all(paths.map(async (path8) => {
4794
+ const res = await this._nodeFsHandler._addToNodeFs(path8, !_internal, void 0, 0, _origAdd);
4541
4795
  if (res)
4542
4796
  this._emitReady();
4543
4797
  return res;
@@ -4559,17 +4813,17 @@ var FSWatcher = class extends import_node_events.EventEmitter {
4559
4813
  return this;
4560
4814
  const paths = unifyPaths(paths_);
4561
4815
  const { cwd } = this.options;
4562
- paths.forEach((path7) => {
4563
- if (!sp2.isAbsolute(path7) && !this._closers.has(path7)) {
4816
+ paths.forEach((path8) => {
4817
+ if (!sp2.isAbsolute(path8) && !this._closers.has(path8)) {
4564
4818
  if (cwd)
4565
- path7 = sp2.join(cwd, path7);
4566
- path7 = sp2.resolve(path7);
4819
+ path8 = sp2.join(cwd, path8);
4820
+ path8 = sp2.resolve(path8);
4567
4821
  }
4568
- this._closePath(path7);
4569
- this._addIgnoredPath(path7);
4570
- if (this._watched.has(path7)) {
4822
+ this._closePath(path8);
4823
+ this._addIgnoredPath(path8);
4824
+ if (this._watched.has(path8)) {
4571
4825
  this._addIgnoredPath({
4572
- path: path7,
4826
+ path: path8,
4573
4827
  recursive: true
4574
4828
  });
4575
4829
  }
@@ -4633,38 +4887,38 @@ var FSWatcher = class extends import_node_events.EventEmitter {
4633
4887
  * @param stats arguments to be passed with event
4634
4888
  * @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
4635
4889
  */
4636
- async _emit(event, path7, stats) {
4890
+ async _emit(event, path8, stats) {
4637
4891
  if (this.closed)
4638
4892
  return;
4639
4893
  const opts = this.options;
4640
4894
  if (isWindows)
4641
- path7 = sp2.normalize(path7);
4895
+ path8 = sp2.normalize(path8);
4642
4896
  if (opts.cwd)
4643
- path7 = sp2.relative(opts.cwd, path7);
4644
- const args = [path7];
4897
+ path8 = sp2.relative(opts.cwd, path8);
4898
+ const args = [path8];
4645
4899
  if (stats != null)
4646
4900
  args.push(stats);
4647
4901
  const awf = opts.awaitWriteFinish;
4648
4902
  let pw;
4649
- if (awf && (pw = this._pendingWrites.get(path7))) {
4903
+ if (awf && (pw = this._pendingWrites.get(path8))) {
4650
4904
  pw.lastChange = /* @__PURE__ */ new Date();
4651
4905
  return this;
4652
4906
  }
4653
4907
  if (opts.atomic) {
4654
4908
  if (event === EVENTS.UNLINK) {
4655
- this._pendingUnlinks.set(path7, [event, ...args]);
4909
+ this._pendingUnlinks.set(path8, [event, ...args]);
4656
4910
  setTimeout(() => {
4657
- this._pendingUnlinks.forEach((entry, path8) => {
4911
+ this._pendingUnlinks.forEach((entry, path9) => {
4658
4912
  this.emit(...entry);
4659
4913
  this.emit(EVENTS.ALL, ...entry);
4660
- this._pendingUnlinks.delete(path8);
4914
+ this._pendingUnlinks.delete(path9);
4661
4915
  });
4662
4916
  }, typeof opts.atomic === "number" ? opts.atomic : 100);
4663
4917
  return this;
4664
4918
  }
4665
- if (event === EVENTS.ADD && this._pendingUnlinks.has(path7)) {
4919
+ if (event === EVENTS.ADD && this._pendingUnlinks.has(path8)) {
4666
4920
  event = EVENTS.CHANGE;
4667
- this._pendingUnlinks.delete(path7);
4921
+ this._pendingUnlinks.delete(path8);
4668
4922
  }
4669
4923
  }
4670
4924
  if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) {
@@ -4682,16 +4936,16 @@ var FSWatcher = class extends import_node_events.EventEmitter {
4682
4936
  this.emitWithAll(event, args);
4683
4937
  }
4684
4938
  };
4685
- this._awaitWriteFinish(path7, awf.stabilityThreshold, event, awfEmit);
4939
+ this._awaitWriteFinish(path8, awf.stabilityThreshold, event, awfEmit);
4686
4940
  return this;
4687
4941
  }
4688
4942
  if (event === EVENTS.CHANGE) {
4689
- const isThrottled = !this._throttle(EVENTS.CHANGE, path7, 50);
4943
+ const isThrottled = !this._throttle(EVENTS.CHANGE, path8, 50);
4690
4944
  if (isThrottled)
4691
4945
  return this;
4692
4946
  }
4693
4947
  if (opts.alwaysStat && stats === void 0 && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) {
4694
- const fullPath = opts.cwd ? sp2.join(opts.cwd, path7) : path7;
4948
+ const fullPath = opts.cwd ? sp2.join(opts.cwd, path8) : path8;
4695
4949
  let stats2;
4696
4950
  try {
4697
4951
  stats2 = await (0, import_promises3.stat)(fullPath);
@@ -4722,23 +4976,23 @@ var FSWatcher = class extends import_node_events.EventEmitter {
4722
4976
  * @param timeout duration of time to suppress duplicate actions
4723
4977
  * @returns tracking object or false if action should be suppressed
4724
4978
  */
4725
- _throttle(actionType, path7, timeout) {
4979
+ _throttle(actionType, path8, timeout) {
4726
4980
  if (!this._throttled.has(actionType)) {
4727
4981
  this._throttled.set(actionType, /* @__PURE__ */ new Map());
4728
4982
  }
4729
4983
  const action = this._throttled.get(actionType);
4730
4984
  if (!action)
4731
4985
  throw new Error("invalid throttle");
4732
- const actionPath = action.get(path7);
4986
+ const actionPath = action.get(path8);
4733
4987
  if (actionPath) {
4734
4988
  actionPath.count++;
4735
4989
  return false;
4736
4990
  }
4737
4991
  let timeoutObject;
4738
4992
  const clear = () => {
4739
- const item = action.get(path7);
4993
+ const item = action.get(path8);
4740
4994
  const count = item ? item.count : 0;
4741
- action.delete(path7);
4995
+ action.delete(path8);
4742
4996
  clearTimeout(timeoutObject);
4743
4997
  if (item)
4744
4998
  clearTimeout(item.timeoutObject);
@@ -4746,7 +5000,7 @@ var FSWatcher = class extends import_node_events.EventEmitter {
4746
5000
  };
4747
5001
  timeoutObject = setTimeout(clear, timeout);
4748
5002
  const thr = { timeoutObject, clear, count: 0 };
4749
- action.set(path7, thr);
5003
+ action.set(path8, thr);
4750
5004
  return thr;
4751
5005
  }
4752
5006
  _incrReadyCount() {
@@ -4760,44 +5014,44 @@ var FSWatcher = class extends import_node_events.EventEmitter {
4760
5014
  * @param event
4761
5015
  * @param awfEmit Callback to be called when ready for event to be emitted.
4762
5016
  */
4763
- _awaitWriteFinish(path7, threshold, event, awfEmit) {
5017
+ _awaitWriteFinish(path8, threshold, event, awfEmit) {
4764
5018
  const awf = this.options.awaitWriteFinish;
4765
5019
  if (typeof awf !== "object")
4766
5020
  return;
4767
5021
  const pollInterval = awf.pollInterval;
4768
5022
  let timeoutHandler;
4769
- let fullPath = path7;
4770
- if (this.options.cwd && !sp2.isAbsolute(path7)) {
4771
- fullPath = sp2.join(this.options.cwd, path7);
5023
+ let fullPath = path8;
5024
+ if (this.options.cwd && !sp2.isAbsolute(path8)) {
5025
+ fullPath = sp2.join(this.options.cwd, path8);
4772
5026
  }
4773
5027
  const now = /* @__PURE__ */ new Date();
4774
5028
  const writes = this._pendingWrites;
4775
5029
  function awaitWriteFinishFn(prevStat) {
4776
5030
  (0, import_node_fs2.stat)(fullPath, (err, curStat) => {
4777
- if (err || !writes.has(path7)) {
5031
+ if (err || !writes.has(path8)) {
4778
5032
  if (err && err.code !== "ENOENT")
4779
5033
  awfEmit(err);
4780
5034
  return;
4781
5035
  }
4782
5036
  const now2 = Number(/* @__PURE__ */ new Date());
4783
5037
  if (prevStat && curStat.size !== prevStat.size) {
4784
- writes.get(path7).lastChange = now2;
5038
+ writes.get(path8).lastChange = now2;
4785
5039
  }
4786
- const pw = writes.get(path7);
5040
+ const pw = writes.get(path8);
4787
5041
  const df = now2 - pw.lastChange;
4788
5042
  if (df >= threshold) {
4789
- writes.delete(path7);
5043
+ writes.delete(path8);
4790
5044
  awfEmit(void 0, curStat);
4791
5045
  } else {
4792
5046
  timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
4793
5047
  }
4794
5048
  });
4795
5049
  }
4796
- if (!writes.has(path7)) {
4797
- writes.set(path7, {
5050
+ if (!writes.has(path8)) {
5051
+ writes.set(path8, {
4798
5052
  lastChange: now,
4799
5053
  cancelWait: () => {
4800
- writes.delete(path7);
5054
+ writes.delete(path8);
4801
5055
  clearTimeout(timeoutHandler);
4802
5056
  return event;
4803
5057
  }
@@ -4808,8 +5062,8 @@ var FSWatcher = class extends import_node_events.EventEmitter {
4808
5062
  /**
4809
5063
  * Determines whether user has asked to ignore this path.
4810
5064
  */
4811
- _isIgnored(path7, stats) {
4812
- if (this.options.atomic && DOT_RE.test(path7))
5065
+ _isIgnored(path8, stats) {
5066
+ if (this.options.atomic && DOT_RE.test(path8))
4813
5067
  return true;
4814
5068
  if (!this._userIgnored) {
4815
5069
  const { cwd } = this.options;
@@ -4819,17 +5073,17 @@ var FSWatcher = class extends import_node_events.EventEmitter {
4819
5073
  const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
4820
5074
  this._userIgnored = anymatch(list, void 0);
4821
5075
  }
4822
- return this._userIgnored(path7, stats);
5076
+ return this._userIgnored(path8, stats);
4823
5077
  }
4824
- _isntIgnored(path7, stat4) {
4825
- return !this._isIgnored(path7, stat4);
5078
+ _isntIgnored(path8, stat4) {
5079
+ return !this._isIgnored(path8, stat4);
4826
5080
  }
4827
5081
  /**
4828
5082
  * Provides a set of common helpers and properties relating to symlink handling.
4829
5083
  * @param path file or directory pattern being watched
4830
5084
  */
4831
- _getWatchHelpers(path7) {
4832
- return new WatchHelper(path7, this.options.followSymlinks, this);
5085
+ _getWatchHelpers(path8) {
5086
+ return new WatchHelper(path8, this.options.followSymlinks, this);
4833
5087
  }
4834
5088
  // Directory helpers
4835
5089
  // -----------------
@@ -4861,63 +5115,63 @@ var FSWatcher = class extends import_node_events.EventEmitter {
4861
5115
  * @param item base path of item/directory
4862
5116
  */
4863
5117
  _remove(directory, item, isDirectory) {
4864
- const path7 = sp2.join(directory, item);
4865
- const fullPath = sp2.resolve(path7);
4866
- isDirectory = isDirectory != null ? isDirectory : this._watched.has(path7) || this._watched.has(fullPath);
4867
- if (!this._throttle("remove", path7, 100))
5118
+ const path8 = sp2.join(directory, item);
5119
+ const fullPath = sp2.resolve(path8);
5120
+ isDirectory = isDirectory != null ? isDirectory : this._watched.has(path8) || this._watched.has(fullPath);
5121
+ if (!this._throttle("remove", path8, 100))
4868
5122
  return;
4869
5123
  if (!isDirectory && this._watched.size === 1) {
4870
5124
  this.add(directory, item, true);
4871
5125
  }
4872
- const wp = this._getWatchedDir(path7);
5126
+ const wp = this._getWatchedDir(path8);
4873
5127
  const nestedDirectoryChildren = wp.getChildren();
4874
- nestedDirectoryChildren.forEach((nested) => this._remove(path7, nested));
5128
+ nestedDirectoryChildren.forEach((nested) => this._remove(path8, nested));
4875
5129
  const parent = this._getWatchedDir(directory);
4876
5130
  const wasTracked = parent.has(item);
4877
5131
  parent.remove(item);
4878
5132
  if (this._symlinkPaths.has(fullPath)) {
4879
5133
  this._symlinkPaths.delete(fullPath);
4880
5134
  }
4881
- let relPath = path7;
5135
+ let relPath = path8;
4882
5136
  if (this.options.cwd)
4883
- relPath = sp2.relative(this.options.cwd, path7);
5137
+ relPath = sp2.relative(this.options.cwd, path8);
4884
5138
  if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
4885
5139
  const event = this._pendingWrites.get(relPath).cancelWait();
4886
5140
  if (event === EVENTS.ADD)
4887
5141
  return;
4888
5142
  }
4889
- this._watched.delete(path7);
5143
+ this._watched.delete(path8);
4890
5144
  this._watched.delete(fullPath);
4891
5145
  const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK;
4892
- if (wasTracked && !this._isIgnored(path7))
4893
- this._emit(eventName, path7);
4894
- this._closePath(path7);
5146
+ if (wasTracked && !this._isIgnored(path8))
5147
+ this._emit(eventName, path8);
5148
+ this._closePath(path8);
4895
5149
  }
4896
5150
  /**
4897
5151
  * Closes all watchers for a path
4898
5152
  */
4899
- _closePath(path7) {
4900
- this._closeFile(path7);
4901
- const dir = sp2.dirname(path7);
4902
- this._getWatchedDir(dir).remove(sp2.basename(path7));
5153
+ _closePath(path8) {
5154
+ this._closeFile(path8);
5155
+ const dir = sp2.dirname(path8);
5156
+ this._getWatchedDir(dir).remove(sp2.basename(path8));
4903
5157
  }
4904
5158
  /**
4905
5159
  * Closes only file-specific watchers
4906
5160
  */
4907
- _closeFile(path7) {
4908
- const closers = this._closers.get(path7);
5161
+ _closeFile(path8) {
5162
+ const closers = this._closers.get(path8);
4909
5163
  if (!closers)
4910
5164
  return;
4911
5165
  closers.forEach((closer) => closer());
4912
- this._closers.delete(path7);
5166
+ this._closers.delete(path8);
4913
5167
  }
4914
- _addPathCloser(path7, closer) {
5168
+ _addPathCloser(path8, closer) {
4915
5169
  if (!closer)
4916
5170
  return;
4917
- let list = this._closers.get(path7);
5171
+ let list = this._closers.get(path8);
4918
5172
  if (!list) {
4919
5173
  list = [];
4920
- this._closers.set(path7, list);
5174
+ this._closers.set(path8, list);
4921
5175
  }
4922
5176
  list.push(closer);
4923
5177
  }
@@ -4947,7 +5201,7 @@ function watch(paths, options = {}) {
4947
5201
  var chokidar_default = { watch, FSWatcher };
4948
5202
 
4949
5203
  // src/watcher/index.ts
4950
- var path5 = __toESM(require("path"), 1);
5204
+ var path6 = __toESM(require("path"), 1);
4951
5205
  var FileWatcher = class {
4952
5206
  watcher = null;
4953
5207
  projectRoot;
@@ -4968,7 +5222,7 @@ var FileWatcher = class {
4968
5222
  const ignoreFilter = createIgnoreFilter(this.projectRoot);
4969
5223
  this.watcher = chokidar_default.watch(this.projectRoot, {
4970
5224
  ignored: (filePath) => {
4971
- const relativePath = path5.relative(this.projectRoot, filePath);
5225
+ const relativePath = path6.relative(this.projectRoot, filePath);
4972
5226
  if (!relativePath) return false;
4973
5227
  if (ignoreFilter.ignores(relativePath)) {
4974
5228
  return true;
@@ -5012,7 +5266,7 @@ var FileWatcher = class {
5012
5266
  return;
5013
5267
  }
5014
5268
  const changes = Array.from(this.pendingChanges.entries()).map(
5015
- ([path7, type]) => ({ path: path7, type })
5269
+ ([path8, type]) => ({ path: path8, type })
5016
5270
  );
5017
5271
  this.pendingChanges.clear();
5018
5272
  try {
@@ -5037,9 +5291,82 @@ var FileWatcher = class {
5037
5291
  return this.watcher !== null;
5038
5292
  }
5039
5293
  };
5294
+ var GitHeadWatcher = class {
5295
+ watcher = null;
5296
+ projectRoot;
5297
+ currentBranch = null;
5298
+ onBranchChange = null;
5299
+ debounceTimer = null;
5300
+ debounceMs = 100;
5301
+ // Short debounce for git operations
5302
+ constructor(projectRoot) {
5303
+ this.projectRoot = projectRoot;
5304
+ }
5305
+ start(handler) {
5306
+ if (this.watcher) {
5307
+ return;
5308
+ }
5309
+ if (!isGitRepo(this.projectRoot)) {
5310
+ return;
5311
+ }
5312
+ this.onBranchChange = handler;
5313
+ this.currentBranch = getCurrentBranch(this.projectRoot);
5314
+ const headPath = getHeadPath(this.projectRoot);
5315
+ const refsPath = path6.join(this.projectRoot, ".git", "refs", "heads");
5316
+ this.watcher = chokidar_default.watch([headPath, refsPath], {
5317
+ persistent: true,
5318
+ ignoreInitial: true,
5319
+ awaitWriteFinish: {
5320
+ stabilityThreshold: 50,
5321
+ pollInterval: 10
5322
+ }
5323
+ });
5324
+ this.watcher.on("change", () => this.handleHeadChange());
5325
+ this.watcher.on("add", () => this.handleHeadChange());
5326
+ }
5327
+ handleHeadChange() {
5328
+ if (this.debounceTimer) {
5329
+ clearTimeout(this.debounceTimer);
5330
+ }
5331
+ this.debounceTimer = setTimeout(() => {
5332
+ this.checkBranchChange();
5333
+ }, this.debounceMs);
5334
+ }
5335
+ async checkBranchChange() {
5336
+ const newBranch = getCurrentBranch(this.projectRoot);
5337
+ if (newBranch && newBranch !== this.currentBranch && this.onBranchChange) {
5338
+ const oldBranch = this.currentBranch;
5339
+ this.currentBranch = newBranch;
5340
+ try {
5341
+ await this.onBranchChange(oldBranch, newBranch);
5342
+ } catch (error) {
5343
+ console.error("Error handling branch change:", error);
5344
+ }
5345
+ } else if (newBranch) {
5346
+ this.currentBranch = newBranch;
5347
+ }
5348
+ }
5349
+ getCurrentBranch() {
5350
+ return this.currentBranch;
5351
+ }
5352
+ stop() {
5353
+ if (this.debounceTimer) {
5354
+ clearTimeout(this.debounceTimer);
5355
+ this.debounceTimer = null;
5356
+ }
5357
+ if (this.watcher) {
5358
+ this.watcher.close();
5359
+ this.watcher = null;
5360
+ }
5361
+ this.onBranchChange = null;
5362
+ }
5363
+ isRunning() {
5364
+ return this.watcher !== null;
5365
+ }
5366
+ };
5040
5367
  function createWatcherWithIndexer(indexer, projectRoot, config) {
5041
- const watcher = new FileWatcher(projectRoot, config);
5042
- watcher.start(async (changes) => {
5368
+ const fileWatcher = new FileWatcher(projectRoot, config);
5369
+ fileWatcher.start(async (changes) => {
5043
5370
  const hasAddOrChange = changes.some(
5044
5371
  (c) => c.type === "add" || c.type === "change"
5045
5372
  );
@@ -5048,7 +5375,22 @@ function createWatcherWithIndexer(indexer, projectRoot, config) {
5048
5375
  await indexer.index();
5049
5376
  }
5050
5377
  });
5051
- return watcher;
5378
+ let gitWatcher = null;
5379
+ if (isGitRepo(projectRoot)) {
5380
+ gitWatcher = new GitHeadWatcher(projectRoot);
5381
+ gitWatcher.start(async (oldBranch, newBranch) => {
5382
+ console.log(`Branch changed: ${oldBranch ?? "(none)"} -> ${newBranch}`);
5383
+ await indexer.index();
5384
+ });
5385
+ }
5386
+ return {
5387
+ fileWatcher,
5388
+ gitWatcher,
5389
+ stop() {
5390
+ fileWatcher.stop();
5391
+ gitWatcher?.stop();
5392
+ }
5393
+ };
5052
5394
  }
5053
5395
 
5054
5396
  // src/tools/index.ts
@@ -5132,13 +5474,19 @@ var index_health_check = (0, import_plugin.tool)({
5132
5474
  async execute() {
5133
5475
  const indexer = getIndexer();
5134
5476
  const result = await indexer.healthCheck();
5135
- if (result.removed === 0) {
5477
+ if (result.removed === 0 && result.gcOrphanEmbeddings === 0 && result.gcOrphanChunks === 0) {
5136
5478
  return "Index is healthy. No stale entries found.";
5137
5479
  }
5138
- const lines = [
5139
- `Health check complete:`,
5140
- ` Removed stale entries: ${result.removed}`
5141
- ];
5480
+ const lines = [`Health check complete:`];
5481
+ if (result.removed > 0) {
5482
+ lines.push(` Removed stale entries: ${result.removed}`);
5483
+ }
5484
+ if (result.gcOrphanEmbeddings > 0) {
5485
+ lines.push(` Garbage collected orphan embeddings: ${result.gcOrphanEmbeddings}`);
5486
+ }
5487
+ if (result.gcOrphanChunks > 0) {
5488
+ lines.push(` Garbage collected orphan chunks: ${result.gcOrphanChunks}`);
5489
+ }
5142
5490
  if (result.filePaths.length > 0) {
5143
5491
  lines.push(` Cleaned paths: ${result.filePaths.join(", ")}`);
5144
5492
  }
@@ -5193,21 +5541,26 @@ function formatStatus(status) {
5193
5541
  if (!status.indexed) {
5194
5542
  return "Codebase is not indexed. Run index_codebase to create an index.";
5195
5543
  }
5196
- return [
5544
+ const lines = [
5197
5545
  `Index status:`,
5198
5546
  ` Indexed chunks: ${status.vectorCount.toLocaleString()}`,
5199
5547
  ` Provider: ${status.provider}`,
5200
5548
  ` Model: ${status.model}`,
5201
5549
  ` Location: ${status.indexPath}`
5202
- ].join("\n");
5550
+ ];
5551
+ if (status.currentBranch !== "default") {
5552
+ lines.push(` Current branch: ${status.currentBranch}`);
5553
+ lines.push(` Base branch: ${status.baseBranch}`);
5554
+ }
5555
+ return lines.join("\n");
5203
5556
  }
5204
5557
 
5205
5558
  // src/index.ts
5206
5559
  function loadPluginConfig(projectRoot) {
5207
- const configPath = path6.join(projectRoot, ".opencode", "codebase-index.json");
5560
+ const configPath = path7.join(projectRoot, ".opencode", "codebase-index.json");
5208
5561
  try {
5209
- if ((0, import_fs4.existsSync)(configPath)) {
5210
- const content = (0, import_fs4.readFileSync)(configPath, "utf-8");
5562
+ if ((0, import_fs5.existsSync)(configPath)) {
5563
+ const content = (0, import_fs5.readFileSync)(configPath, "utf-8");
5211
5564
  return JSON.parse(content);
5212
5565
  }
5213
5566
  } catch {