opencode-codebase-index 0.4.0 → 0.5.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
@@ -662,7 +662,7 @@ var path8 = __toESM(require("path"), 1);
662
662
  var os3 = __toESM(require("os"), 1);
663
663
  var import_url2 = require("url");
664
664
 
665
- // src/config/schema.ts
665
+ // src/config/constants.ts
666
666
  var DEFAULT_INCLUDE = [
667
667
  "**/*.{ts,tsx,js,jsx,mjs,cjs}",
668
668
  "**/*.{py,pyi}",
@@ -690,6 +690,80 @@ var DEFAULT_EXCLUDE = [
690
690
  "**/.nuxt/**",
691
691
  "**/.opencode/**"
692
692
  ];
693
+ var EMBEDDING_MODELS = {
694
+ "google": {
695
+ // `text-embedding-004` is DEPRECATED - https://ai.google.dev/gemini-api/docs/deprecations
696
+ "text-embedding-005": {
697
+ provider: "google",
698
+ model: "text-embedding-005",
699
+ dimensions: 768,
700
+ maxTokens: 2048,
701
+ costPer1MTokens: 0.025,
702
+ taskAble: false
703
+ // Note: on reality, this model allows for task-specific embeddings. See: https://docs.cloud.google.com/vertex-ai/generative-ai/docs/embeddings/task-types
704
+ },
705
+ "gemini-embedding-001": {
706
+ provider: "google",
707
+ model: "gemini-embedding-001",
708
+ // Native output is 3072D, but we use Matryoshka truncation via outputDimensionality
709
+ // to reduce to 1536D for better storage/search efficiency with minimal quality loss.
710
+ // Google recommends 768, 1536, or 3072. See: https://ai.google.dev/gemini-api/docs/embeddings
711
+ dimensions: 1536,
712
+ maxTokens: 2048,
713
+ costPer1MTokens: 0.15,
714
+ taskAble: true
715
+ }
716
+ },
717
+ "openai": {
718
+ "text-embedding-3-small": {
719
+ provider: "openai",
720
+ model: "text-embedding-3-small",
721
+ dimensions: 1536,
722
+ maxTokens: 8191,
723
+ costPer1MTokens: 0.02
724
+ },
725
+ "text-embedding-3-large": {
726
+ provider: "openai",
727
+ model: "text-embedding-3-large",
728
+ dimensions: 3072,
729
+ maxTokens: 8191,
730
+ costPer1MTokens: 0.13
731
+ }
732
+ },
733
+ "ollama": {
734
+ "nomic-embed-text": {
735
+ provider: "ollama",
736
+ model: "nomic-embed-text",
737
+ dimensions: 768,
738
+ maxTokens: 8192,
739
+ costPer1MTokens: 0
740
+ },
741
+ "mxbai-embed-large": {
742
+ provider: "ollama",
743
+ model: "mxbai-embed-large",
744
+ dimensions: 1024,
745
+ maxTokens: 512,
746
+ costPer1MTokens: 0
747
+ }
748
+ },
749
+ "github-copilot": {
750
+ "text-embedding-3-small": {
751
+ provider: "github-copilot",
752
+ model: "text-embedding-3-small",
753
+ dimensions: 1536,
754
+ maxTokens: 8191,
755
+ costPer1MTokens: 0
756
+ }
757
+ }
758
+ };
759
+ var DEFAULT_PROVIDER_MODELS = {
760
+ "github-copilot": "text-embedding-3-small",
761
+ "openai": "text-embedding-3-small",
762
+ "google": "text-embedding-005",
763
+ "ollama": "nomic-embed-text"
764
+ };
765
+
766
+ // src/config/schema.ts
693
767
  function getDefaultIndexingConfig() {
694
768
  return {
695
769
  autoIndex: false,
@@ -701,7 +775,8 @@ function getDefaultIndexingConfig() {
701
775
  retryDelayMs: 1e3,
702
776
  autoGc: true,
703
777
  gcIntervalDays: 7,
704
- gcOrphanThreshold: 100
778
+ gcOrphanThreshold: 100,
779
+ requireProjectMarker: true
705
780
  };
706
781
  }
707
782
  function getDefaultSearchConfig() {
@@ -725,11 +800,13 @@ function getDefaultDebugConfig() {
725
800
  metrics: true
726
801
  };
727
802
  }
728
- var VALID_PROVIDERS = ["auto", "github-copilot", "openai", "google", "ollama"];
729
803
  var VALID_SCOPES = ["project", "global"];
730
804
  var VALID_LOG_LEVELS = ["error", "warn", "info", "debug"];
731
805
  function isValidProvider(value) {
732
- return typeof value === "string" && VALID_PROVIDERS.includes(value);
806
+ return typeof value === "string" && Object.keys(EMBEDDING_MODELS).includes(value);
807
+ }
808
+ function isValidModel(value, provider) {
809
+ return typeof value === "string" && Object.keys(EMBEDDING_MODELS[provider]).includes(value);
733
810
  }
734
811
  function isValidScope(value) {
735
812
  return typeof value === "string" && VALID_SCOPES.includes(value);
@@ -756,7 +833,8 @@ function parseConfig(raw) {
756
833
  retryDelayMs: typeof rawIndexing.retryDelayMs === "number" ? rawIndexing.retryDelayMs : defaultIndexing.retryDelayMs,
757
834
  autoGc: typeof rawIndexing.autoGc === "boolean" ? rawIndexing.autoGc : defaultIndexing.autoGc,
758
835
  gcIntervalDays: typeof rawIndexing.gcIntervalDays === "number" ? Math.max(1, rawIndexing.gcIntervalDays) : defaultIndexing.gcIntervalDays,
759
- gcOrphanThreshold: typeof rawIndexing.gcOrphanThreshold === "number" ? Math.max(0, rawIndexing.gcOrphanThreshold) : defaultIndexing.gcOrphanThreshold
836
+ gcOrphanThreshold: typeof rawIndexing.gcOrphanThreshold === "number" ? Math.max(0, rawIndexing.gcOrphanThreshold) : defaultIndexing.gcOrphanThreshold,
837
+ requireProjectMarker: typeof rawIndexing.requireProjectMarker === "boolean" ? rawIndexing.requireProjectMarker : defaultIndexing.requireProjectMarker
760
838
  };
761
839
  const rawSearch = input.search && typeof input.search === "object" ? input.search : {};
762
840
  const search = {
@@ -777,9 +855,19 @@ function parseConfig(raw) {
777
855
  logBranch: typeof rawDebug.logBranch === "boolean" ? rawDebug.logBranch : defaultDebug.logBranch,
778
856
  metrics: typeof rawDebug.metrics === "boolean" ? rawDebug.metrics : defaultDebug.metrics
779
857
  };
858
+ let embeddingProvider;
859
+ let embeddingModel = void 0;
860
+ if (isValidProvider(input.embeddingProvider)) {
861
+ embeddingProvider = input.embeddingProvider;
862
+ if (input.embeddingModel) {
863
+ embeddingModel = isValidModel(input.embeddingModel, embeddingProvider) ? input.embeddingModel : DEFAULT_PROVIDER_MODELS[embeddingProvider];
864
+ }
865
+ } else {
866
+ embeddingProvider = "auto";
867
+ }
780
868
  return {
781
- embeddingProvider: isValidProvider(input.embeddingProvider) ? input.embeddingProvider : "auto",
782
- embeddingModel: typeof input.embeddingModel === "string" ? input.embeddingModel : "auto",
869
+ embeddingProvider,
870
+ embeddingModel,
783
871
  scope: isValidScope(input.scope) ? input.scope : "project",
784
872
  include: isStringArray(input.include) ? input.include : DEFAULT_INCLUDE,
785
873
  exclude: isStringArray(input.exclude) ? input.exclude : DEFAULT_EXCLUDE,
@@ -788,64 +876,12 @@ function parseConfig(raw) {
788
876
  debug
789
877
  };
790
878
  }
791
- var EMBEDDING_MODELS = {
792
- "github-copilot/text-embedding-3-small": {
793
- provider: "github-copilot",
794
- model: "text-embedding-3-small",
795
- dimensions: 1536,
796
- maxTokens: 8191,
797
- costPer1MTokens: 0
798
- },
799
- "openai/text-embedding-3-small": {
800
- provider: "openai",
801
- model: "text-embedding-3-small",
802
- dimensions: 1536,
803
- maxTokens: 8191,
804
- costPer1MTokens: 0.02
805
- },
806
- "openai/text-embedding-3-large": {
807
- provider: "openai",
808
- model: "text-embedding-3-large",
809
- dimensions: 3072,
810
- maxTokens: 8191,
811
- costPer1MTokens: 0.13
812
- },
813
- "google/text-embedding-004": {
814
- provider: "google",
815
- model: "text-embedding-004",
816
- dimensions: 768,
817
- maxTokens: 2048,
818
- costPer1MTokens: 0
819
- },
820
- "ollama/nomic-embed-text": {
821
- provider: "ollama",
822
- model: "nomic-embed-text",
823
- dimensions: 768,
824
- maxTokens: 8192,
825
- costPer1MTokens: 0
826
- },
827
- "ollama/mxbai-embed-large": {
828
- provider: "ollama",
829
- model: "mxbai-embed-large",
830
- dimensions: 1024,
831
- maxTokens: 512,
832
- costPer1MTokens: 0
833
- }
834
- };
835
879
  function getDefaultModelForProvider(provider) {
836
- switch (provider) {
837
- case "github-copilot":
838
- return EMBEDDING_MODELS["github-copilot/text-embedding-3-small"];
839
- case "openai":
840
- return EMBEDDING_MODELS["openai/text-embedding-3-small"];
841
- case "google":
842
- return EMBEDDING_MODELS["google/text-embedding-004"];
843
- case "ollama":
844
- return EMBEDDING_MODELS["ollama/nomic-embed-text"];
845
- default:
846
- return EMBEDDING_MODELS["github-copilot/text-embedding-3-small"];
847
- }
880
+ const models = EMBEDDING_MODELS[provider];
881
+ const providerDefault = DEFAULT_PROVIDER_MODELS[provider];
882
+ return models[providerDefault];
848
883
  }
884
+ var availableProviders = Object.keys(EMBEDDING_MODELS);
849
885
 
850
886
  // src/indexer/index.ts
851
887
  var import_fs4 = require("fs");
@@ -874,7 +910,7 @@ function pTimeout(promise, options) {
874
910
  } = options;
875
911
  let timer;
876
912
  let abortHandler;
877
- const wrappedPromise = new Promise((resolve4, reject) => {
913
+ const wrappedPromise = new Promise((resolve5, reject) => {
878
914
  if (typeof milliseconds !== "number" || Math.sign(milliseconds) !== 1) {
879
915
  throw new TypeError(`Expected \`milliseconds\` to be a positive number, got \`${milliseconds}\``);
880
916
  }
@@ -888,7 +924,7 @@ function pTimeout(promise, options) {
888
924
  };
889
925
  signal.addEventListener("abort", abortHandler, { once: true });
890
926
  }
891
- promise.then(resolve4, reject);
927
+ promise.then(resolve5, reject);
892
928
  if (milliseconds === Number.POSITIVE_INFINITY) {
893
929
  return;
894
930
  }
@@ -896,7 +932,7 @@ function pTimeout(promise, options) {
896
932
  timer = customTimers.setTimeout.call(void 0, () => {
897
933
  if (fallback) {
898
934
  try {
899
- resolve4(fallback());
935
+ resolve5(fallback());
900
936
  } catch (error) {
901
937
  reject(error);
902
938
  }
@@ -906,7 +942,7 @@ function pTimeout(promise, options) {
906
942
  promise.cancel();
907
943
  }
908
944
  if (message === false) {
909
- resolve4();
945
+ resolve5();
910
946
  } else if (message instanceof Error) {
911
947
  reject(message);
912
948
  } else {
@@ -1296,7 +1332,7 @@ var PQueue = class extends import_index.default {
1296
1332
  // Assign unique ID if not provided
1297
1333
  id: options.id ?? (this.#idAssigner++).toString()
1298
1334
  };
1299
- return new Promise((resolve4, reject) => {
1335
+ return new Promise((resolve5, reject) => {
1300
1336
  const taskSymbol = /* @__PURE__ */ Symbol(`task-${options.id}`);
1301
1337
  this.#queue.enqueue(async () => {
1302
1338
  this.#pending++;
@@ -1334,7 +1370,7 @@ var PQueue = class extends import_index.default {
1334
1370
  })]);
1335
1371
  }
1336
1372
  const result = await operation;
1337
- resolve4(result);
1373
+ resolve5(result);
1338
1374
  this.emit("completed", result);
1339
1375
  } catch (error) {
1340
1376
  reject(error);
@@ -1491,13 +1527,13 @@ var PQueue = class extends import_index.default {
1491
1527
  });
1492
1528
  }
1493
1529
  async #onEvent(event, filter) {
1494
- return new Promise((resolve4) => {
1530
+ return new Promise((resolve5) => {
1495
1531
  const listener = () => {
1496
1532
  if (filter && !filter()) {
1497
1533
  return;
1498
1534
  }
1499
1535
  this.off(event, listener);
1500
- resolve4();
1536
+ resolve5();
1501
1537
  };
1502
1538
  this.on(event, listener);
1503
1539
  });
@@ -1782,7 +1818,7 @@ async function onAttemptFailure({ error, attemptNumber, retriesConsumed, startTi
1782
1818
  const finalDelay = Math.min(delayTime, remainingTime);
1783
1819
  options.signal?.throwIfAborted();
1784
1820
  if (finalDelay > 0) {
1785
- await new Promise((resolve4, reject) => {
1821
+ await new Promise((resolve5, reject) => {
1786
1822
  const onAbort = () => {
1787
1823
  clearTimeout(timeoutToken);
1788
1824
  options.signal?.removeEventListener("abort", onAbort);
@@ -1790,7 +1826,7 @@ async function onAttemptFailure({ error, attemptNumber, retriesConsumed, startTi
1790
1826
  };
1791
1827
  const timeoutToken = setTimeout(() => {
1792
1828
  options.signal?.removeEventListener("abort", onAbort);
1793
- resolve4();
1829
+ resolve5();
1794
1830
  }, finalDelay);
1795
1831
  if (options.unref) {
1796
1832
  timeoutToken.unref?.();
@@ -1854,12 +1890,6 @@ async function pRetry(input, options = {}) {
1854
1890
  var import_fs = require("fs");
1855
1891
  var path = __toESM(require("path"), 1);
1856
1892
  var os = __toESM(require("os"), 1);
1857
- var EMBEDDING_CAPABLE_PROVIDERS = [
1858
- "github-copilot",
1859
- "openai",
1860
- "google",
1861
- "ollama"
1862
- ];
1863
1893
  function getOpenCodeAuthPath() {
1864
1894
  return path.join(os.homedir(), ".local", "share", "opencode", "auth.json");
1865
1895
  }
@@ -1873,21 +1903,34 @@ function loadOpenCodeAuth() {
1873
1903
  }
1874
1904
  return {};
1875
1905
  }
1876
- async function detectEmbeddingProvider(preferredProvider) {
1877
- if (preferredProvider && preferredProvider !== "auto") {
1878
- const credentials = await getProviderCredentials(preferredProvider);
1879
- if (credentials) {
1906
+ async function detectEmbeddingProvider(preferredProvider, model) {
1907
+ const credentials = await getProviderCredentials(preferredProvider);
1908
+ if (credentials) {
1909
+ if (!model) {
1880
1910
  return {
1881
1911
  provider: preferredProvider,
1882
1912
  credentials,
1883
1913
  modelInfo: getDefaultModelForProvider(preferredProvider)
1884
1914
  };
1885
1915
  }
1886
- throw new Error(
1887
- `Preferred provider '${preferredProvider}' is not configured or authenticated`
1888
- );
1916
+ if (!isValidModel(model, preferredProvider)) {
1917
+ throw new Error(
1918
+ `Model '${model}' is not supported by provider '${preferredProvider}'`
1919
+ );
1920
+ }
1921
+ const providerModels = EMBEDDING_MODELS[preferredProvider];
1922
+ return {
1923
+ provider: preferredProvider,
1924
+ credentials,
1925
+ modelInfo: providerModels[model]
1926
+ };
1889
1927
  }
1890
- for (const provider of EMBEDDING_CAPABLE_PROVIDERS) {
1928
+ throw new Error(
1929
+ `Preferred provider '${preferredProvider}' is not configured or authenticated`
1930
+ );
1931
+ }
1932
+ async function tryDetectProvider() {
1933
+ for (const provider of availableProviders) {
1891
1934
  const credentials = await getProviderCredentials(provider);
1892
1935
  if (credentials) {
1893
1936
  return {
@@ -1898,7 +1941,7 @@ async function detectEmbeddingProvider(preferredProvider) {
1898
1941
  }
1899
1942
  }
1900
1943
  throw new Error(
1901
- `No embedding-capable provider found. Please authenticate with OpenCode using one of: ${EMBEDDING_CAPABLE_PROVIDERS.join(", ")}.`
1944
+ `No embedding-capable provider found. Please authenticate with OpenCode using one of: ${availableProviders.join(", ")}.`
1902
1945
  );
1903
1946
  }
1904
1947
  async function getProviderCredentials(provider) {
@@ -1996,18 +2039,20 @@ function getProviderDisplayName(provider) {
1996
2039
  }
1997
2040
 
1998
2041
  // src/embeddings/provider.ts
1999
- function createEmbeddingProvider(credentials, modelInfo) {
2000
- switch (credentials.provider) {
2042
+ function createEmbeddingProvider(configuredProviderInfo) {
2043
+ switch (configuredProviderInfo.provider) {
2001
2044
  case "github-copilot":
2002
- return new GitHubCopilotEmbeddingProvider(credentials, modelInfo);
2045
+ return new GitHubCopilotEmbeddingProvider(configuredProviderInfo.credentials, configuredProviderInfo.modelInfo);
2003
2046
  case "openai":
2004
- return new OpenAIEmbeddingProvider(credentials, modelInfo);
2047
+ return new OpenAIEmbeddingProvider(configuredProviderInfo.credentials, configuredProviderInfo.modelInfo);
2005
2048
  case "google":
2006
- return new GoogleEmbeddingProvider(credentials, modelInfo);
2049
+ return new GoogleEmbeddingProvider(configuredProviderInfo.credentials, configuredProviderInfo.modelInfo);
2007
2050
  case "ollama":
2008
- return new OllamaEmbeddingProvider(credentials, modelInfo);
2009
- default:
2010
- throw new Error(`Unsupported embedding provider: ${credentials.provider}`);
2051
+ return new OllamaEmbeddingProvider(configuredProviderInfo.credentials, configuredProviderInfo.modelInfo);
2052
+ default: {
2053
+ const _exhaustive = configuredProviderInfo;
2054
+ throw new Error(`Unsupported embedding provider: ${_exhaustive.provider}`);
2055
+ }
2011
2056
  }
2012
2057
  }
2013
2058
  var GitHubCopilotEmbeddingProvider = class {
@@ -2021,8 +2066,15 @@ var GitHubCopilotEmbeddingProvider = class {
2021
2066
  }
2022
2067
  return this.credentials.refreshToken;
2023
2068
  }
2024
- async embed(text) {
2025
- const result = await this.embedBatch([text]);
2069
+ async embedQuery(query) {
2070
+ const result = await this.embedBatch([query]);
2071
+ return {
2072
+ embedding: result.embeddings[0],
2073
+ tokensUsed: result.totalTokensUsed
2074
+ };
2075
+ }
2076
+ async embedDocument(document) {
2077
+ const result = await this.embedBatch([document]);
2026
2078
  return {
2027
2079
  embedding: result.embeddings[0],
2028
2080
  tokensUsed: result.totalTokensUsed
@@ -2062,8 +2114,15 @@ var OpenAIEmbeddingProvider = class {
2062
2114
  this.credentials = credentials;
2063
2115
  this.modelInfo = modelInfo;
2064
2116
  }
2065
- async embed(text) {
2066
- const result = await this.embedBatch([text]);
2117
+ async embedQuery(query) {
2118
+ const result = await this.embedBatch([query]);
2119
+ return {
2120
+ embedding: result.embeddings[0],
2121
+ tokensUsed: result.totalTokensUsed
2122
+ };
2123
+ }
2124
+ async embedDocument(document) {
2125
+ const result = await this.embedBatch([document]);
2067
2126
  return {
2068
2127
  embedding: result.embeddings[0],
2069
2128
  tokensUsed: result.totalTokensUsed
@@ -2095,33 +2154,61 @@ var OpenAIEmbeddingProvider = class {
2095
2154
  return this.modelInfo;
2096
2155
  }
2097
2156
  };
2098
- var GoogleEmbeddingProvider = class {
2157
+ var GoogleEmbeddingProvider = class _GoogleEmbeddingProvider {
2099
2158
  constructor(credentials, modelInfo) {
2100
2159
  this.credentials = credentials;
2101
2160
  this.modelInfo = modelInfo;
2102
2161
  }
2103
- async embed(text) {
2104
- const result = await this.embedBatch([text]);
2162
+ static BATCH_SIZE = 20;
2163
+ async embedQuery(query) {
2164
+ const taskType = this.modelInfo.taskAble ? "CODE_RETRIEVAL_QUERY" : void 0;
2165
+ const result = await this.embedWithTaskType([query], taskType);
2166
+ return {
2167
+ embedding: result.embeddings[0],
2168
+ tokensUsed: result.totalTokensUsed
2169
+ };
2170
+ }
2171
+ async embedDocument(document) {
2172
+ const taskType = this.modelInfo.taskAble ? "RETRIEVAL_DOCUMENT" : void 0;
2173
+ const result = await this.embedWithTaskType([document], taskType);
2105
2174
  return {
2106
2175
  embedding: result.embeddings[0],
2107
2176
  tokensUsed: result.totalTokensUsed
2108
2177
  };
2109
2178
  }
2110
2179
  async embedBatch(texts) {
2111
- const results = await Promise.all(
2112
- texts.map(async (text) => {
2180
+ const taskType = this.modelInfo.taskAble ? "RETRIEVAL_DOCUMENT" : void 0;
2181
+ return this.embedWithTaskType(texts, taskType);
2182
+ }
2183
+ /**
2184
+ * Embeds texts using the Google embedContent API.
2185
+ * Sends multiple texts as parts in batched requests (up to BATCH_SIZE per call).
2186
+ * When taskType is provided (gemini-embedding-001), includes it in the request
2187
+ * for task-specific embedding optimization.
2188
+ */
2189
+ async embedWithTaskType(texts, taskType) {
2190
+ const batches = [];
2191
+ for (let i = 0; i < texts.length; i += _GoogleEmbeddingProvider.BATCH_SIZE) {
2192
+ batches.push(texts.slice(i, i + _GoogleEmbeddingProvider.BATCH_SIZE));
2193
+ }
2194
+ const batchResults = await Promise.all(
2195
+ batches.map(async (batch) => {
2196
+ const requests = batch.map((text) => ({
2197
+ model: `models/${this.modelInfo.model}`,
2198
+ content: {
2199
+ parts: [{ text }]
2200
+ },
2201
+ taskType,
2202
+ outputDimensionality: this.modelInfo.dimensions
2203
+ }));
2113
2204
  const response = await fetch(
2114
- `${this.credentials.baseUrl}/models/${this.modelInfo.model}:embedContent?key=${this.credentials.apiKey}`,
2205
+ `${this.credentials.baseUrl}/models/${this.modelInfo.model}:batchEmbedContents?key=${this.credentials.apiKey}`,
2115
2206
  {
2116
2207
  method: "POST",
2117
2208
  headers: {
2118
2209
  "Content-Type": "application/json"
2119
2210
  },
2120
- body: JSON.stringify({
2121
- content: {
2122
- parts: [{ text }]
2123
- }
2124
- })
2211
+ body: JSON.stringify({ requests })
2125
2212
  }
2126
2213
  );
2127
2214
  if (!response.ok) {
@@ -2130,14 +2217,14 @@ var GoogleEmbeddingProvider = class {
2130
2217
  }
2131
2218
  const data = await response.json();
2132
2219
  return {
2133
- embedding: data.embedding.values,
2134
- tokensUsed: Math.ceil(text.length / 4)
2220
+ embeddings: data.embeddings.map((e) => e.values),
2221
+ tokensUsed: batch.reduce((sum, text) => sum + Math.ceil(text.length / 4), 0)
2135
2222
  };
2136
2223
  })
2137
2224
  );
2138
2225
  return {
2139
- embeddings: results.map((r) => r.embedding),
2140
- totalTokensUsed: results.reduce((sum, r) => sum + r.tokensUsed, 0)
2226
+ embeddings: batchResults.flatMap((r) => r.embeddings),
2227
+ totalTokensUsed: batchResults.reduce((sum, r) => sum + r.tokensUsed, 0)
2141
2228
  };
2142
2229
  }
2143
2230
  getModelInfo() {
@@ -2149,29 +2236,44 @@ var OllamaEmbeddingProvider = class {
2149
2236
  this.credentials = credentials;
2150
2237
  this.modelInfo = modelInfo;
2151
2238
  }
2152
- async embed(text) {
2153
- const response = await fetch(`${this.credentials.baseUrl}/api/embeddings`, {
2154
- method: "POST",
2155
- headers: {
2156
- "Content-Type": "application/json"
2157
- },
2158
- body: JSON.stringify({
2159
- model: this.modelInfo.model,
2160
- prompt: text
2161
- })
2162
- });
2163
- if (!response.ok) {
2164
- const error = await response.text();
2165
- throw new Error(`Ollama embedding API error: ${response.status} - ${error}`);
2166
- }
2167
- const data = await response.json();
2239
+ async embedQuery(query) {
2240
+ const result = await this.embedBatch([query]);
2168
2241
  return {
2169
- embedding: data.embedding,
2170
- tokensUsed: Math.ceil(text.length / 4)
2242
+ embedding: result.embeddings[0],
2243
+ tokensUsed: result.totalTokensUsed
2244
+ };
2245
+ }
2246
+ async embedDocument(document) {
2247
+ const result = await this.embedBatch([document]);
2248
+ return {
2249
+ embedding: result.embeddings[0],
2250
+ tokensUsed: result.totalTokensUsed
2171
2251
  };
2172
2252
  }
2173
2253
  async embedBatch(texts) {
2174
- const results = await Promise.all(texts.map((text) => this.embed(text)));
2254
+ const results = await Promise.all(
2255
+ texts.map(async (text) => {
2256
+ const response = await fetch(`${this.credentials.baseUrl}/api/embeddings`, {
2257
+ method: "POST",
2258
+ headers: {
2259
+ "Content-Type": "application/json"
2260
+ },
2261
+ body: JSON.stringify({
2262
+ model: this.modelInfo.model,
2263
+ prompt: text
2264
+ })
2265
+ });
2266
+ if (!response.ok) {
2267
+ const error = await response.text();
2268
+ throw new Error(`Ollama embedding API error: ${response.status} - ${error}`);
2269
+ }
2270
+ const data = await response.json();
2271
+ return {
2272
+ embedding: data.embedding,
2273
+ tokensUsed: Math.ceil(text.length / 4)
2274
+ };
2275
+ })
2276
+ );
2175
2277
  return {
2176
2278
  embeddings: results.map((r) => r.embedding),
2177
2279
  totalTokensUsed: results.reduce((sum, r) => sum + r.tokensUsed, 0)
@@ -2186,6 +2288,30 @@ var OllamaEmbeddingProvider = class {
2186
2288
  var import_ignore = __toESM(require_ignore(), 1);
2187
2289
  var import_fs2 = require("fs");
2188
2290
  var path2 = __toESM(require("path"), 1);
2291
+ var PROJECT_MARKERS = [
2292
+ ".git",
2293
+ "package.json",
2294
+ "Cargo.toml",
2295
+ "go.mod",
2296
+ "pyproject.toml",
2297
+ "setup.py",
2298
+ "requirements.txt",
2299
+ "Gemfile",
2300
+ "composer.json",
2301
+ "pom.xml",
2302
+ "build.gradle",
2303
+ "CMakeLists.txt",
2304
+ "Makefile",
2305
+ ".opencode"
2306
+ ];
2307
+ function hasProjectMarker(projectRoot) {
2308
+ for (const marker of PROJECT_MARKERS) {
2309
+ if ((0, import_fs2.existsSync)(path2.join(projectRoot, marker))) {
2310
+ return true;
2311
+ }
2312
+ }
2313
+ return false;
2314
+ }
2189
2315
  function createIgnoreFilter(projectRoot) {
2190
2316
  const ig = (0, import_ignore.default)();
2191
2317
  const defaultIgnores = [
@@ -3092,11 +3218,40 @@ var Database = class {
3092
3218
  var import_fs3 = require("fs");
3093
3219
  var path4 = __toESM(require("path"), 1);
3094
3220
  var import_child_process = require("child_process");
3221
+ function resolveGitDir(repoRoot) {
3222
+ const gitPath = path4.join(repoRoot, ".git");
3223
+ if (!(0, import_fs3.existsSync)(gitPath)) {
3224
+ return null;
3225
+ }
3226
+ try {
3227
+ const stat4 = (0, import_fs3.statSync)(gitPath);
3228
+ if (stat4.isDirectory()) {
3229
+ return gitPath;
3230
+ }
3231
+ if (stat4.isFile()) {
3232
+ const content = (0, import_fs3.readFileSync)(gitPath, "utf-8").trim();
3233
+ const match = content.match(/^gitdir:\s*(.+)$/);
3234
+ if (match) {
3235
+ const gitdir = match[1];
3236
+ const resolvedPath = path4.isAbsolute(gitdir) ? gitdir : path4.resolve(repoRoot, gitdir);
3237
+ if ((0, import_fs3.existsSync)(resolvedPath)) {
3238
+ return resolvedPath;
3239
+ }
3240
+ }
3241
+ }
3242
+ } catch {
3243
+ }
3244
+ return null;
3245
+ }
3095
3246
  function isGitRepo(dir) {
3096
- return (0, import_fs3.existsSync)(path4.join(dir, ".git"));
3247
+ return resolveGitDir(dir) !== null;
3097
3248
  }
3098
3249
  function getCurrentBranch(repoRoot) {
3099
- const headPath = path4.join(repoRoot, ".git", "HEAD");
3250
+ const gitDir = resolveGitDir(repoRoot);
3251
+ if (!gitDir) {
3252
+ return null;
3253
+ }
3254
+ const headPath = path4.join(gitDir, "HEAD");
3100
3255
  if (!(0, import_fs3.existsSync)(headPath)) {
3101
3256
  return null;
3102
3257
  }
@@ -3115,20 +3270,23 @@ function getCurrentBranch(repoRoot) {
3115
3270
  }
3116
3271
  }
3117
3272
  function getBaseBranch(repoRoot) {
3273
+ const gitDir = resolveGitDir(repoRoot);
3118
3274
  const candidates = ["main", "master", "develop", "trunk"];
3119
- for (const candidate of candidates) {
3120
- const refPath = path4.join(repoRoot, ".git", "refs", "heads", candidate);
3121
- if ((0, import_fs3.existsSync)(refPath)) {
3122
- return candidate;
3123
- }
3124
- const packedRefsPath = path4.join(repoRoot, ".git", "packed-refs");
3125
- if ((0, import_fs3.existsSync)(packedRefsPath)) {
3126
- try {
3127
- const content = (0, import_fs3.readFileSync)(packedRefsPath, "utf-8");
3128
- if (content.includes(`refs/heads/${candidate}`)) {
3129
- return candidate;
3275
+ if (gitDir) {
3276
+ for (const candidate of candidates) {
3277
+ const refPath = path4.join(gitDir, "refs", "heads", candidate);
3278
+ if ((0, import_fs3.existsSync)(refPath)) {
3279
+ return candidate;
3280
+ }
3281
+ const packedRefsPath = path4.join(gitDir, "packed-refs");
3282
+ if ((0, import_fs3.existsSync)(packedRefsPath)) {
3283
+ try {
3284
+ const content = (0, import_fs3.readFileSync)(packedRefsPath, "utf-8");
3285
+ if (content.includes(`refs/heads/${candidate}`)) {
3286
+ return candidate;
3287
+ }
3288
+ } catch {
3130
3289
  }
3131
- } catch {
3132
3290
  }
3133
3291
  }
3134
3292
  }
@@ -3153,6 +3311,10 @@ function getBranchOrDefault(repoRoot) {
3153
3311
  return getCurrentBranch(repoRoot) ?? "default";
3154
3312
  }
3155
3313
  function getHeadPath(repoRoot) {
3314
+ const gitDir = resolveGitDir(repoRoot);
3315
+ if (gitDir) {
3316
+ return path4.join(gitDir, "HEAD");
3317
+ }
3156
3318
  return path4.join(repoRoot, ".git", "HEAD");
3157
3319
  }
3158
3320
 
@@ -3180,6 +3342,7 @@ function isRateLimitError(error) {
3180
3342
  const message = getErrorMessage(error);
3181
3343
  return message.includes("429") || message.toLowerCase().includes("rate limit") || message.toLowerCase().includes("too many requests");
3182
3344
  }
3345
+ var INDEX_METADATA_VERSION = "1";
3183
3346
  var Indexer = class {
3184
3347
  config;
3185
3348
  projectRoot;
@@ -3188,7 +3351,7 @@ var Indexer = class {
3188
3351
  invertedIndex = null;
3189
3352
  database = null;
3190
3353
  provider = null;
3191
- detectedProvider = null;
3354
+ configuredProviderInfo = null;
3192
3355
  fileHashCache = /* @__PURE__ */ new Map();
3193
3356
  fileHashCachePath = "";
3194
3357
  failedBatchesPath = "";
@@ -3199,12 +3362,15 @@ var Indexer = class {
3199
3362
  maxQueryCacheSize = 100;
3200
3363
  queryCacheTtlMs = 5 * 60 * 1e3;
3201
3364
  querySimilarityThreshold = 0.85;
3365
+ indexCompatibility = null;
3366
+ indexingLockPath = "";
3202
3367
  constructor(projectRoot, config) {
3203
3368
  this.projectRoot = projectRoot;
3204
3369
  this.config = config;
3205
3370
  this.indexPath = this.getIndexPath();
3206
3371
  this.fileHashCachePath = path5.join(this.indexPath, "file-hashes.json");
3207
3372
  this.failedBatchesPath = path5.join(this.indexPath, "failed-batches.json");
3373
+ this.indexingLockPath = path5.join(this.indexPath, "indexing.lock");
3208
3374
  this.logger = initializeLogger(config.debug);
3209
3375
  }
3210
3376
  getIndexPath() {
@@ -3230,7 +3396,36 @@ var Indexer = class {
3230
3396
  for (const [k, v] of this.fileHashCache) {
3231
3397
  obj[k] = v;
3232
3398
  }
3233
- (0, import_fs4.writeFileSync)(this.fileHashCachePath, JSON.stringify(obj));
3399
+ this.atomicWriteSync(this.fileHashCachePath, JSON.stringify(obj));
3400
+ }
3401
+ atomicWriteSync(targetPath, data) {
3402
+ const tempPath = `${targetPath}.tmp`;
3403
+ (0, import_fs4.writeFileSync)(tempPath, data);
3404
+ (0, import_fs4.renameSync)(tempPath, targetPath);
3405
+ }
3406
+ checkForInterruptedIndexing() {
3407
+ return (0, import_fs4.existsSync)(this.indexingLockPath);
3408
+ }
3409
+ acquireIndexingLock() {
3410
+ const lockData = {
3411
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3412
+ pid: process.pid
3413
+ };
3414
+ (0, import_fs4.writeFileSync)(this.indexingLockPath, JSON.stringify(lockData));
3415
+ }
3416
+ releaseIndexingLock() {
3417
+ if ((0, import_fs4.existsSync)(this.indexingLockPath)) {
3418
+ (0, import_fs4.unlinkSync)(this.indexingLockPath);
3419
+ }
3420
+ }
3421
+ async recoverFromInterruptedIndexing() {
3422
+ this.logger.warn("Detected interrupted indexing session, recovering...");
3423
+ if ((0, import_fs4.existsSync)(this.fileHashCachePath)) {
3424
+ (0, import_fs4.unlinkSync)(this.fileHashCachePath);
3425
+ }
3426
+ await this.healthCheck();
3427
+ this.releaseIndexingLock();
3428
+ this.logger.info("Recovery complete, next index will re-process all files");
3234
3429
  }
3235
3430
  loadFailedBatches() {
3236
3431
  try {
@@ -3278,23 +3473,27 @@ var Indexer = class {
3278
3473
  }
3279
3474
  }
3280
3475
  async initialize() {
3281
- this.detectedProvider = await detectEmbeddingProvider(this.config.embeddingProvider);
3282
- if (!this.detectedProvider) {
3476
+ if (this.config.embeddingProvider === "auto") {
3477
+ this.configuredProviderInfo = await tryDetectProvider();
3478
+ } else {
3479
+ this.configuredProviderInfo = await detectEmbeddingProvider(this.config.embeddingProvider, this.config.embeddingModel);
3480
+ }
3481
+ if (!this.configuredProviderInfo) {
3283
3482
  throw new Error(
3284
3483
  "No embedding provider available. Configure GitHub, OpenAI, Google, or Ollama."
3285
3484
  );
3286
3485
  }
3287
3486
  this.logger.info("Initializing indexer", {
3288
- provider: this.detectedProvider.provider,
3289
- model: this.detectedProvider.modelInfo.model,
3487
+ provider: this.configuredProviderInfo.provider,
3488
+ model: this.configuredProviderInfo.modelInfo.model,
3290
3489
  scope: this.config.scope
3291
3490
  });
3292
- this.provider = createEmbeddingProvider(
3293
- this.detectedProvider.credentials,
3294
- this.detectedProvider.modelInfo
3295
- );
3491
+ this.provider = createEmbeddingProvider(this.configuredProviderInfo);
3296
3492
  await import_fs4.promises.mkdir(this.indexPath, { recursive: true });
3297
- const dimensions = this.detectedProvider.modelInfo.dimensions;
3493
+ if (this.checkForInterruptedIndexing()) {
3494
+ await this.recoverFromInterruptedIndexing();
3495
+ }
3496
+ const dimensions = this.configuredProviderInfo.modelInfo.dimensions;
3298
3497
  const storePath = path5.join(this.indexPath, "vectors");
3299
3498
  this.store = new VectorStore(storePath, dimensions);
3300
3499
  const indexFilePath = path5.join(this.indexPath, "vectors.usearch");
@@ -3317,6 +3516,14 @@ var Indexer = class {
3317
3516
  if (dbIsNew && this.store.count() > 0) {
3318
3517
  this.migrateFromLegacyIndex();
3319
3518
  }
3519
+ this.indexCompatibility = this.validateIndexCompatibility(this.configuredProviderInfo);
3520
+ if (!this.indexCompatibility.compatible) {
3521
+ this.logger.warn("Index compatibility issue detected", {
3522
+ reason: this.indexCompatibility.reason,
3523
+ storedMetadata: this.indexCompatibility.storedMetadata,
3524
+ configuredProviderInfo: this.configuredProviderInfo
3525
+ });
3526
+ }
3320
3527
  if (isGitRepo(this.projectRoot)) {
3321
3528
  this.currentBranch = getBranchOrDefault(this.projectRoot);
3322
3529
  this.baseBranch = getBaseBranch(this.projectRoot);
@@ -3387,30 +3594,106 @@ var Indexer = class {
3387
3594
  }
3388
3595
  this.database.addChunksToBranchBatch(this.currentBranch || "default", chunkIds);
3389
3596
  }
3597
+ loadIndexMetadata() {
3598
+ if (!this.database) return null;
3599
+ const version = this.database.getMetadata("index.version");
3600
+ if (!version) return null;
3601
+ return {
3602
+ indexVersion: version,
3603
+ embeddingProvider: this.database.getMetadata("index.embeddingProvider") ?? "",
3604
+ embeddingModel: this.database.getMetadata("index.embeddingModel") ?? "",
3605
+ embeddingDimensions: parseInt(this.database.getMetadata("index.embeddingDimensions") ?? "0", 10),
3606
+ createdAt: this.database.getMetadata("index.createdAt") ?? "",
3607
+ updatedAt: this.database.getMetadata("index.updatedAt") ?? ""
3608
+ };
3609
+ }
3610
+ saveIndexMetadata(provider) {
3611
+ if (!this.database) return;
3612
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3613
+ const existingCreatedAt = this.database.getMetadata("index.createdAt");
3614
+ this.database.setMetadata("index.version", INDEX_METADATA_VERSION);
3615
+ this.database.setMetadata("index.embeddingProvider", provider.provider);
3616
+ this.database.setMetadata("index.embeddingModel", provider.modelInfo.model);
3617
+ this.database.setMetadata("index.embeddingDimensions", provider.modelInfo.dimensions.toString());
3618
+ this.database.setMetadata("index.updatedAt", now);
3619
+ if (!existingCreatedAt) {
3620
+ this.database.setMetadata("index.createdAt", now);
3621
+ }
3622
+ }
3623
+ validateIndexCompatibility(provider) {
3624
+ const storedMetadata = this.loadIndexMetadata();
3625
+ if (!storedMetadata) {
3626
+ return { compatible: true };
3627
+ }
3628
+ const currentProvider = provider.provider;
3629
+ const currentModel = provider.modelInfo.model;
3630
+ const currentDimensions = provider.modelInfo.dimensions;
3631
+ if (storedMetadata.embeddingDimensions !== currentDimensions) {
3632
+ return {
3633
+ compatible: false,
3634
+ code: "DIMENSION_MISMATCH" /* DIMENSION_MISMATCH */,
3635
+ reason: `Dimension mismatch: index has ${storedMetadata.embeddingDimensions}D vectors (${storedMetadata.embeddingProvider}/${storedMetadata.embeddingModel}), but current provider uses ${currentDimensions}D (${currentProvider}/${currentModel}). Run index_codebase with force=true to rebuild.`,
3636
+ storedMetadata
3637
+ };
3638
+ }
3639
+ if (storedMetadata.embeddingModel !== currentModel) {
3640
+ return {
3641
+ compatible: false,
3642
+ code: "MODEL_MISMATCH" /* MODEL_MISMATCH */,
3643
+ reason: `Model mismatch: index was built with "${storedMetadata.embeddingModel}", but current model is "${currentModel}". Embeddings are incompatible. Run index_codebase with force=true to rebuild.`,
3644
+ storedMetadata
3645
+ };
3646
+ }
3647
+ if (storedMetadata.embeddingProvider !== currentProvider) {
3648
+ this.logger.warn("Provider changed", {
3649
+ storedProvider: storedMetadata.embeddingProvider,
3650
+ currentProvider
3651
+ });
3652
+ }
3653
+ return {
3654
+ compatible: true,
3655
+ storedMetadata
3656
+ };
3657
+ }
3658
+ checkCompatibility() {
3659
+ if (!this.indexCompatibility) {
3660
+ if (!this.configuredProviderInfo) {
3661
+ throw new Error("No embedding provider info, you must initialize the indexer first.");
3662
+ }
3663
+ this.indexCompatibility = this.validateIndexCompatibility(this.configuredProviderInfo);
3664
+ }
3665
+ return this.indexCompatibility;
3666
+ }
3390
3667
  async ensureInitialized() {
3391
- if (!this.store || !this.provider || !this.invertedIndex || !this.detectedProvider || !this.database) {
3668
+ if (!this.store || !this.provider || !this.invertedIndex || !this.configuredProviderInfo || !this.database) {
3392
3669
  await this.initialize();
3393
3670
  }
3394
3671
  return {
3395
3672
  store: this.store,
3396
3673
  provider: this.provider,
3397
3674
  invertedIndex: this.invertedIndex,
3398
- detectedProvider: this.detectedProvider,
3675
+ configuredProviderInfo: this.configuredProviderInfo,
3399
3676
  database: this.database
3400
3677
  };
3401
3678
  }
3402
3679
  async estimateCost() {
3403
- const { detectedProvider } = await this.ensureInitialized();
3680
+ const { configuredProviderInfo } = await this.ensureInitialized();
3404
3681
  const { files } = await collectFiles(
3405
3682
  this.projectRoot,
3406
3683
  this.config.include,
3407
3684
  this.config.exclude,
3408
3685
  this.config.indexing.maxFileSize
3409
3686
  );
3410
- return createCostEstimate(files, detectedProvider);
3687
+ return createCostEstimate(files, configuredProviderInfo);
3411
3688
  }
3412
3689
  async index(onProgress) {
3413
- const { store, provider, invertedIndex, database, detectedProvider } = await this.ensureInitialized();
3690
+ const { store, provider, invertedIndex, database, configuredProviderInfo } = await this.ensureInitialized();
3691
+ if (!this.indexCompatibility?.compatible) {
3692
+ throw new Error(
3693
+ `${this.indexCompatibility?.reason} Run index_codebase with force=true to rebuild the index.`
3694
+ );
3695
+ }
3696
+ this.acquireIndexingLock();
3414
3697
  this.logger.recordIndexingStart();
3415
3698
  this.logger.info("Starting indexing", { projectRoot: this.projectRoot });
3416
3699
  const startTime = Date.now();
@@ -3517,7 +3800,7 @@ var Indexer = class {
3517
3800
  const id = generateChunkId(parsed.path, chunk);
3518
3801
  const contentHash = generateChunkHash(chunk);
3519
3802
  currentChunkIds.add(id);
3520
- const chunkData = {
3803
+ chunkDataBatch.push({
3521
3804
  chunkId: id,
3522
3805
  contentHash,
3523
3806
  filePath: parsed.path,
@@ -3526,8 +3809,7 @@ var Indexer = class {
3526
3809
  nodeType: chunk.chunkType,
3527
3810
  name: chunk.name,
3528
3811
  language: chunk.language
3529
- };
3530
- chunkDataBatch.push(chunkData);
3812
+ });
3531
3813
  if (existingChunks.get(id) === contentHash) {
3532
3814
  fileChunkCount++;
3533
3815
  continue;
@@ -3580,6 +3862,7 @@ var Indexer = class {
3580
3862
  chunksProcessed: 0,
3581
3863
  totalChunks: 0
3582
3864
  });
3865
+ this.releaseIndexingLock();
3583
3866
  return stats;
3584
3867
  }
3585
3868
  if (pendingChunks.length === 0) {
@@ -3597,6 +3880,7 @@ var Indexer = class {
3597
3880
  chunksProcessed: 0,
3598
3881
  totalChunks: 0
3599
3882
  });
3883
+ this.releaseIndexingLock();
3600
3884
  return stats;
3601
3885
  }
3602
3886
  onProgress?.({
@@ -3625,7 +3909,7 @@ var Indexer = class {
3625
3909
  stats.indexedChunks++;
3626
3910
  }
3627
3911
  }
3628
- const providerRateLimits = this.getProviderRateLimits(detectedProvider.provider);
3912
+ const providerRateLimits = this.getProviderRateLimits(configuredProviderInfo.provider);
3629
3913
  const queue = new PQueue({
3630
3914
  concurrency: providerRateLimits.concurrency,
3631
3915
  interval: providerRateLimits.intervalMs,
@@ -3636,7 +3920,7 @@ var Indexer = class {
3636
3920
  for (const batch of dynamicBatches) {
3637
3921
  queue.add(async () => {
3638
3922
  if (rateLimitBackoffMs > 0) {
3639
- await new Promise((resolve4) => setTimeout(resolve4, rateLimitBackoffMs));
3923
+ await new Promise((resolve5) => setTimeout(resolve5, rateLimitBackoffMs));
3640
3924
  }
3641
3925
  try {
3642
3926
  const result = await pRetry(
@@ -3680,7 +3964,7 @@ var Indexer = class {
3680
3964
  contentHash: chunk.contentHash,
3681
3965
  embedding: float32ArrayToBuffer(result.embeddings[i]),
3682
3966
  chunkText: chunk.text,
3683
- model: detectedProvider.modelInfo.model
3967
+ model: configuredProviderInfo.modelInfo.model
3684
3968
  }));
3685
3969
  database.upsertEmbeddingsBatch(embeddingBatchItems);
3686
3970
  for (const chunk of batch) {
@@ -3731,6 +4015,8 @@ var Indexer = class {
3731
4015
  await this.maybeRunOrphanGc();
3732
4016
  }
3733
4017
  stats.durationMs = Date.now() - startTime;
4018
+ this.saveIndexMetadata(configuredProviderInfo);
4019
+ this.indexCompatibility = { compatible: true };
3734
4020
  this.logger.recordIndexingEnd();
3735
4021
  this.logger.info("Indexing complete", {
3736
4022
  files: stats.totalFiles,
@@ -3751,6 +4037,7 @@ var Indexer = class {
3751
4037
  chunksProcessed: stats.indexedChunks,
3752
4038
  totalChunks: pendingChunks.length
3753
4039
  });
4040
+ this.releaseIndexingLock();
3754
4041
  return stats;
3755
4042
  }
3756
4043
  async getQueryEmbedding(query, provider) {
@@ -3773,7 +4060,7 @@ var Indexer = class {
3773
4060
  }
3774
4061
  this.logger.cache("debug", "Query embedding cache miss", { query: query.slice(0, 50) });
3775
4062
  this.logger.recordQueryCacheMiss();
3776
- const { embedding, tokensUsed } = await provider.embed(query);
4063
+ const { embedding, tokensUsed } = await provider.embedQuery(query);
3777
4064
  this.logger.recordEmbeddingApiCall(tokensUsed);
3778
4065
  if (this.queryEmbeddingCache.size >= this.maxQueryCacheSize) {
3779
4066
  const oldestKey = this.queryEmbeddingCache.keys().next().value;
@@ -3816,8 +4103,14 @@ var Indexer = class {
3816
4103
  return intersection / union;
3817
4104
  }
3818
4105
  async search(query, limit, options) {
3819
- const searchStartTime = import_perf_hooks.performance.now();
3820
4106
  const { store, provider, database } = await this.ensureInitialized();
4107
+ const compatibility = this.checkCompatibility();
4108
+ if (!compatibility.compatible) {
4109
+ throw new Error(
4110
+ `${compatibility.reason ?? "Index is incompatible with current embedding provider."} A possible solution is to run index_codebase with force=true to rebuild the index.`
4111
+ );
4112
+ }
4113
+ const searchStartTime = import_perf_hooks.performance.now();
3821
4114
  if (store.count() === 0) {
3822
4115
  this.logger.search("debug", "Search on empty index", { query });
3823
4116
  return [];
@@ -3959,15 +4252,16 @@ var Indexer = class {
3959
4252
  return results.slice(0, limit);
3960
4253
  }
3961
4254
  async getStatus() {
3962
- const { store, detectedProvider } = await this.ensureInitialized();
4255
+ const { store, configuredProviderInfo } = await this.ensureInitialized();
3963
4256
  return {
3964
4257
  indexed: store.count() > 0,
3965
4258
  vectorCount: store.count(),
3966
- provider: detectedProvider.provider,
3967
- model: detectedProvider.modelInfo.model,
4259
+ provider: configuredProviderInfo.provider,
4260
+ model: configuredProviderInfo.modelInfo.model,
3968
4261
  indexPath: this.indexPath,
3969
4262
  currentBranch: this.currentBranch,
3970
- baseBranch: this.baseBranch
4263
+ baseBranch: this.baseBranch,
4264
+ compatibility: this.indexCompatibility
3971
4265
  };
3972
4266
  }
3973
4267
  async clearIndex() {
@@ -3979,6 +4273,13 @@ var Indexer = class {
3979
4273
  this.fileHashCache.clear();
3980
4274
  this.saveFileHashCache();
3981
4275
  database.clearBranch(this.currentBranch);
4276
+ database.deleteMetadata("index.version");
4277
+ database.deleteMetadata("index.embeddingProvider");
4278
+ database.deleteMetadata("index.embeddingModel");
4279
+ database.deleteMetadata("index.embeddingDimensions");
4280
+ database.deleteMetadata("index.createdAt");
4281
+ database.deleteMetadata("index.updatedAt");
4282
+ this.indexCompatibility = this.validateIndexCompatibility(this.configuredProviderInfo);
3982
4283
  }
3983
4284
  async healthCheck() {
3984
4285
  const { store, invertedIndex, database } = await this.ensureInitialized();
@@ -4092,26 +4393,31 @@ var Indexer = class {
4092
4393
  getLogger() {
4093
4394
  return this.logger;
4094
4395
  }
4095
- async findSimilar(code, limit, options) {
4096
- const searchStartTime = import_perf_hooks.performance.now();
4396
+ async findSimilar(code, limit = this.config.search.maxResults, options) {
4097
4397
  const { store, provider, database } = await this.ensureInitialized();
4398
+ const compatibility = this.checkCompatibility();
4399
+ if (!compatibility.compatible) {
4400
+ throw new Error(
4401
+ `${compatibility.reason ?? "Index is incompatible with current embedding provider."} Run index_codebase with force=true to rebuild the index.`
4402
+ );
4403
+ }
4404
+ const searchStartTime = import_perf_hooks.performance.now();
4098
4405
  if (store.count() === 0) {
4099
4406
  this.logger.search("debug", "Find similar on empty index");
4100
4407
  return [];
4101
4408
  }
4102
- const maxResults = limit ?? this.config.search.maxResults;
4103
4409
  const filterByBranch = options?.filterByBranch ?? true;
4104
4410
  this.logger.search("debug", "Starting find similar", {
4105
4411
  codeLength: code.length,
4106
- maxResults,
4412
+ limit,
4107
4413
  filterByBranch
4108
4414
  });
4109
4415
  const embeddingStartTime = import_perf_hooks.performance.now();
4110
- const { embedding, tokensUsed } = await provider.embed(code);
4416
+ const { embedding, tokensUsed } = await provider.embedDocument(code);
4111
4417
  const embeddingMs = import_perf_hooks.performance.now() - embeddingStartTime;
4112
4418
  this.logger.recordEmbeddingApiCall(tokensUsed);
4113
4419
  const vectorStartTime = import_perf_hooks.performance.now();
4114
- const semanticResults = store.search(embedding, maxResults * 2);
4420
+ const semanticResults = store.search(embedding, limit * 2);
4115
4421
  const vectorMs = import_perf_hooks.performance.now() - vectorStartTime;
4116
4422
  let branchChunkIds = null;
4117
4423
  if (filterByBranch && this.currentBranch !== "default") {
@@ -4135,7 +4441,7 @@ var Indexer = class {
4135
4441
  if (r.metadata.chunkType !== options.chunkType) return false;
4136
4442
  }
4137
4443
  return true;
4138
- }).slice(0, maxResults);
4444
+ }).slice(0, limit);
4139
4445
  const totalSearchMs = import_perf_hooks.performance.now() - searchStartTime;
4140
4446
  this.logger.recordSearch(totalSearchMs, {
4141
4447
  embeddingMs,
@@ -5050,7 +5356,7 @@ var NodeFsHandler = class {
5050
5356
  this._addToNodeFs(path9, initialAdd, wh, depth + 1);
5051
5357
  }
5052
5358
  }).on(EV.ERROR, this._boundHandleError);
5053
- return new Promise((resolve4, reject) => {
5359
+ return new Promise((resolve5, reject) => {
5054
5360
  if (!stream)
5055
5361
  return reject();
5056
5362
  stream.once(STR_END, () => {
@@ -5059,7 +5365,7 @@ var NodeFsHandler = class {
5059
5365
  return;
5060
5366
  }
5061
5367
  const wasThrottled = throttler ? throttler.clear() : false;
5062
- resolve4(void 0);
5368
+ resolve5(void 0);
5063
5369
  previous.getChildren().filter((item) => {
5064
5370
  return item !== directory && !current.has(item);
5065
5371
  }).forEach((item) => {
@@ -6106,6 +6412,175 @@ function createWatcherWithIndexer(indexer, projectRoot, config) {
6106
6412
 
6107
6413
  // src/tools/index.ts
6108
6414
  var import_plugin = require("@opencode-ai/plugin");
6415
+
6416
+ // src/tools/utils.ts
6417
+ var MAX_CONTENT_LINES = 30;
6418
+ function truncateContent(content) {
6419
+ const lines = content.split("\n");
6420
+ if (lines.length <= MAX_CONTENT_LINES) return content;
6421
+ return lines.slice(0, MAX_CONTENT_LINES).join("\n") + `
6422
+ // ... (${lines.length - MAX_CONTENT_LINES} more lines)`;
6423
+ }
6424
+ function formatIndexStats(stats, verbose = false) {
6425
+ const lines = [];
6426
+ if (stats.indexedChunks === 0 && stats.removedChunks === 0) {
6427
+ lines.push(`Indexed. ${stats.totalFiles} files processed, ${stats.existingChunks} code chunks already up to date.`);
6428
+ } else if (stats.indexedChunks === 0) {
6429
+ lines.push(`Indexed. ${stats.totalFiles} files, removed ${stats.removedChunks} stale chunks, ${stats.existingChunks} chunks remain.`);
6430
+ } else {
6431
+ let main = `Indexed. ${stats.totalFiles} files processed, ${stats.indexedChunks} new chunks embedded.`;
6432
+ if (stats.existingChunks > 0) {
6433
+ main += ` ${stats.existingChunks} unchanged chunks skipped.`;
6434
+ }
6435
+ lines.push(main);
6436
+ if (stats.removedChunks > 0) {
6437
+ lines.push(`Removed ${stats.removedChunks} stale chunks.`);
6438
+ }
6439
+ if (stats.failedChunks > 0) {
6440
+ lines.push(`Failed: ${stats.failedChunks} chunks.`);
6441
+ }
6442
+ lines.push(`Tokens: ${stats.tokensUsed.toLocaleString()}, Duration: ${(stats.durationMs / 1e3).toFixed(1)}s`);
6443
+ }
6444
+ if (verbose) {
6445
+ if (stats.skippedFiles.length > 0) {
6446
+ const tooLarge = stats.skippedFiles.filter((f) => f.reason === "too_large");
6447
+ const excluded = stats.skippedFiles.filter((f) => f.reason === "excluded");
6448
+ const gitignored = stats.skippedFiles.filter((f) => f.reason === "gitignore");
6449
+ lines.push("");
6450
+ lines.push(`Skipped files: ${stats.skippedFiles.length}`);
6451
+ if (tooLarge.length > 0) {
6452
+ lines.push(` Too large (${tooLarge.length}): ${tooLarge.slice(0, 5).map((f) => f.path).join(", ")}${tooLarge.length > 5 ? "..." : ""}`);
6453
+ }
6454
+ if (excluded.length > 0) {
6455
+ lines.push(` Excluded (${excluded.length}): ${excluded.slice(0, 5).map((f) => f.path).join(", ")}${excluded.length > 5 ? "..." : ""}`);
6456
+ }
6457
+ if (gitignored.length > 0) {
6458
+ lines.push(` Gitignored (${gitignored.length}): ${gitignored.slice(0, 5).map((f) => f.path).join(", ")}${gitignored.length > 5 ? "..." : ""}`);
6459
+ }
6460
+ }
6461
+ if (stats.parseFailures.length > 0) {
6462
+ lines.push("");
6463
+ lines.push(`Files with no extractable chunks (${stats.parseFailures.length}): ${stats.parseFailures.slice(0, 10).join(", ")}${stats.parseFailures.length > 10 ? "..." : ""}`);
6464
+ }
6465
+ }
6466
+ return lines.join("\n");
6467
+ }
6468
+ function formatStatus(status) {
6469
+ if (!status.indexed) {
6470
+ return "Codebase is not indexed. Run index_codebase to create an index.";
6471
+ }
6472
+ const lines = [
6473
+ `Index status:`,
6474
+ ` Indexed chunks: ${status.vectorCount.toLocaleString()}`,
6475
+ ` Provider: ${status.provider}`,
6476
+ ` Model: ${status.model}`,
6477
+ ` Location: ${status.indexPath}`
6478
+ ];
6479
+ if (status.currentBranch !== "default") {
6480
+ lines.push(` Current branch: ${status.currentBranch}`);
6481
+ lines.push(` Base branch: ${status.baseBranch}`);
6482
+ }
6483
+ if (status.compatibility && !status.compatibility.compatible) {
6484
+ lines.push("");
6485
+ lines.push(`COMPATIBILITY WARNING: ${status.compatibility.reason}`);
6486
+ if (status.compatibility.storedMetadata) {
6487
+ const stored = status.compatibility.storedMetadata;
6488
+ lines.push(` Index was built with: ${stored.embeddingProvider}/${stored.embeddingModel} (${stored.embeddingDimensions}D)`);
6489
+ lines.push(` Current config: ${status.provider}/${status.model}`);
6490
+ }
6491
+ } else if (!status.compatibility) {
6492
+ lines.push(` Compatibility: No compatibility information found. Maybe the index is not initialized yet, try running index_codebase.`);
6493
+ } else {
6494
+ lines.push(` Compatibility: Index is compatible with the current provider and model.`);
6495
+ }
6496
+ return lines.join("\n");
6497
+ }
6498
+ function formatProgressTitle(progress) {
6499
+ switch (progress.phase) {
6500
+ case "scanning":
6501
+ return "Scanning files...";
6502
+ case "parsing":
6503
+ return `Parsing: ${progress.filesProcessed}/${progress.totalFiles} files`;
6504
+ case "embedding":
6505
+ return `Embedding: ${progress.chunksProcessed}/${progress.totalChunks} chunks`;
6506
+ case "storing":
6507
+ return "Storing index...";
6508
+ case "complete":
6509
+ return "Indexing complete";
6510
+ default:
6511
+ return "Indexing...";
6512
+ }
6513
+ }
6514
+ function calculatePercentage(progress) {
6515
+ if (progress.phase === "scanning") return 0;
6516
+ if (progress.phase === "complete") return 100;
6517
+ if (progress.phase === "parsing") {
6518
+ if (progress.totalFiles === 0) return 5;
6519
+ return Math.round(5 + progress.filesProcessed / progress.totalFiles * 15);
6520
+ }
6521
+ if (progress.phase === "embedding") {
6522
+ if (progress.totalChunks === 0) return 20;
6523
+ return Math.round(20 + progress.chunksProcessed / progress.totalChunks * 70);
6524
+ }
6525
+ if (progress.phase === "storing") return 95;
6526
+ return 0;
6527
+ }
6528
+ function formatCodebasePeek(results, query) {
6529
+ if (results.length === 0) {
6530
+ return "No matching code found. Try a different query or run index_codebase first.";
6531
+ }
6532
+ const formatted = results.map((r, idx) => {
6533
+ const location = `${r.filePath}:${r.startLine}-${r.endLine}`;
6534
+ const name = r.name ? `"${r.name}"` : "(anonymous)";
6535
+ return `[${idx + 1}] ${r.chunkType} ${name} at ${location} (score: ${r.score.toFixed(2)})`;
6536
+ });
6537
+ return `Found ${results.length} locations for "${query}":
6538
+
6539
+ ${formatted.join("\n")}
6540
+
6541
+ Use Read tool to examine specific files.`;
6542
+ }
6543
+ function formatHealthCheck(result) {
6544
+ if (result.removed === 0 && result.gcOrphanEmbeddings === 0 && result.gcOrphanChunks === 0) {
6545
+ return "Index is healthy. No stale entries found.";
6546
+ }
6547
+ const lines = [`Health check complete:`];
6548
+ if (result.removed > 0) {
6549
+ lines.push(` Removed stale entries: ${result.removed}`);
6550
+ }
6551
+ if (result.gcOrphanEmbeddings > 0) {
6552
+ lines.push(` Garbage collected orphan embeddings: ${result.gcOrphanEmbeddings}`);
6553
+ }
6554
+ if (result.gcOrphanChunks > 0) {
6555
+ lines.push(` Garbage collected orphan chunks: ${result.gcOrphanChunks}`);
6556
+ }
6557
+ if (result.filePaths.length > 0) {
6558
+ lines.push(` Cleaned paths: ${result.filePaths.join(", ")}`);
6559
+ }
6560
+ return lines.join("\n");
6561
+ }
6562
+ function formatLogs(logs) {
6563
+ if (logs.length === 0) {
6564
+ return "No logs recorded yet. Logs are captured during indexing and search operations.";
6565
+ }
6566
+ return logs.map((l) => {
6567
+ const dataStr = l.data ? ` ${JSON.stringify(l.data)}` : "";
6568
+ return `[${l.timestamp}] [${l.level.toUpperCase()}] [${l.category}] ${l.message}${dataStr}`;
6569
+ }).join("\n");
6570
+ }
6571
+ function formatSearchResults(results, scoreFormat = "similarity") {
6572
+ const formatted = results.map((r, idx) => {
6573
+ const header = r.name ? `[${idx + 1}] ${r.chunkType} "${r.name}" in ${r.filePath}:${r.startLine}-${r.endLine}` : `[${idx + 1}] ${r.chunkType} in ${r.filePath}:${r.startLine}-${r.endLine}`;
6574
+ const scoreLabel = scoreFormat === "similarity" ? `(similarity: ${(r.score * 100).toFixed(1)}%)` : `(score: ${r.score.toFixed(2)})`;
6575
+ return `${header} ${scoreLabel}
6576
+ \`\`\`
6577
+ ${truncateContent(r.content)}
6578
+ \`\`\``;
6579
+ });
6580
+ return formatted.join("\n\n");
6581
+ }
6582
+
6583
+ // src/tools/index.ts
6109
6584
  var z = import_plugin.tool.schema;
6110
6585
  var sharedIndexer = null;
6111
6586
  function initializeTools(projectRoot, config) {
@@ -6117,39 +6592,6 @@ function getIndexer() {
6117
6592
  }
6118
6593
  return sharedIndexer;
6119
6594
  }
6120
- var codebase_search = (0, import_plugin.tool)({
6121
- description: "Search codebase by MEANING, not keywords. Returns full code content. Use when you need to see actual implementation. For just finding WHERE code is (saves ~90% tokens), use codebase_peek instead. For known identifiers like 'validateToken', use grep - it's faster.",
6122
- args: {
6123
- query: z.string().describe("Natural language description of what code you're looking for. Describe behavior, not syntax."),
6124
- limit: z.number().optional().default(10).describe("Maximum number of results to return"),
6125
- fileType: z.string().optional().describe("Filter by file extension (e.g., 'ts', 'py', 'rs')"),
6126
- directory: z.string().optional().describe("Filter by directory path (e.g., 'src/utils', 'lib')"),
6127
- chunkType: z.enum(["function", "class", "method", "interface", "type", "enum", "struct", "impl", "trait", "module", "other"]).optional().describe("Filter by code chunk type"),
6128
- contextLines: z.number().optional().describe("Number of extra lines to include before/after each match (default: 0)")
6129
- },
6130
- async execute(args) {
6131
- const indexer = getIndexer();
6132
- const results = await indexer.search(args.query, args.limit ?? 10, {
6133
- fileType: args.fileType,
6134
- directory: args.directory,
6135
- chunkType: args.chunkType,
6136
- contextLines: args.contextLines
6137
- });
6138
- if (results.length === 0) {
6139
- return "No matching code found. Try a different query or run index_codebase first.";
6140
- }
6141
- const formatted = results.map((r, idx) => {
6142
- const header = r.name ? `[${idx + 1}] ${r.chunkType} "${r.name}" in ${r.filePath}:${r.startLine}-${r.endLine}` : `[${idx + 1}] ${r.chunkType} in ${r.filePath}:${r.startLine}-${r.endLine}`;
6143
- return `${header} (score: ${r.score.toFixed(2)})
6144
- \`\`\`
6145
- ${r.content}
6146
- \`\`\``;
6147
- });
6148
- return `Found ${results.length} results for "${args.query}":
6149
-
6150
- ${formatted.join("\n\n")}`;
6151
- }
6152
- });
6153
6595
  var codebase_peek = (0, import_plugin.tool)({
6154
6596
  description: "Quick lookup of code locations by meaning. Returns only metadata (file, line, name, type) WITHOUT code content. Use this first to find WHERE code is, then use Read tool to examine specific files. Saves tokens by not returning full code blocks. Best for: discovery, navigation, finding multiple related locations.",
6155
6597
  args: {
@@ -6167,19 +6609,7 @@ var codebase_peek = (0, import_plugin.tool)({
6167
6609
  chunkType: args.chunkType,
6168
6610
  metadataOnly: true
6169
6611
  });
6170
- if (results.length === 0) {
6171
- return "No matching code found. Try a different query or run index_codebase first.";
6172
- }
6173
- const formatted = results.map((r, idx) => {
6174
- const location = `${r.filePath}:${r.startLine}-${r.endLine}`;
6175
- const name = r.name ? `"${r.name}"` : "(anonymous)";
6176
- return `[${idx + 1}] ${r.chunkType} ${name} at ${location} (score: ${r.score.toFixed(2)})`;
6177
- });
6178
- return `Found ${results.length} locations for "${args.query}":
6179
-
6180
- ${formatted.join("\n")}
6181
-
6182
- Use Read tool to examine specific files.`;
6612
+ return formatCodebasePeek(results, args.query);
6183
6613
  }
6184
6614
  });
6185
6615
  var index_codebase = (0, import_plugin.tool)({
@@ -6229,23 +6659,7 @@ var index_health_check = (0, import_plugin.tool)({
6229
6659
  async execute() {
6230
6660
  const indexer = getIndexer();
6231
6661
  const result = await indexer.healthCheck();
6232
- if (result.removed === 0 && result.gcOrphanEmbeddings === 0 && result.gcOrphanChunks === 0) {
6233
- return "Index is healthy. No stale entries found.";
6234
- }
6235
- const lines = [`Health check complete:`];
6236
- if (result.removed > 0) {
6237
- lines.push(` Removed stale entries: ${result.removed}`);
6238
- }
6239
- if (result.gcOrphanEmbeddings > 0) {
6240
- lines.push(` Garbage collected orphan embeddings: ${result.gcOrphanEmbeddings}`);
6241
- }
6242
- if (result.gcOrphanChunks > 0) {
6243
- lines.push(` Garbage collected orphan chunks: ${result.gcOrphanChunks}`);
6244
- }
6245
- if (result.filePaths.length > 0) {
6246
- lines.push(` Cleaned paths: ${result.filePaths.join(", ")}`);
6247
- }
6248
- return lines.join("\n");
6662
+ return formatHealthCheck(result);
6249
6663
  }
6250
6664
  });
6251
6665
  var index_metrics = (0, import_plugin.tool)({
@@ -6284,13 +6698,7 @@ var index_logs = (0, import_plugin.tool)({
6284
6698
  } else {
6285
6699
  logs = logger.getLogs(args.limit);
6286
6700
  }
6287
- if (logs.length === 0) {
6288
- return "No logs recorded yet. Logs are captured during indexing and search operations.";
6289
- }
6290
- return logs.map((l) => {
6291
- const dataStr = l.data ? ` ${JSON.stringify(l.data)}` : "";
6292
- return `[${l.timestamp}] [${l.level.toUpperCase()}] [${l.category}] ${l.message}${dataStr}`;
6293
- }).join("\n");
6701
+ return formatLogs(logs);
6294
6702
  }
6295
6703
  });
6296
6704
  var find_similar = (0, import_plugin.tool)({
@@ -6305,7 +6713,7 @@ var find_similar = (0, import_plugin.tool)({
6305
6713
  },
6306
6714
  async execute(args) {
6307
6715
  const indexer = getIndexer();
6308
- const results = await indexer.findSimilar(args.code, args.limit ?? 10, {
6716
+ const results = await indexer.findSimilar(args.code, args.limit, {
6309
6717
  fileType: args.fileType,
6310
6718
  directory: args.directory,
6311
6719
  chunkType: args.chunkType,
@@ -6314,109 +6722,37 @@ var find_similar = (0, import_plugin.tool)({
6314
6722
  if (results.length === 0) {
6315
6723
  return "No similar code found. Try a different snippet or run index_codebase first.";
6316
6724
  }
6317
- const formatted = results.map((r, idx) => {
6318
- const header = r.name ? `[${idx + 1}] ${r.chunkType} "${r.name}" in ${r.filePath}:${r.startLine}-${r.endLine}` : `[${idx + 1}] ${r.chunkType} in ${r.filePath}:${r.startLine}-${r.endLine}`;
6319
- return `${header} (similarity: ${(r.score * 100).toFixed(1)}%)
6320
- \`\`\`
6321
- ${r.content}
6322
- \`\`\``;
6323
- });
6324
6725
  return `Found ${results.length} similar code blocks:
6325
6726
 
6326
- ${formatted.join("\n\n")}`;
6727
+ ${formatSearchResults(results)}`;
6327
6728
  }
6328
6729
  });
6329
- function formatIndexStats(stats, verbose = false) {
6330
- const lines = [];
6331
- if (stats.indexedChunks === 0 && stats.removedChunks === 0) {
6332
- lines.push(`Indexed. ${stats.totalFiles} files processed, ${stats.existingChunks} code chunks already up to date.`);
6333
- } else if (stats.indexedChunks === 0) {
6334
- lines.push(`Indexed. ${stats.totalFiles} files, removed ${stats.removedChunks} stale chunks, ${stats.existingChunks} chunks remain.`);
6335
- } else {
6336
- let main = `Indexed. ${stats.totalFiles} files processed, ${stats.indexedChunks} new chunks embedded.`;
6337
- if (stats.existingChunks > 0) {
6338
- main += ` ${stats.existingChunks} unchanged chunks skipped.`;
6339
- }
6340
- lines.push(main);
6341
- if (stats.removedChunks > 0) {
6342
- lines.push(`Removed ${stats.removedChunks} stale chunks.`);
6343
- }
6344
- if (stats.failedChunks > 0) {
6345
- lines.push(`Failed: ${stats.failedChunks} chunks.`);
6346
- }
6347
- lines.push(`Tokens: ${stats.tokensUsed.toLocaleString()}, Duration: ${(stats.durationMs / 1e3).toFixed(1)}s`);
6348
- }
6349
- if (verbose) {
6350
- if (stats.skippedFiles.length > 0) {
6351
- const tooLarge = stats.skippedFiles.filter((f) => f.reason === "too_large");
6352
- const excluded = stats.skippedFiles.filter((f) => f.reason === "excluded");
6353
- const gitignored = stats.skippedFiles.filter((f) => f.reason === "gitignore");
6354
- lines.push("");
6355
- lines.push(`Skipped files: ${stats.skippedFiles.length}`);
6356
- if (tooLarge.length > 0) {
6357
- lines.push(` Too large (${tooLarge.length}): ${tooLarge.slice(0, 5).map((f) => f.path).join(", ")}${tooLarge.length > 5 ? "..." : ""}`);
6358
- }
6359
- if (excluded.length > 0) {
6360
- lines.push(` Excluded (${excluded.length}): ${excluded.slice(0, 5).map((f) => f.path).join(", ")}${excluded.length > 5 ? "..." : ""}`);
6361
- }
6362
- if (gitignored.length > 0) {
6363
- lines.push(` Gitignored (${gitignored.length}): ${gitignored.slice(0, 5).map((f) => f.path).join(", ")}${gitignored.length > 5 ? "..." : ""}`);
6364
- }
6365
- }
6366
- if (stats.parseFailures.length > 0) {
6367
- lines.push("");
6368
- lines.push(`Files with no extractable chunks (${stats.parseFailures.length}): ${stats.parseFailures.slice(0, 10).join(", ")}${stats.parseFailures.length > 10 ? "..." : ""}`);
6730
+ var codebase_search = (0, import_plugin.tool)({
6731
+ description: "Search codebase by MEANING, not keywords. Returns full code content. Use when you need to see actual implementation. For just finding WHERE code is (saves ~90% tokens), use codebase_peek instead. For known identifiers like 'validateToken', use grep - it's faster.",
6732
+ args: {
6733
+ query: z.string().describe("Natural language description of what code you're looking for. Describe behavior, not syntax."),
6734
+ limit: z.number().optional().default(5).describe("Maximum number of results to return"),
6735
+ fileType: z.string().optional().describe("Filter by file extension (e.g., 'ts', 'py', 'rs')"),
6736
+ directory: z.string().optional().describe("Filter by directory path (e.g., 'src/utils', 'lib')"),
6737
+ chunkType: z.enum(["function", "class", "method", "interface", "type", "enum", "struct", "impl", "trait", "module", "other"]).optional().describe("Filter by code chunk type"),
6738
+ contextLines: z.number().optional().describe("Number of extra lines to include before/after each match (default: 0)")
6739
+ },
6740
+ async execute(args) {
6741
+ const indexer = getIndexer();
6742
+ const results = await indexer.search(args.query, args.limit ?? 5, {
6743
+ fileType: args.fileType,
6744
+ directory: args.directory,
6745
+ chunkType: args.chunkType,
6746
+ contextLines: args.contextLines
6747
+ });
6748
+ if (results.length === 0) {
6749
+ return "No matching code found. Try a different query or run index_codebase first.";
6369
6750
  }
6751
+ return `Found ${results.length} results for "${args.query}":
6752
+
6753
+ ${formatSearchResults(results, "score")}`;
6370
6754
  }
6371
- return lines.join("\n");
6372
- }
6373
- function formatStatus(status) {
6374
- if (!status.indexed) {
6375
- return "Codebase is not indexed. Run index_codebase to create an index.";
6376
- }
6377
- const lines = [
6378
- `Index status:`,
6379
- ` Indexed chunks: ${status.vectorCount.toLocaleString()}`,
6380
- ` Provider: ${status.provider}`,
6381
- ` Model: ${status.model}`,
6382
- ` Location: ${status.indexPath}`
6383
- ];
6384
- if (status.currentBranch !== "default") {
6385
- lines.push(` Current branch: ${status.currentBranch}`);
6386
- lines.push(` Base branch: ${status.baseBranch}`);
6387
- }
6388
- return lines.join("\n");
6389
- }
6390
- function formatProgressTitle(progress) {
6391
- switch (progress.phase) {
6392
- case "scanning":
6393
- return "Scanning files...";
6394
- case "parsing":
6395
- return `Parsing: ${progress.filesProcessed}/${progress.totalFiles} files`;
6396
- case "embedding":
6397
- return `Embedding: ${progress.chunksProcessed}/${progress.totalChunks} chunks`;
6398
- case "storing":
6399
- return "Storing index...";
6400
- case "complete":
6401
- return "Indexing complete";
6402
- default:
6403
- return "Indexing...";
6404
- }
6405
- }
6406
- function calculatePercentage(progress) {
6407
- if (progress.phase === "scanning") return 0;
6408
- if (progress.phase === "complete") return 100;
6409
- if (progress.phase === "parsing") {
6410
- if (progress.totalFiles === 0) return 5;
6411
- return Math.round(5 + progress.filesProcessed / progress.totalFiles * 15);
6412
- }
6413
- if (progress.phase === "embedding") {
6414
- if (progress.totalChunks === 0) return 20;
6415
- return Math.round(20 + progress.chunksProcessed / progress.totalChunks * 70);
6416
- }
6417
- if (progress.phase === "storing") return 95;
6418
- return 0;
6419
- }
6755
+ });
6420
6756
 
6421
6757
  // src/commands/loader.ts
6422
6758
  var import_fs5 = require("fs");
@@ -6496,14 +6832,20 @@ var plugin = async ({ directory }) => {
6496
6832
  const config = parseConfig(rawConfig);
6497
6833
  initializeTools(projectRoot, config);
6498
6834
  const indexer = new Indexer(projectRoot, config);
6499
- if (config.indexing.autoIndex) {
6835
+ const isValidProject = !config.indexing.requireProjectMarker || hasProjectMarker(projectRoot);
6836
+ if (!isValidProject) {
6837
+ console.warn(
6838
+ `[codebase-index] Skipping file watching and auto-indexing: no project marker found in "${projectRoot}". Set "indexing.requireProjectMarker": false in config to override.`
6839
+ );
6840
+ }
6841
+ if (config.indexing.autoIndex && isValidProject) {
6500
6842
  indexer.initialize().then(() => {
6501
6843
  indexer.index().catch(() => {
6502
6844
  });
6503
6845
  }).catch(() => {
6504
6846
  });
6505
6847
  }
6506
- if (config.indexing.watchFiles) {
6848
+ if (config.indexing.watchFiles && isValidProject) {
6507
6849
  createWatcherWithIndexer(indexer, projectRoot, config);
6508
6850
  }
6509
6851
  return {