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.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // opencode-codebase-index - Semantic codebase search for OpenCode
2
+ import { createRequire } from 'module'; const require = createRequire(import.meta.url);
2
3
  var __create = Object.create;
3
4
  var __defProp = Object.defineProperty;
4
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -657,7 +658,7 @@ import * as path8 from "path";
657
658
  import * as os3 from "os";
658
659
  import { fileURLToPath as fileURLToPath2 } from "url";
659
660
 
660
- // src/config/schema.ts
661
+ // src/config/constants.ts
661
662
  var DEFAULT_INCLUDE = [
662
663
  "**/*.{ts,tsx,js,jsx,mjs,cjs}",
663
664
  "**/*.{py,pyi}",
@@ -685,6 +686,80 @@ var DEFAULT_EXCLUDE = [
685
686
  "**/.nuxt/**",
686
687
  "**/.opencode/**"
687
688
  ];
689
+ var EMBEDDING_MODELS = {
690
+ "google": {
691
+ // `text-embedding-004` is DEPRECATED - https://ai.google.dev/gemini-api/docs/deprecations
692
+ "text-embedding-005": {
693
+ provider: "google",
694
+ model: "text-embedding-005",
695
+ dimensions: 768,
696
+ maxTokens: 2048,
697
+ costPer1MTokens: 0.025,
698
+ taskAble: false
699
+ // Note: on reality, this model allows for task-specific embeddings. See: https://docs.cloud.google.com/vertex-ai/generative-ai/docs/embeddings/task-types
700
+ },
701
+ "gemini-embedding-001": {
702
+ provider: "google",
703
+ model: "gemini-embedding-001",
704
+ // Native output is 3072D, but we use Matryoshka truncation via outputDimensionality
705
+ // to reduce to 1536D for better storage/search efficiency with minimal quality loss.
706
+ // Google recommends 768, 1536, or 3072. See: https://ai.google.dev/gemini-api/docs/embeddings
707
+ dimensions: 1536,
708
+ maxTokens: 2048,
709
+ costPer1MTokens: 0.15,
710
+ taskAble: true
711
+ }
712
+ },
713
+ "openai": {
714
+ "text-embedding-3-small": {
715
+ provider: "openai",
716
+ model: "text-embedding-3-small",
717
+ dimensions: 1536,
718
+ maxTokens: 8191,
719
+ costPer1MTokens: 0.02
720
+ },
721
+ "text-embedding-3-large": {
722
+ provider: "openai",
723
+ model: "text-embedding-3-large",
724
+ dimensions: 3072,
725
+ maxTokens: 8191,
726
+ costPer1MTokens: 0.13
727
+ }
728
+ },
729
+ "ollama": {
730
+ "nomic-embed-text": {
731
+ provider: "ollama",
732
+ model: "nomic-embed-text",
733
+ dimensions: 768,
734
+ maxTokens: 8192,
735
+ costPer1MTokens: 0
736
+ },
737
+ "mxbai-embed-large": {
738
+ provider: "ollama",
739
+ model: "mxbai-embed-large",
740
+ dimensions: 1024,
741
+ maxTokens: 512,
742
+ costPer1MTokens: 0
743
+ }
744
+ },
745
+ "github-copilot": {
746
+ "text-embedding-3-small": {
747
+ provider: "github-copilot",
748
+ model: "text-embedding-3-small",
749
+ dimensions: 1536,
750
+ maxTokens: 8191,
751
+ costPer1MTokens: 0
752
+ }
753
+ }
754
+ };
755
+ var DEFAULT_PROVIDER_MODELS = {
756
+ "github-copilot": "text-embedding-3-small",
757
+ "openai": "text-embedding-3-small",
758
+ "google": "text-embedding-005",
759
+ "ollama": "nomic-embed-text"
760
+ };
761
+
762
+ // src/config/schema.ts
688
763
  function getDefaultIndexingConfig() {
689
764
  return {
690
765
  autoIndex: false,
@@ -696,7 +771,8 @@ function getDefaultIndexingConfig() {
696
771
  retryDelayMs: 1e3,
697
772
  autoGc: true,
698
773
  gcIntervalDays: 7,
699
- gcOrphanThreshold: 100
774
+ gcOrphanThreshold: 100,
775
+ requireProjectMarker: true
700
776
  };
701
777
  }
702
778
  function getDefaultSearchConfig() {
@@ -720,11 +796,13 @@ function getDefaultDebugConfig() {
720
796
  metrics: true
721
797
  };
722
798
  }
723
- var VALID_PROVIDERS = ["auto", "github-copilot", "openai", "google", "ollama"];
724
799
  var VALID_SCOPES = ["project", "global"];
725
800
  var VALID_LOG_LEVELS = ["error", "warn", "info", "debug"];
726
801
  function isValidProvider(value) {
727
- return typeof value === "string" && VALID_PROVIDERS.includes(value);
802
+ return typeof value === "string" && Object.keys(EMBEDDING_MODELS).includes(value);
803
+ }
804
+ function isValidModel(value, provider) {
805
+ return typeof value === "string" && Object.keys(EMBEDDING_MODELS[provider]).includes(value);
728
806
  }
729
807
  function isValidScope(value) {
730
808
  return typeof value === "string" && VALID_SCOPES.includes(value);
@@ -751,7 +829,8 @@ function parseConfig(raw) {
751
829
  retryDelayMs: typeof rawIndexing.retryDelayMs === "number" ? rawIndexing.retryDelayMs : defaultIndexing.retryDelayMs,
752
830
  autoGc: typeof rawIndexing.autoGc === "boolean" ? rawIndexing.autoGc : defaultIndexing.autoGc,
753
831
  gcIntervalDays: typeof rawIndexing.gcIntervalDays === "number" ? Math.max(1, rawIndexing.gcIntervalDays) : defaultIndexing.gcIntervalDays,
754
- gcOrphanThreshold: typeof rawIndexing.gcOrphanThreshold === "number" ? Math.max(0, rawIndexing.gcOrphanThreshold) : defaultIndexing.gcOrphanThreshold
832
+ gcOrphanThreshold: typeof rawIndexing.gcOrphanThreshold === "number" ? Math.max(0, rawIndexing.gcOrphanThreshold) : defaultIndexing.gcOrphanThreshold,
833
+ requireProjectMarker: typeof rawIndexing.requireProjectMarker === "boolean" ? rawIndexing.requireProjectMarker : defaultIndexing.requireProjectMarker
755
834
  };
756
835
  const rawSearch = input.search && typeof input.search === "object" ? input.search : {};
757
836
  const search = {
@@ -772,9 +851,19 @@ function parseConfig(raw) {
772
851
  logBranch: typeof rawDebug.logBranch === "boolean" ? rawDebug.logBranch : defaultDebug.logBranch,
773
852
  metrics: typeof rawDebug.metrics === "boolean" ? rawDebug.metrics : defaultDebug.metrics
774
853
  };
854
+ let embeddingProvider;
855
+ let embeddingModel = void 0;
856
+ if (isValidProvider(input.embeddingProvider)) {
857
+ embeddingProvider = input.embeddingProvider;
858
+ if (input.embeddingModel) {
859
+ embeddingModel = isValidModel(input.embeddingModel, embeddingProvider) ? input.embeddingModel : DEFAULT_PROVIDER_MODELS[embeddingProvider];
860
+ }
861
+ } else {
862
+ embeddingProvider = "auto";
863
+ }
775
864
  return {
776
- embeddingProvider: isValidProvider(input.embeddingProvider) ? input.embeddingProvider : "auto",
777
- embeddingModel: typeof input.embeddingModel === "string" ? input.embeddingModel : "auto",
865
+ embeddingProvider,
866
+ embeddingModel,
778
867
  scope: isValidScope(input.scope) ? input.scope : "project",
779
868
  include: isStringArray(input.include) ? input.include : DEFAULT_INCLUDE,
780
869
  exclude: isStringArray(input.exclude) ? input.exclude : DEFAULT_EXCLUDE,
@@ -783,67 +872,15 @@ function parseConfig(raw) {
783
872
  debug
784
873
  };
785
874
  }
786
- var EMBEDDING_MODELS = {
787
- "github-copilot/text-embedding-3-small": {
788
- provider: "github-copilot",
789
- model: "text-embedding-3-small",
790
- dimensions: 1536,
791
- maxTokens: 8191,
792
- costPer1MTokens: 0
793
- },
794
- "openai/text-embedding-3-small": {
795
- provider: "openai",
796
- model: "text-embedding-3-small",
797
- dimensions: 1536,
798
- maxTokens: 8191,
799
- costPer1MTokens: 0.02
800
- },
801
- "openai/text-embedding-3-large": {
802
- provider: "openai",
803
- model: "text-embedding-3-large",
804
- dimensions: 3072,
805
- maxTokens: 8191,
806
- costPer1MTokens: 0.13
807
- },
808
- "google/text-embedding-004": {
809
- provider: "google",
810
- model: "text-embedding-004",
811
- dimensions: 768,
812
- maxTokens: 2048,
813
- costPer1MTokens: 0
814
- },
815
- "ollama/nomic-embed-text": {
816
- provider: "ollama",
817
- model: "nomic-embed-text",
818
- dimensions: 768,
819
- maxTokens: 8192,
820
- costPer1MTokens: 0
821
- },
822
- "ollama/mxbai-embed-large": {
823
- provider: "ollama",
824
- model: "mxbai-embed-large",
825
- dimensions: 1024,
826
- maxTokens: 512,
827
- costPer1MTokens: 0
828
- }
829
- };
830
875
  function getDefaultModelForProvider(provider) {
831
- switch (provider) {
832
- case "github-copilot":
833
- return EMBEDDING_MODELS["github-copilot/text-embedding-3-small"];
834
- case "openai":
835
- return EMBEDDING_MODELS["openai/text-embedding-3-small"];
836
- case "google":
837
- return EMBEDDING_MODELS["google/text-embedding-004"];
838
- case "ollama":
839
- return EMBEDDING_MODELS["ollama/nomic-embed-text"];
840
- default:
841
- return EMBEDDING_MODELS["github-copilot/text-embedding-3-small"];
842
- }
876
+ const models = EMBEDDING_MODELS[provider];
877
+ const providerDefault = DEFAULT_PROVIDER_MODELS[provider];
878
+ return models[providerDefault];
843
879
  }
880
+ var availableProviders = Object.keys(EMBEDDING_MODELS);
844
881
 
845
882
  // src/indexer/index.ts
846
- import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync, promises as fsPromises2 } from "fs";
883
+ import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync, renameSync, unlinkSync, promises as fsPromises2 } from "fs";
847
884
  import * as path5 from "path";
848
885
  import { performance as performance2 } from "perf_hooks";
849
886
 
@@ -869,7 +906,7 @@ function pTimeout(promise, options) {
869
906
  } = options;
870
907
  let timer;
871
908
  let abortHandler;
872
- const wrappedPromise = new Promise((resolve4, reject) => {
909
+ const wrappedPromise = new Promise((resolve5, reject) => {
873
910
  if (typeof milliseconds !== "number" || Math.sign(milliseconds) !== 1) {
874
911
  throw new TypeError(`Expected \`milliseconds\` to be a positive number, got \`${milliseconds}\``);
875
912
  }
@@ -883,7 +920,7 @@ function pTimeout(promise, options) {
883
920
  };
884
921
  signal.addEventListener("abort", abortHandler, { once: true });
885
922
  }
886
- promise.then(resolve4, reject);
923
+ promise.then(resolve5, reject);
887
924
  if (milliseconds === Number.POSITIVE_INFINITY) {
888
925
  return;
889
926
  }
@@ -891,7 +928,7 @@ function pTimeout(promise, options) {
891
928
  timer = customTimers.setTimeout.call(void 0, () => {
892
929
  if (fallback) {
893
930
  try {
894
- resolve4(fallback());
931
+ resolve5(fallback());
895
932
  } catch (error) {
896
933
  reject(error);
897
934
  }
@@ -901,7 +938,7 @@ function pTimeout(promise, options) {
901
938
  promise.cancel();
902
939
  }
903
940
  if (message === false) {
904
- resolve4();
941
+ resolve5();
905
942
  } else if (message instanceof Error) {
906
943
  reject(message);
907
944
  } else {
@@ -1291,7 +1328,7 @@ var PQueue = class extends import_index.default {
1291
1328
  // Assign unique ID if not provided
1292
1329
  id: options.id ?? (this.#idAssigner++).toString()
1293
1330
  };
1294
- return new Promise((resolve4, reject) => {
1331
+ return new Promise((resolve5, reject) => {
1295
1332
  const taskSymbol = /* @__PURE__ */ Symbol(`task-${options.id}`);
1296
1333
  this.#queue.enqueue(async () => {
1297
1334
  this.#pending++;
@@ -1329,7 +1366,7 @@ var PQueue = class extends import_index.default {
1329
1366
  })]);
1330
1367
  }
1331
1368
  const result = await operation;
1332
- resolve4(result);
1369
+ resolve5(result);
1333
1370
  this.emit("completed", result);
1334
1371
  } catch (error) {
1335
1372
  reject(error);
@@ -1486,13 +1523,13 @@ var PQueue = class extends import_index.default {
1486
1523
  });
1487
1524
  }
1488
1525
  async #onEvent(event, filter) {
1489
- return new Promise((resolve4) => {
1526
+ return new Promise((resolve5) => {
1490
1527
  const listener = () => {
1491
1528
  if (filter && !filter()) {
1492
1529
  return;
1493
1530
  }
1494
1531
  this.off(event, listener);
1495
- resolve4();
1532
+ resolve5();
1496
1533
  };
1497
1534
  this.on(event, listener);
1498
1535
  });
@@ -1777,7 +1814,7 @@ async function onAttemptFailure({ error, attemptNumber, retriesConsumed, startTi
1777
1814
  const finalDelay = Math.min(delayTime, remainingTime);
1778
1815
  options.signal?.throwIfAborted();
1779
1816
  if (finalDelay > 0) {
1780
- await new Promise((resolve4, reject) => {
1817
+ await new Promise((resolve5, reject) => {
1781
1818
  const onAbort = () => {
1782
1819
  clearTimeout(timeoutToken);
1783
1820
  options.signal?.removeEventListener("abort", onAbort);
@@ -1785,7 +1822,7 @@ async function onAttemptFailure({ error, attemptNumber, retriesConsumed, startTi
1785
1822
  };
1786
1823
  const timeoutToken = setTimeout(() => {
1787
1824
  options.signal?.removeEventListener("abort", onAbort);
1788
- resolve4();
1825
+ resolve5();
1789
1826
  }, finalDelay);
1790
1827
  if (options.unref) {
1791
1828
  timeoutToken.unref?.();
@@ -1849,12 +1886,6 @@ async function pRetry(input, options = {}) {
1849
1886
  import { existsSync, readFileSync } from "fs";
1850
1887
  import * as path from "path";
1851
1888
  import * as os from "os";
1852
- var EMBEDDING_CAPABLE_PROVIDERS = [
1853
- "github-copilot",
1854
- "openai",
1855
- "google",
1856
- "ollama"
1857
- ];
1858
1889
  function getOpenCodeAuthPath() {
1859
1890
  return path.join(os.homedir(), ".local", "share", "opencode", "auth.json");
1860
1891
  }
@@ -1868,21 +1899,34 @@ function loadOpenCodeAuth() {
1868
1899
  }
1869
1900
  return {};
1870
1901
  }
1871
- async function detectEmbeddingProvider(preferredProvider) {
1872
- if (preferredProvider && preferredProvider !== "auto") {
1873
- const credentials = await getProviderCredentials(preferredProvider);
1874
- if (credentials) {
1902
+ async function detectEmbeddingProvider(preferredProvider, model) {
1903
+ const credentials = await getProviderCredentials(preferredProvider);
1904
+ if (credentials) {
1905
+ if (!model) {
1875
1906
  return {
1876
1907
  provider: preferredProvider,
1877
1908
  credentials,
1878
1909
  modelInfo: getDefaultModelForProvider(preferredProvider)
1879
1910
  };
1880
1911
  }
1881
- throw new Error(
1882
- `Preferred provider '${preferredProvider}' is not configured or authenticated`
1883
- );
1912
+ if (!isValidModel(model, preferredProvider)) {
1913
+ throw new Error(
1914
+ `Model '${model}' is not supported by provider '${preferredProvider}'`
1915
+ );
1916
+ }
1917
+ const providerModels = EMBEDDING_MODELS[preferredProvider];
1918
+ return {
1919
+ provider: preferredProvider,
1920
+ credentials,
1921
+ modelInfo: providerModels[model]
1922
+ };
1884
1923
  }
1885
- for (const provider of EMBEDDING_CAPABLE_PROVIDERS) {
1924
+ throw new Error(
1925
+ `Preferred provider '${preferredProvider}' is not configured or authenticated`
1926
+ );
1927
+ }
1928
+ async function tryDetectProvider() {
1929
+ for (const provider of availableProviders) {
1886
1930
  const credentials = await getProviderCredentials(provider);
1887
1931
  if (credentials) {
1888
1932
  return {
@@ -1893,7 +1937,7 @@ async function detectEmbeddingProvider(preferredProvider) {
1893
1937
  }
1894
1938
  }
1895
1939
  throw new Error(
1896
- `No embedding-capable provider found. Please authenticate with OpenCode using one of: ${EMBEDDING_CAPABLE_PROVIDERS.join(", ")}.`
1940
+ `No embedding-capable provider found. Please authenticate with OpenCode using one of: ${availableProviders.join(", ")}.`
1897
1941
  );
1898
1942
  }
1899
1943
  async function getProviderCredentials(provider) {
@@ -1991,18 +2035,20 @@ function getProviderDisplayName(provider) {
1991
2035
  }
1992
2036
 
1993
2037
  // src/embeddings/provider.ts
1994
- function createEmbeddingProvider(credentials, modelInfo) {
1995
- switch (credentials.provider) {
2038
+ function createEmbeddingProvider(configuredProviderInfo) {
2039
+ switch (configuredProviderInfo.provider) {
1996
2040
  case "github-copilot":
1997
- return new GitHubCopilotEmbeddingProvider(credentials, modelInfo);
2041
+ return new GitHubCopilotEmbeddingProvider(configuredProviderInfo.credentials, configuredProviderInfo.modelInfo);
1998
2042
  case "openai":
1999
- return new OpenAIEmbeddingProvider(credentials, modelInfo);
2043
+ return new OpenAIEmbeddingProvider(configuredProviderInfo.credentials, configuredProviderInfo.modelInfo);
2000
2044
  case "google":
2001
- return new GoogleEmbeddingProvider(credentials, modelInfo);
2045
+ return new GoogleEmbeddingProvider(configuredProviderInfo.credentials, configuredProviderInfo.modelInfo);
2002
2046
  case "ollama":
2003
- return new OllamaEmbeddingProvider(credentials, modelInfo);
2004
- default:
2005
- throw new Error(`Unsupported embedding provider: ${credentials.provider}`);
2047
+ return new OllamaEmbeddingProvider(configuredProviderInfo.credentials, configuredProviderInfo.modelInfo);
2048
+ default: {
2049
+ const _exhaustive = configuredProviderInfo;
2050
+ throw new Error(`Unsupported embedding provider: ${_exhaustive.provider}`);
2051
+ }
2006
2052
  }
2007
2053
  }
2008
2054
  var GitHubCopilotEmbeddingProvider = class {
@@ -2016,8 +2062,15 @@ var GitHubCopilotEmbeddingProvider = class {
2016
2062
  }
2017
2063
  return this.credentials.refreshToken;
2018
2064
  }
2019
- async embed(text) {
2020
- const result = await this.embedBatch([text]);
2065
+ async embedQuery(query) {
2066
+ const result = await this.embedBatch([query]);
2067
+ return {
2068
+ embedding: result.embeddings[0],
2069
+ tokensUsed: result.totalTokensUsed
2070
+ };
2071
+ }
2072
+ async embedDocument(document) {
2073
+ const result = await this.embedBatch([document]);
2021
2074
  return {
2022
2075
  embedding: result.embeddings[0],
2023
2076
  tokensUsed: result.totalTokensUsed
@@ -2057,8 +2110,15 @@ var OpenAIEmbeddingProvider = class {
2057
2110
  this.credentials = credentials;
2058
2111
  this.modelInfo = modelInfo;
2059
2112
  }
2060
- async embed(text) {
2061
- const result = await this.embedBatch([text]);
2113
+ async embedQuery(query) {
2114
+ const result = await this.embedBatch([query]);
2115
+ return {
2116
+ embedding: result.embeddings[0],
2117
+ tokensUsed: result.totalTokensUsed
2118
+ };
2119
+ }
2120
+ async embedDocument(document) {
2121
+ const result = await this.embedBatch([document]);
2062
2122
  return {
2063
2123
  embedding: result.embeddings[0],
2064
2124
  tokensUsed: result.totalTokensUsed
@@ -2090,33 +2150,61 @@ var OpenAIEmbeddingProvider = class {
2090
2150
  return this.modelInfo;
2091
2151
  }
2092
2152
  };
2093
- var GoogleEmbeddingProvider = class {
2153
+ var GoogleEmbeddingProvider = class _GoogleEmbeddingProvider {
2094
2154
  constructor(credentials, modelInfo) {
2095
2155
  this.credentials = credentials;
2096
2156
  this.modelInfo = modelInfo;
2097
2157
  }
2098
- async embed(text) {
2099
- const result = await this.embedBatch([text]);
2158
+ static BATCH_SIZE = 20;
2159
+ async embedQuery(query) {
2160
+ const taskType = this.modelInfo.taskAble ? "CODE_RETRIEVAL_QUERY" : void 0;
2161
+ const result = await this.embedWithTaskType([query], taskType);
2162
+ return {
2163
+ embedding: result.embeddings[0],
2164
+ tokensUsed: result.totalTokensUsed
2165
+ };
2166
+ }
2167
+ async embedDocument(document) {
2168
+ const taskType = this.modelInfo.taskAble ? "RETRIEVAL_DOCUMENT" : void 0;
2169
+ const result = await this.embedWithTaskType([document], taskType);
2100
2170
  return {
2101
2171
  embedding: result.embeddings[0],
2102
2172
  tokensUsed: result.totalTokensUsed
2103
2173
  };
2104
2174
  }
2105
2175
  async embedBatch(texts) {
2106
- const results = await Promise.all(
2107
- texts.map(async (text) => {
2176
+ const taskType = this.modelInfo.taskAble ? "RETRIEVAL_DOCUMENT" : void 0;
2177
+ return this.embedWithTaskType(texts, taskType);
2178
+ }
2179
+ /**
2180
+ * Embeds texts using the Google embedContent API.
2181
+ * Sends multiple texts as parts in batched requests (up to BATCH_SIZE per call).
2182
+ * When taskType is provided (gemini-embedding-001), includes it in the request
2183
+ * for task-specific embedding optimization.
2184
+ */
2185
+ async embedWithTaskType(texts, taskType) {
2186
+ const batches = [];
2187
+ for (let i = 0; i < texts.length; i += _GoogleEmbeddingProvider.BATCH_SIZE) {
2188
+ batches.push(texts.slice(i, i + _GoogleEmbeddingProvider.BATCH_SIZE));
2189
+ }
2190
+ const batchResults = await Promise.all(
2191
+ batches.map(async (batch) => {
2192
+ const requests = batch.map((text) => ({
2193
+ model: `models/${this.modelInfo.model}`,
2194
+ content: {
2195
+ parts: [{ text }]
2196
+ },
2197
+ taskType,
2198
+ outputDimensionality: this.modelInfo.dimensions
2199
+ }));
2108
2200
  const response = await fetch(
2109
- `${this.credentials.baseUrl}/models/${this.modelInfo.model}:embedContent?key=${this.credentials.apiKey}`,
2201
+ `${this.credentials.baseUrl}/models/${this.modelInfo.model}:batchEmbedContents?key=${this.credentials.apiKey}`,
2110
2202
  {
2111
2203
  method: "POST",
2112
2204
  headers: {
2113
2205
  "Content-Type": "application/json"
2114
2206
  },
2115
- body: JSON.stringify({
2116
- content: {
2117
- parts: [{ text }]
2118
- }
2119
- })
2207
+ body: JSON.stringify({ requests })
2120
2208
  }
2121
2209
  );
2122
2210
  if (!response.ok) {
@@ -2125,14 +2213,14 @@ var GoogleEmbeddingProvider = class {
2125
2213
  }
2126
2214
  const data = await response.json();
2127
2215
  return {
2128
- embedding: data.embedding.values,
2129
- tokensUsed: Math.ceil(text.length / 4)
2216
+ embeddings: data.embeddings.map((e) => e.values),
2217
+ tokensUsed: batch.reduce((sum, text) => sum + Math.ceil(text.length / 4), 0)
2130
2218
  };
2131
2219
  })
2132
2220
  );
2133
2221
  return {
2134
- embeddings: results.map((r) => r.embedding),
2135
- totalTokensUsed: results.reduce((sum, r) => sum + r.tokensUsed, 0)
2222
+ embeddings: batchResults.flatMap((r) => r.embeddings),
2223
+ totalTokensUsed: batchResults.reduce((sum, r) => sum + r.tokensUsed, 0)
2136
2224
  };
2137
2225
  }
2138
2226
  getModelInfo() {
@@ -2144,29 +2232,44 @@ var OllamaEmbeddingProvider = class {
2144
2232
  this.credentials = credentials;
2145
2233
  this.modelInfo = modelInfo;
2146
2234
  }
2147
- async embed(text) {
2148
- const response = await fetch(`${this.credentials.baseUrl}/api/embeddings`, {
2149
- method: "POST",
2150
- headers: {
2151
- "Content-Type": "application/json"
2152
- },
2153
- body: JSON.stringify({
2154
- model: this.modelInfo.model,
2155
- prompt: text
2156
- })
2157
- });
2158
- if (!response.ok) {
2159
- const error = await response.text();
2160
- throw new Error(`Ollama embedding API error: ${response.status} - ${error}`);
2161
- }
2162
- const data = await response.json();
2235
+ async embedQuery(query) {
2236
+ const result = await this.embedBatch([query]);
2163
2237
  return {
2164
- embedding: data.embedding,
2165
- tokensUsed: Math.ceil(text.length / 4)
2238
+ embedding: result.embeddings[0],
2239
+ tokensUsed: result.totalTokensUsed
2240
+ };
2241
+ }
2242
+ async embedDocument(document) {
2243
+ const result = await this.embedBatch([document]);
2244
+ return {
2245
+ embedding: result.embeddings[0],
2246
+ tokensUsed: result.totalTokensUsed
2166
2247
  };
2167
2248
  }
2168
2249
  async embedBatch(texts) {
2169
- const results = await Promise.all(texts.map((text) => this.embed(text)));
2250
+ const results = await Promise.all(
2251
+ texts.map(async (text) => {
2252
+ const response = await fetch(`${this.credentials.baseUrl}/api/embeddings`, {
2253
+ method: "POST",
2254
+ headers: {
2255
+ "Content-Type": "application/json"
2256
+ },
2257
+ body: JSON.stringify({
2258
+ model: this.modelInfo.model,
2259
+ prompt: text
2260
+ })
2261
+ });
2262
+ if (!response.ok) {
2263
+ const error = await response.text();
2264
+ throw new Error(`Ollama embedding API error: ${response.status} - ${error}`);
2265
+ }
2266
+ const data = await response.json();
2267
+ return {
2268
+ embedding: data.embedding,
2269
+ tokensUsed: Math.ceil(text.length / 4)
2270
+ };
2271
+ })
2272
+ );
2170
2273
  return {
2171
2274
  embeddings: results.map((r) => r.embedding),
2172
2275
  totalTokensUsed: results.reduce((sum, r) => sum + r.tokensUsed, 0)
@@ -2181,6 +2284,30 @@ var OllamaEmbeddingProvider = class {
2181
2284
  var import_ignore = __toESM(require_ignore(), 1);
2182
2285
  import { existsSync as existsSync2, readFileSync as readFileSync2, promises as fsPromises } from "fs";
2183
2286
  import * as path2 from "path";
2287
+ var PROJECT_MARKERS = [
2288
+ ".git",
2289
+ "package.json",
2290
+ "Cargo.toml",
2291
+ "go.mod",
2292
+ "pyproject.toml",
2293
+ "setup.py",
2294
+ "requirements.txt",
2295
+ "Gemfile",
2296
+ "composer.json",
2297
+ "pom.xml",
2298
+ "build.gradle",
2299
+ "CMakeLists.txt",
2300
+ "Makefile",
2301
+ ".opencode"
2302
+ ];
2303
+ function hasProjectMarker(projectRoot) {
2304
+ for (const marker of PROJECT_MARKERS) {
2305
+ if (existsSync2(path2.join(projectRoot, marker))) {
2306
+ return true;
2307
+ }
2308
+ }
2309
+ return false;
2310
+ }
2184
2311
  function createIgnoreFilter(projectRoot) {
2185
2312
  const ig = (0, import_ignore.default)();
2186
2313
  const defaultIgnores = [
@@ -3086,11 +3213,40 @@ var Database = class {
3086
3213
  import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync, statSync } from "fs";
3087
3214
  import * as path4 from "path";
3088
3215
  import { execSync } from "child_process";
3216
+ function resolveGitDir(repoRoot) {
3217
+ const gitPath = path4.join(repoRoot, ".git");
3218
+ if (!existsSync3(gitPath)) {
3219
+ return null;
3220
+ }
3221
+ try {
3222
+ const stat4 = statSync(gitPath);
3223
+ if (stat4.isDirectory()) {
3224
+ return gitPath;
3225
+ }
3226
+ if (stat4.isFile()) {
3227
+ const content = readFileSync3(gitPath, "utf-8").trim();
3228
+ const match = content.match(/^gitdir:\s*(.+)$/);
3229
+ if (match) {
3230
+ const gitdir = match[1];
3231
+ const resolvedPath = path4.isAbsolute(gitdir) ? gitdir : path4.resolve(repoRoot, gitdir);
3232
+ if (existsSync3(resolvedPath)) {
3233
+ return resolvedPath;
3234
+ }
3235
+ }
3236
+ }
3237
+ } catch {
3238
+ }
3239
+ return null;
3240
+ }
3089
3241
  function isGitRepo(dir) {
3090
- return existsSync3(path4.join(dir, ".git"));
3242
+ return resolveGitDir(dir) !== null;
3091
3243
  }
3092
3244
  function getCurrentBranch(repoRoot) {
3093
- const headPath = path4.join(repoRoot, ".git", "HEAD");
3245
+ const gitDir = resolveGitDir(repoRoot);
3246
+ if (!gitDir) {
3247
+ return null;
3248
+ }
3249
+ const headPath = path4.join(gitDir, "HEAD");
3094
3250
  if (!existsSync3(headPath)) {
3095
3251
  return null;
3096
3252
  }
@@ -3109,20 +3265,23 @@ function getCurrentBranch(repoRoot) {
3109
3265
  }
3110
3266
  }
3111
3267
  function getBaseBranch(repoRoot) {
3268
+ const gitDir = resolveGitDir(repoRoot);
3112
3269
  const candidates = ["main", "master", "develop", "trunk"];
3113
- for (const candidate of candidates) {
3114
- const refPath = path4.join(repoRoot, ".git", "refs", "heads", candidate);
3115
- if (existsSync3(refPath)) {
3116
- return candidate;
3117
- }
3118
- const packedRefsPath = path4.join(repoRoot, ".git", "packed-refs");
3119
- if (existsSync3(packedRefsPath)) {
3120
- try {
3121
- const content = readFileSync3(packedRefsPath, "utf-8");
3122
- if (content.includes(`refs/heads/${candidate}`)) {
3123
- return candidate;
3270
+ if (gitDir) {
3271
+ for (const candidate of candidates) {
3272
+ const refPath = path4.join(gitDir, "refs", "heads", candidate);
3273
+ if (existsSync3(refPath)) {
3274
+ return candidate;
3275
+ }
3276
+ const packedRefsPath = path4.join(gitDir, "packed-refs");
3277
+ if (existsSync3(packedRefsPath)) {
3278
+ try {
3279
+ const content = readFileSync3(packedRefsPath, "utf-8");
3280
+ if (content.includes(`refs/heads/${candidate}`)) {
3281
+ return candidate;
3282
+ }
3283
+ } catch {
3124
3284
  }
3125
- } catch {
3126
3285
  }
3127
3286
  }
3128
3287
  }
@@ -3147,6 +3306,10 @@ function getBranchOrDefault(repoRoot) {
3147
3306
  return getCurrentBranch(repoRoot) ?? "default";
3148
3307
  }
3149
3308
  function getHeadPath(repoRoot) {
3309
+ const gitDir = resolveGitDir(repoRoot);
3310
+ if (gitDir) {
3311
+ return path4.join(gitDir, "HEAD");
3312
+ }
3150
3313
  return path4.join(repoRoot, ".git", "HEAD");
3151
3314
  }
3152
3315
 
@@ -3174,6 +3337,7 @@ function isRateLimitError(error) {
3174
3337
  const message = getErrorMessage(error);
3175
3338
  return message.includes("429") || message.toLowerCase().includes("rate limit") || message.toLowerCase().includes("too many requests");
3176
3339
  }
3340
+ var INDEX_METADATA_VERSION = "1";
3177
3341
  var Indexer = class {
3178
3342
  config;
3179
3343
  projectRoot;
@@ -3182,7 +3346,7 @@ var Indexer = class {
3182
3346
  invertedIndex = null;
3183
3347
  database = null;
3184
3348
  provider = null;
3185
- detectedProvider = null;
3349
+ configuredProviderInfo = null;
3186
3350
  fileHashCache = /* @__PURE__ */ new Map();
3187
3351
  fileHashCachePath = "";
3188
3352
  failedBatchesPath = "";
@@ -3193,12 +3357,15 @@ var Indexer = class {
3193
3357
  maxQueryCacheSize = 100;
3194
3358
  queryCacheTtlMs = 5 * 60 * 1e3;
3195
3359
  querySimilarityThreshold = 0.85;
3360
+ indexCompatibility = null;
3361
+ indexingLockPath = "";
3196
3362
  constructor(projectRoot, config) {
3197
3363
  this.projectRoot = projectRoot;
3198
3364
  this.config = config;
3199
3365
  this.indexPath = this.getIndexPath();
3200
3366
  this.fileHashCachePath = path5.join(this.indexPath, "file-hashes.json");
3201
3367
  this.failedBatchesPath = path5.join(this.indexPath, "failed-batches.json");
3368
+ this.indexingLockPath = path5.join(this.indexPath, "indexing.lock");
3202
3369
  this.logger = initializeLogger(config.debug);
3203
3370
  }
3204
3371
  getIndexPath() {
@@ -3224,7 +3391,36 @@ var Indexer = class {
3224
3391
  for (const [k, v] of this.fileHashCache) {
3225
3392
  obj[k] = v;
3226
3393
  }
3227
- writeFileSync(this.fileHashCachePath, JSON.stringify(obj));
3394
+ this.atomicWriteSync(this.fileHashCachePath, JSON.stringify(obj));
3395
+ }
3396
+ atomicWriteSync(targetPath, data) {
3397
+ const tempPath = `${targetPath}.tmp`;
3398
+ writeFileSync(tempPath, data);
3399
+ renameSync(tempPath, targetPath);
3400
+ }
3401
+ checkForInterruptedIndexing() {
3402
+ return existsSync4(this.indexingLockPath);
3403
+ }
3404
+ acquireIndexingLock() {
3405
+ const lockData = {
3406
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3407
+ pid: process.pid
3408
+ };
3409
+ writeFileSync(this.indexingLockPath, JSON.stringify(lockData));
3410
+ }
3411
+ releaseIndexingLock() {
3412
+ if (existsSync4(this.indexingLockPath)) {
3413
+ unlinkSync(this.indexingLockPath);
3414
+ }
3415
+ }
3416
+ async recoverFromInterruptedIndexing() {
3417
+ this.logger.warn("Detected interrupted indexing session, recovering...");
3418
+ if (existsSync4(this.fileHashCachePath)) {
3419
+ unlinkSync(this.fileHashCachePath);
3420
+ }
3421
+ await this.healthCheck();
3422
+ this.releaseIndexingLock();
3423
+ this.logger.info("Recovery complete, next index will re-process all files");
3228
3424
  }
3229
3425
  loadFailedBatches() {
3230
3426
  try {
@@ -3272,23 +3468,27 @@ var Indexer = class {
3272
3468
  }
3273
3469
  }
3274
3470
  async initialize() {
3275
- this.detectedProvider = await detectEmbeddingProvider(this.config.embeddingProvider);
3276
- if (!this.detectedProvider) {
3471
+ if (this.config.embeddingProvider === "auto") {
3472
+ this.configuredProviderInfo = await tryDetectProvider();
3473
+ } else {
3474
+ this.configuredProviderInfo = await detectEmbeddingProvider(this.config.embeddingProvider, this.config.embeddingModel);
3475
+ }
3476
+ if (!this.configuredProviderInfo) {
3277
3477
  throw new Error(
3278
3478
  "No embedding provider available. Configure GitHub, OpenAI, Google, or Ollama."
3279
3479
  );
3280
3480
  }
3281
3481
  this.logger.info("Initializing indexer", {
3282
- provider: this.detectedProvider.provider,
3283
- model: this.detectedProvider.modelInfo.model,
3482
+ provider: this.configuredProviderInfo.provider,
3483
+ model: this.configuredProviderInfo.modelInfo.model,
3284
3484
  scope: this.config.scope
3285
3485
  });
3286
- this.provider = createEmbeddingProvider(
3287
- this.detectedProvider.credentials,
3288
- this.detectedProvider.modelInfo
3289
- );
3486
+ this.provider = createEmbeddingProvider(this.configuredProviderInfo);
3290
3487
  await fsPromises2.mkdir(this.indexPath, { recursive: true });
3291
- const dimensions = this.detectedProvider.modelInfo.dimensions;
3488
+ if (this.checkForInterruptedIndexing()) {
3489
+ await this.recoverFromInterruptedIndexing();
3490
+ }
3491
+ const dimensions = this.configuredProviderInfo.modelInfo.dimensions;
3292
3492
  const storePath = path5.join(this.indexPath, "vectors");
3293
3493
  this.store = new VectorStore(storePath, dimensions);
3294
3494
  const indexFilePath = path5.join(this.indexPath, "vectors.usearch");
@@ -3311,6 +3511,14 @@ var Indexer = class {
3311
3511
  if (dbIsNew && this.store.count() > 0) {
3312
3512
  this.migrateFromLegacyIndex();
3313
3513
  }
3514
+ this.indexCompatibility = this.validateIndexCompatibility(this.configuredProviderInfo);
3515
+ if (!this.indexCompatibility.compatible) {
3516
+ this.logger.warn("Index compatibility issue detected", {
3517
+ reason: this.indexCompatibility.reason,
3518
+ storedMetadata: this.indexCompatibility.storedMetadata,
3519
+ configuredProviderInfo: this.configuredProviderInfo
3520
+ });
3521
+ }
3314
3522
  if (isGitRepo(this.projectRoot)) {
3315
3523
  this.currentBranch = getBranchOrDefault(this.projectRoot);
3316
3524
  this.baseBranch = getBaseBranch(this.projectRoot);
@@ -3381,30 +3589,106 @@ var Indexer = class {
3381
3589
  }
3382
3590
  this.database.addChunksToBranchBatch(this.currentBranch || "default", chunkIds);
3383
3591
  }
3592
+ loadIndexMetadata() {
3593
+ if (!this.database) return null;
3594
+ const version = this.database.getMetadata("index.version");
3595
+ if (!version) return null;
3596
+ return {
3597
+ indexVersion: version,
3598
+ embeddingProvider: this.database.getMetadata("index.embeddingProvider") ?? "",
3599
+ embeddingModel: this.database.getMetadata("index.embeddingModel") ?? "",
3600
+ embeddingDimensions: parseInt(this.database.getMetadata("index.embeddingDimensions") ?? "0", 10),
3601
+ createdAt: this.database.getMetadata("index.createdAt") ?? "",
3602
+ updatedAt: this.database.getMetadata("index.updatedAt") ?? ""
3603
+ };
3604
+ }
3605
+ saveIndexMetadata(provider) {
3606
+ if (!this.database) return;
3607
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3608
+ const existingCreatedAt = this.database.getMetadata("index.createdAt");
3609
+ this.database.setMetadata("index.version", INDEX_METADATA_VERSION);
3610
+ this.database.setMetadata("index.embeddingProvider", provider.provider);
3611
+ this.database.setMetadata("index.embeddingModel", provider.modelInfo.model);
3612
+ this.database.setMetadata("index.embeddingDimensions", provider.modelInfo.dimensions.toString());
3613
+ this.database.setMetadata("index.updatedAt", now);
3614
+ if (!existingCreatedAt) {
3615
+ this.database.setMetadata("index.createdAt", now);
3616
+ }
3617
+ }
3618
+ validateIndexCompatibility(provider) {
3619
+ const storedMetadata = this.loadIndexMetadata();
3620
+ if (!storedMetadata) {
3621
+ return { compatible: true };
3622
+ }
3623
+ const currentProvider = provider.provider;
3624
+ const currentModel = provider.modelInfo.model;
3625
+ const currentDimensions = provider.modelInfo.dimensions;
3626
+ if (storedMetadata.embeddingDimensions !== currentDimensions) {
3627
+ return {
3628
+ compatible: false,
3629
+ code: "DIMENSION_MISMATCH" /* DIMENSION_MISMATCH */,
3630
+ 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.`,
3631
+ storedMetadata
3632
+ };
3633
+ }
3634
+ if (storedMetadata.embeddingModel !== currentModel) {
3635
+ return {
3636
+ compatible: false,
3637
+ code: "MODEL_MISMATCH" /* MODEL_MISMATCH */,
3638
+ 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.`,
3639
+ storedMetadata
3640
+ };
3641
+ }
3642
+ if (storedMetadata.embeddingProvider !== currentProvider) {
3643
+ this.logger.warn("Provider changed", {
3644
+ storedProvider: storedMetadata.embeddingProvider,
3645
+ currentProvider
3646
+ });
3647
+ }
3648
+ return {
3649
+ compatible: true,
3650
+ storedMetadata
3651
+ };
3652
+ }
3653
+ checkCompatibility() {
3654
+ if (!this.indexCompatibility) {
3655
+ if (!this.configuredProviderInfo) {
3656
+ throw new Error("No embedding provider info, you must initialize the indexer first.");
3657
+ }
3658
+ this.indexCompatibility = this.validateIndexCompatibility(this.configuredProviderInfo);
3659
+ }
3660
+ return this.indexCompatibility;
3661
+ }
3384
3662
  async ensureInitialized() {
3385
- if (!this.store || !this.provider || !this.invertedIndex || !this.detectedProvider || !this.database) {
3663
+ if (!this.store || !this.provider || !this.invertedIndex || !this.configuredProviderInfo || !this.database) {
3386
3664
  await this.initialize();
3387
3665
  }
3388
3666
  return {
3389
3667
  store: this.store,
3390
3668
  provider: this.provider,
3391
3669
  invertedIndex: this.invertedIndex,
3392
- detectedProvider: this.detectedProvider,
3670
+ configuredProviderInfo: this.configuredProviderInfo,
3393
3671
  database: this.database
3394
3672
  };
3395
3673
  }
3396
3674
  async estimateCost() {
3397
- const { detectedProvider } = await this.ensureInitialized();
3675
+ const { configuredProviderInfo } = await this.ensureInitialized();
3398
3676
  const { files } = await collectFiles(
3399
3677
  this.projectRoot,
3400
3678
  this.config.include,
3401
3679
  this.config.exclude,
3402
3680
  this.config.indexing.maxFileSize
3403
3681
  );
3404
- return createCostEstimate(files, detectedProvider);
3682
+ return createCostEstimate(files, configuredProviderInfo);
3405
3683
  }
3406
3684
  async index(onProgress) {
3407
- const { store, provider, invertedIndex, database, detectedProvider } = await this.ensureInitialized();
3685
+ const { store, provider, invertedIndex, database, configuredProviderInfo } = await this.ensureInitialized();
3686
+ if (!this.indexCompatibility?.compatible) {
3687
+ throw new Error(
3688
+ `${this.indexCompatibility?.reason} Run index_codebase with force=true to rebuild the index.`
3689
+ );
3690
+ }
3691
+ this.acquireIndexingLock();
3408
3692
  this.logger.recordIndexingStart();
3409
3693
  this.logger.info("Starting indexing", { projectRoot: this.projectRoot });
3410
3694
  const startTime = Date.now();
@@ -3511,7 +3795,7 @@ var Indexer = class {
3511
3795
  const id = generateChunkId(parsed.path, chunk);
3512
3796
  const contentHash = generateChunkHash(chunk);
3513
3797
  currentChunkIds.add(id);
3514
- const chunkData = {
3798
+ chunkDataBatch.push({
3515
3799
  chunkId: id,
3516
3800
  contentHash,
3517
3801
  filePath: parsed.path,
@@ -3520,8 +3804,7 @@ var Indexer = class {
3520
3804
  nodeType: chunk.chunkType,
3521
3805
  name: chunk.name,
3522
3806
  language: chunk.language
3523
- };
3524
- chunkDataBatch.push(chunkData);
3807
+ });
3525
3808
  if (existingChunks.get(id) === contentHash) {
3526
3809
  fileChunkCount++;
3527
3810
  continue;
@@ -3574,6 +3857,7 @@ var Indexer = class {
3574
3857
  chunksProcessed: 0,
3575
3858
  totalChunks: 0
3576
3859
  });
3860
+ this.releaseIndexingLock();
3577
3861
  return stats;
3578
3862
  }
3579
3863
  if (pendingChunks.length === 0) {
@@ -3591,6 +3875,7 @@ var Indexer = class {
3591
3875
  chunksProcessed: 0,
3592
3876
  totalChunks: 0
3593
3877
  });
3878
+ this.releaseIndexingLock();
3594
3879
  return stats;
3595
3880
  }
3596
3881
  onProgress?.({
@@ -3619,7 +3904,7 @@ var Indexer = class {
3619
3904
  stats.indexedChunks++;
3620
3905
  }
3621
3906
  }
3622
- const providerRateLimits = this.getProviderRateLimits(detectedProvider.provider);
3907
+ const providerRateLimits = this.getProviderRateLimits(configuredProviderInfo.provider);
3623
3908
  const queue = new PQueue({
3624
3909
  concurrency: providerRateLimits.concurrency,
3625
3910
  interval: providerRateLimits.intervalMs,
@@ -3630,7 +3915,7 @@ var Indexer = class {
3630
3915
  for (const batch of dynamicBatches) {
3631
3916
  queue.add(async () => {
3632
3917
  if (rateLimitBackoffMs > 0) {
3633
- await new Promise((resolve4) => setTimeout(resolve4, rateLimitBackoffMs));
3918
+ await new Promise((resolve5) => setTimeout(resolve5, rateLimitBackoffMs));
3634
3919
  }
3635
3920
  try {
3636
3921
  const result = await pRetry(
@@ -3674,7 +3959,7 @@ var Indexer = class {
3674
3959
  contentHash: chunk.contentHash,
3675
3960
  embedding: float32ArrayToBuffer(result.embeddings[i]),
3676
3961
  chunkText: chunk.text,
3677
- model: detectedProvider.modelInfo.model
3962
+ model: configuredProviderInfo.modelInfo.model
3678
3963
  }));
3679
3964
  database.upsertEmbeddingsBatch(embeddingBatchItems);
3680
3965
  for (const chunk of batch) {
@@ -3725,6 +4010,8 @@ var Indexer = class {
3725
4010
  await this.maybeRunOrphanGc();
3726
4011
  }
3727
4012
  stats.durationMs = Date.now() - startTime;
4013
+ this.saveIndexMetadata(configuredProviderInfo);
4014
+ this.indexCompatibility = { compatible: true };
3728
4015
  this.logger.recordIndexingEnd();
3729
4016
  this.logger.info("Indexing complete", {
3730
4017
  files: stats.totalFiles,
@@ -3745,6 +4032,7 @@ var Indexer = class {
3745
4032
  chunksProcessed: stats.indexedChunks,
3746
4033
  totalChunks: pendingChunks.length
3747
4034
  });
4035
+ this.releaseIndexingLock();
3748
4036
  return stats;
3749
4037
  }
3750
4038
  async getQueryEmbedding(query, provider) {
@@ -3767,7 +4055,7 @@ var Indexer = class {
3767
4055
  }
3768
4056
  this.logger.cache("debug", "Query embedding cache miss", { query: query.slice(0, 50) });
3769
4057
  this.logger.recordQueryCacheMiss();
3770
- const { embedding, tokensUsed } = await provider.embed(query);
4058
+ const { embedding, tokensUsed } = await provider.embedQuery(query);
3771
4059
  this.logger.recordEmbeddingApiCall(tokensUsed);
3772
4060
  if (this.queryEmbeddingCache.size >= this.maxQueryCacheSize) {
3773
4061
  const oldestKey = this.queryEmbeddingCache.keys().next().value;
@@ -3810,8 +4098,14 @@ var Indexer = class {
3810
4098
  return intersection / union;
3811
4099
  }
3812
4100
  async search(query, limit, options) {
3813
- const searchStartTime = performance2.now();
3814
4101
  const { store, provider, database } = await this.ensureInitialized();
4102
+ const compatibility = this.checkCompatibility();
4103
+ if (!compatibility.compatible) {
4104
+ throw new Error(
4105
+ `${compatibility.reason ?? "Index is incompatible with current embedding provider."} A possible solution is to run index_codebase with force=true to rebuild the index.`
4106
+ );
4107
+ }
4108
+ const searchStartTime = performance2.now();
3815
4109
  if (store.count() === 0) {
3816
4110
  this.logger.search("debug", "Search on empty index", { query });
3817
4111
  return [];
@@ -3953,15 +4247,16 @@ var Indexer = class {
3953
4247
  return results.slice(0, limit);
3954
4248
  }
3955
4249
  async getStatus() {
3956
- const { store, detectedProvider } = await this.ensureInitialized();
4250
+ const { store, configuredProviderInfo } = await this.ensureInitialized();
3957
4251
  return {
3958
4252
  indexed: store.count() > 0,
3959
4253
  vectorCount: store.count(),
3960
- provider: detectedProvider.provider,
3961
- model: detectedProvider.modelInfo.model,
4254
+ provider: configuredProviderInfo.provider,
4255
+ model: configuredProviderInfo.modelInfo.model,
3962
4256
  indexPath: this.indexPath,
3963
4257
  currentBranch: this.currentBranch,
3964
- baseBranch: this.baseBranch
4258
+ baseBranch: this.baseBranch,
4259
+ compatibility: this.indexCompatibility
3965
4260
  };
3966
4261
  }
3967
4262
  async clearIndex() {
@@ -3973,6 +4268,13 @@ var Indexer = class {
3973
4268
  this.fileHashCache.clear();
3974
4269
  this.saveFileHashCache();
3975
4270
  database.clearBranch(this.currentBranch);
4271
+ database.deleteMetadata("index.version");
4272
+ database.deleteMetadata("index.embeddingProvider");
4273
+ database.deleteMetadata("index.embeddingModel");
4274
+ database.deleteMetadata("index.embeddingDimensions");
4275
+ database.deleteMetadata("index.createdAt");
4276
+ database.deleteMetadata("index.updatedAt");
4277
+ this.indexCompatibility = this.validateIndexCompatibility(this.configuredProviderInfo);
3976
4278
  }
3977
4279
  async healthCheck() {
3978
4280
  const { store, invertedIndex, database } = await this.ensureInitialized();
@@ -4086,26 +4388,31 @@ var Indexer = class {
4086
4388
  getLogger() {
4087
4389
  return this.logger;
4088
4390
  }
4089
- async findSimilar(code, limit, options) {
4090
- const searchStartTime = performance2.now();
4391
+ async findSimilar(code, limit = this.config.search.maxResults, options) {
4091
4392
  const { store, provider, database } = await this.ensureInitialized();
4393
+ const compatibility = this.checkCompatibility();
4394
+ if (!compatibility.compatible) {
4395
+ throw new Error(
4396
+ `${compatibility.reason ?? "Index is incompatible with current embedding provider."} Run index_codebase with force=true to rebuild the index.`
4397
+ );
4398
+ }
4399
+ const searchStartTime = performance2.now();
4092
4400
  if (store.count() === 0) {
4093
4401
  this.logger.search("debug", "Find similar on empty index");
4094
4402
  return [];
4095
4403
  }
4096
- const maxResults = limit ?? this.config.search.maxResults;
4097
4404
  const filterByBranch = options?.filterByBranch ?? true;
4098
4405
  this.logger.search("debug", "Starting find similar", {
4099
4406
  codeLength: code.length,
4100
- maxResults,
4407
+ limit,
4101
4408
  filterByBranch
4102
4409
  });
4103
4410
  const embeddingStartTime = performance2.now();
4104
- const { embedding, tokensUsed } = await provider.embed(code);
4411
+ const { embedding, tokensUsed } = await provider.embedDocument(code);
4105
4412
  const embeddingMs = performance2.now() - embeddingStartTime;
4106
4413
  this.logger.recordEmbeddingApiCall(tokensUsed);
4107
4414
  const vectorStartTime = performance2.now();
4108
- const semanticResults = store.search(embedding, maxResults * 2);
4415
+ const semanticResults = store.search(embedding, limit * 2);
4109
4416
  const vectorMs = performance2.now() - vectorStartTime;
4110
4417
  let branchChunkIds = null;
4111
4418
  if (filterByBranch && this.currentBranch !== "default") {
@@ -4129,7 +4436,7 @@ var Indexer = class {
4129
4436
  if (r.metadata.chunkType !== options.chunkType) return false;
4130
4437
  }
4131
4438
  return true;
4132
- }).slice(0, maxResults);
4439
+ }).slice(0, limit);
4133
4440
  const totalSearchMs = performance2.now() - searchStartTime;
4134
4441
  this.logger.recordSearch(totalSearchMs, {
4135
4442
  embeddingMs,
@@ -5044,7 +5351,7 @@ var NodeFsHandler = class {
5044
5351
  this._addToNodeFs(path9, initialAdd, wh, depth + 1);
5045
5352
  }
5046
5353
  }).on(EV.ERROR, this._boundHandleError);
5047
- return new Promise((resolve4, reject) => {
5354
+ return new Promise((resolve5, reject) => {
5048
5355
  if (!stream)
5049
5356
  return reject();
5050
5357
  stream.once(STR_END, () => {
@@ -5053,7 +5360,7 @@ var NodeFsHandler = class {
5053
5360
  return;
5054
5361
  }
5055
5362
  const wasThrottled = throttler ? throttler.clear() : false;
5056
- resolve4(void 0);
5363
+ resolve5(void 0);
5057
5364
  previous.getChildren().filter((item) => {
5058
5365
  return item !== directory && !current.has(item);
5059
5366
  }).forEach((item) => {
@@ -6100,6 +6407,175 @@ function createWatcherWithIndexer(indexer, projectRoot, config) {
6100
6407
 
6101
6408
  // src/tools/index.ts
6102
6409
  import { tool } from "@opencode-ai/plugin";
6410
+
6411
+ // src/tools/utils.ts
6412
+ var MAX_CONTENT_LINES = 30;
6413
+ function truncateContent(content) {
6414
+ const lines = content.split("\n");
6415
+ if (lines.length <= MAX_CONTENT_LINES) return content;
6416
+ return lines.slice(0, MAX_CONTENT_LINES).join("\n") + `
6417
+ // ... (${lines.length - MAX_CONTENT_LINES} more lines)`;
6418
+ }
6419
+ function formatIndexStats(stats, verbose = false) {
6420
+ const lines = [];
6421
+ if (stats.indexedChunks === 0 && stats.removedChunks === 0) {
6422
+ lines.push(`Indexed. ${stats.totalFiles} files processed, ${stats.existingChunks} code chunks already up to date.`);
6423
+ } else if (stats.indexedChunks === 0) {
6424
+ lines.push(`Indexed. ${stats.totalFiles} files, removed ${stats.removedChunks} stale chunks, ${stats.existingChunks} chunks remain.`);
6425
+ } else {
6426
+ let main = `Indexed. ${stats.totalFiles} files processed, ${stats.indexedChunks} new chunks embedded.`;
6427
+ if (stats.existingChunks > 0) {
6428
+ main += ` ${stats.existingChunks} unchanged chunks skipped.`;
6429
+ }
6430
+ lines.push(main);
6431
+ if (stats.removedChunks > 0) {
6432
+ lines.push(`Removed ${stats.removedChunks} stale chunks.`);
6433
+ }
6434
+ if (stats.failedChunks > 0) {
6435
+ lines.push(`Failed: ${stats.failedChunks} chunks.`);
6436
+ }
6437
+ lines.push(`Tokens: ${stats.tokensUsed.toLocaleString()}, Duration: ${(stats.durationMs / 1e3).toFixed(1)}s`);
6438
+ }
6439
+ if (verbose) {
6440
+ if (stats.skippedFiles.length > 0) {
6441
+ const tooLarge = stats.skippedFiles.filter((f) => f.reason === "too_large");
6442
+ const excluded = stats.skippedFiles.filter((f) => f.reason === "excluded");
6443
+ const gitignored = stats.skippedFiles.filter((f) => f.reason === "gitignore");
6444
+ lines.push("");
6445
+ lines.push(`Skipped files: ${stats.skippedFiles.length}`);
6446
+ if (tooLarge.length > 0) {
6447
+ lines.push(` Too large (${tooLarge.length}): ${tooLarge.slice(0, 5).map((f) => f.path).join(", ")}${tooLarge.length > 5 ? "..." : ""}`);
6448
+ }
6449
+ if (excluded.length > 0) {
6450
+ lines.push(` Excluded (${excluded.length}): ${excluded.slice(0, 5).map((f) => f.path).join(", ")}${excluded.length > 5 ? "..." : ""}`);
6451
+ }
6452
+ if (gitignored.length > 0) {
6453
+ lines.push(` Gitignored (${gitignored.length}): ${gitignored.slice(0, 5).map((f) => f.path).join(", ")}${gitignored.length > 5 ? "..." : ""}`);
6454
+ }
6455
+ }
6456
+ if (stats.parseFailures.length > 0) {
6457
+ lines.push("");
6458
+ lines.push(`Files with no extractable chunks (${stats.parseFailures.length}): ${stats.parseFailures.slice(0, 10).join(", ")}${stats.parseFailures.length > 10 ? "..." : ""}`);
6459
+ }
6460
+ }
6461
+ return lines.join("\n");
6462
+ }
6463
+ function formatStatus(status) {
6464
+ if (!status.indexed) {
6465
+ return "Codebase is not indexed. Run index_codebase to create an index.";
6466
+ }
6467
+ const lines = [
6468
+ `Index status:`,
6469
+ ` Indexed chunks: ${status.vectorCount.toLocaleString()}`,
6470
+ ` Provider: ${status.provider}`,
6471
+ ` Model: ${status.model}`,
6472
+ ` Location: ${status.indexPath}`
6473
+ ];
6474
+ if (status.currentBranch !== "default") {
6475
+ lines.push(` Current branch: ${status.currentBranch}`);
6476
+ lines.push(` Base branch: ${status.baseBranch}`);
6477
+ }
6478
+ if (status.compatibility && !status.compatibility.compatible) {
6479
+ lines.push("");
6480
+ lines.push(`COMPATIBILITY WARNING: ${status.compatibility.reason}`);
6481
+ if (status.compatibility.storedMetadata) {
6482
+ const stored = status.compatibility.storedMetadata;
6483
+ lines.push(` Index was built with: ${stored.embeddingProvider}/${stored.embeddingModel} (${stored.embeddingDimensions}D)`);
6484
+ lines.push(` Current config: ${status.provider}/${status.model}`);
6485
+ }
6486
+ } else if (!status.compatibility) {
6487
+ lines.push(` Compatibility: No compatibility information found. Maybe the index is not initialized yet, try running index_codebase.`);
6488
+ } else {
6489
+ lines.push(` Compatibility: Index is compatible with the current provider and model.`);
6490
+ }
6491
+ return lines.join("\n");
6492
+ }
6493
+ function formatProgressTitle(progress) {
6494
+ switch (progress.phase) {
6495
+ case "scanning":
6496
+ return "Scanning files...";
6497
+ case "parsing":
6498
+ return `Parsing: ${progress.filesProcessed}/${progress.totalFiles} files`;
6499
+ case "embedding":
6500
+ return `Embedding: ${progress.chunksProcessed}/${progress.totalChunks} chunks`;
6501
+ case "storing":
6502
+ return "Storing index...";
6503
+ case "complete":
6504
+ return "Indexing complete";
6505
+ default:
6506
+ return "Indexing...";
6507
+ }
6508
+ }
6509
+ function calculatePercentage(progress) {
6510
+ if (progress.phase === "scanning") return 0;
6511
+ if (progress.phase === "complete") return 100;
6512
+ if (progress.phase === "parsing") {
6513
+ if (progress.totalFiles === 0) return 5;
6514
+ return Math.round(5 + progress.filesProcessed / progress.totalFiles * 15);
6515
+ }
6516
+ if (progress.phase === "embedding") {
6517
+ if (progress.totalChunks === 0) return 20;
6518
+ return Math.round(20 + progress.chunksProcessed / progress.totalChunks * 70);
6519
+ }
6520
+ if (progress.phase === "storing") return 95;
6521
+ return 0;
6522
+ }
6523
+ function formatCodebasePeek(results, query) {
6524
+ if (results.length === 0) {
6525
+ return "No matching code found. Try a different query or run index_codebase first.";
6526
+ }
6527
+ const formatted = results.map((r, idx) => {
6528
+ const location = `${r.filePath}:${r.startLine}-${r.endLine}`;
6529
+ const name = r.name ? `"${r.name}"` : "(anonymous)";
6530
+ return `[${idx + 1}] ${r.chunkType} ${name} at ${location} (score: ${r.score.toFixed(2)})`;
6531
+ });
6532
+ return `Found ${results.length} locations for "${query}":
6533
+
6534
+ ${formatted.join("\n")}
6535
+
6536
+ Use Read tool to examine specific files.`;
6537
+ }
6538
+ function formatHealthCheck(result) {
6539
+ if (result.removed === 0 && result.gcOrphanEmbeddings === 0 && result.gcOrphanChunks === 0) {
6540
+ return "Index is healthy. No stale entries found.";
6541
+ }
6542
+ const lines = [`Health check complete:`];
6543
+ if (result.removed > 0) {
6544
+ lines.push(` Removed stale entries: ${result.removed}`);
6545
+ }
6546
+ if (result.gcOrphanEmbeddings > 0) {
6547
+ lines.push(` Garbage collected orphan embeddings: ${result.gcOrphanEmbeddings}`);
6548
+ }
6549
+ if (result.gcOrphanChunks > 0) {
6550
+ lines.push(` Garbage collected orphan chunks: ${result.gcOrphanChunks}`);
6551
+ }
6552
+ if (result.filePaths.length > 0) {
6553
+ lines.push(` Cleaned paths: ${result.filePaths.join(", ")}`);
6554
+ }
6555
+ return lines.join("\n");
6556
+ }
6557
+ function formatLogs(logs) {
6558
+ if (logs.length === 0) {
6559
+ return "No logs recorded yet. Logs are captured during indexing and search operations.";
6560
+ }
6561
+ return logs.map((l) => {
6562
+ const dataStr = l.data ? ` ${JSON.stringify(l.data)}` : "";
6563
+ return `[${l.timestamp}] [${l.level.toUpperCase()}] [${l.category}] ${l.message}${dataStr}`;
6564
+ }).join("\n");
6565
+ }
6566
+ function formatSearchResults(results, scoreFormat = "similarity") {
6567
+ const formatted = results.map((r, idx) => {
6568
+ 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}`;
6569
+ const scoreLabel = scoreFormat === "similarity" ? `(similarity: ${(r.score * 100).toFixed(1)}%)` : `(score: ${r.score.toFixed(2)})`;
6570
+ return `${header} ${scoreLabel}
6571
+ \`\`\`
6572
+ ${truncateContent(r.content)}
6573
+ \`\`\``;
6574
+ });
6575
+ return formatted.join("\n\n");
6576
+ }
6577
+
6578
+ // src/tools/index.ts
6103
6579
  var z = tool.schema;
6104
6580
  var sharedIndexer = null;
6105
6581
  function initializeTools(projectRoot, config) {
@@ -6111,39 +6587,6 @@ function getIndexer() {
6111
6587
  }
6112
6588
  return sharedIndexer;
6113
6589
  }
6114
- var codebase_search = tool({
6115
- 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.",
6116
- args: {
6117
- query: z.string().describe("Natural language description of what code you're looking for. Describe behavior, not syntax."),
6118
- limit: z.number().optional().default(10).describe("Maximum number of results to return"),
6119
- fileType: z.string().optional().describe("Filter by file extension (e.g., 'ts', 'py', 'rs')"),
6120
- directory: z.string().optional().describe("Filter by directory path (e.g., 'src/utils', 'lib')"),
6121
- chunkType: z.enum(["function", "class", "method", "interface", "type", "enum", "struct", "impl", "trait", "module", "other"]).optional().describe("Filter by code chunk type"),
6122
- contextLines: z.number().optional().describe("Number of extra lines to include before/after each match (default: 0)")
6123
- },
6124
- async execute(args) {
6125
- const indexer = getIndexer();
6126
- const results = await indexer.search(args.query, args.limit ?? 10, {
6127
- fileType: args.fileType,
6128
- directory: args.directory,
6129
- chunkType: args.chunkType,
6130
- contextLines: args.contextLines
6131
- });
6132
- if (results.length === 0) {
6133
- return "No matching code found. Try a different query or run index_codebase first.";
6134
- }
6135
- const formatted = results.map((r, idx) => {
6136
- 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}`;
6137
- return `${header} (score: ${r.score.toFixed(2)})
6138
- \`\`\`
6139
- ${r.content}
6140
- \`\`\``;
6141
- });
6142
- return `Found ${results.length} results for "${args.query}":
6143
-
6144
- ${formatted.join("\n\n")}`;
6145
- }
6146
- });
6147
6590
  var codebase_peek = tool({
6148
6591
  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.",
6149
6592
  args: {
@@ -6161,19 +6604,7 @@ var codebase_peek = tool({
6161
6604
  chunkType: args.chunkType,
6162
6605
  metadataOnly: true
6163
6606
  });
6164
- if (results.length === 0) {
6165
- return "No matching code found. Try a different query or run index_codebase first.";
6166
- }
6167
- const formatted = results.map((r, idx) => {
6168
- const location = `${r.filePath}:${r.startLine}-${r.endLine}`;
6169
- const name = r.name ? `"${r.name}"` : "(anonymous)";
6170
- return `[${idx + 1}] ${r.chunkType} ${name} at ${location} (score: ${r.score.toFixed(2)})`;
6171
- });
6172
- return `Found ${results.length} locations for "${args.query}":
6173
-
6174
- ${formatted.join("\n")}
6175
-
6176
- Use Read tool to examine specific files.`;
6607
+ return formatCodebasePeek(results, args.query);
6177
6608
  }
6178
6609
  });
6179
6610
  var index_codebase = tool({
@@ -6223,23 +6654,7 @@ var index_health_check = tool({
6223
6654
  async execute() {
6224
6655
  const indexer = getIndexer();
6225
6656
  const result = await indexer.healthCheck();
6226
- if (result.removed === 0 && result.gcOrphanEmbeddings === 0 && result.gcOrphanChunks === 0) {
6227
- return "Index is healthy. No stale entries found.";
6228
- }
6229
- const lines = [`Health check complete:`];
6230
- if (result.removed > 0) {
6231
- lines.push(` Removed stale entries: ${result.removed}`);
6232
- }
6233
- if (result.gcOrphanEmbeddings > 0) {
6234
- lines.push(` Garbage collected orphan embeddings: ${result.gcOrphanEmbeddings}`);
6235
- }
6236
- if (result.gcOrphanChunks > 0) {
6237
- lines.push(` Garbage collected orphan chunks: ${result.gcOrphanChunks}`);
6238
- }
6239
- if (result.filePaths.length > 0) {
6240
- lines.push(` Cleaned paths: ${result.filePaths.join(", ")}`);
6241
- }
6242
- return lines.join("\n");
6657
+ return formatHealthCheck(result);
6243
6658
  }
6244
6659
  });
6245
6660
  var index_metrics = tool({
@@ -6278,13 +6693,7 @@ var index_logs = tool({
6278
6693
  } else {
6279
6694
  logs = logger.getLogs(args.limit);
6280
6695
  }
6281
- if (logs.length === 0) {
6282
- return "No logs recorded yet. Logs are captured during indexing and search operations.";
6283
- }
6284
- return logs.map((l) => {
6285
- const dataStr = l.data ? ` ${JSON.stringify(l.data)}` : "";
6286
- return `[${l.timestamp}] [${l.level.toUpperCase()}] [${l.category}] ${l.message}${dataStr}`;
6287
- }).join("\n");
6696
+ return formatLogs(logs);
6288
6697
  }
6289
6698
  });
6290
6699
  var find_similar = tool({
@@ -6299,7 +6708,7 @@ var find_similar = tool({
6299
6708
  },
6300
6709
  async execute(args) {
6301
6710
  const indexer = getIndexer();
6302
- const results = await indexer.findSimilar(args.code, args.limit ?? 10, {
6711
+ const results = await indexer.findSimilar(args.code, args.limit, {
6303
6712
  fileType: args.fileType,
6304
6713
  directory: args.directory,
6305
6714
  chunkType: args.chunkType,
@@ -6308,109 +6717,37 @@ var find_similar = tool({
6308
6717
  if (results.length === 0) {
6309
6718
  return "No similar code found. Try a different snippet or run index_codebase first.";
6310
6719
  }
6311
- const formatted = results.map((r, idx) => {
6312
- 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}`;
6313
- return `${header} (similarity: ${(r.score * 100).toFixed(1)}%)
6314
- \`\`\`
6315
- ${r.content}
6316
- \`\`\``;
6317
- });
6318
6720
  return `Found ${results.length} similar code blocks:
6319
6721
 
6320
- ${formatted.join("\n\n")}`;
6722
+ ${formatSearchResults(results)}`;
6321
6723
  }
6322
6724
  });
6323
- function formatIndexStats(stats, verbose = false) {
6324
- const lines = [];
6325
- if (stats.indexedChunks === 0 && stats.removedChunks === 0) {
6326
- lines.push(`Indexed. ${stats.totalFiles} files processed, ${stats.existingChunks} code chunks already up to date.`);
6327
- } else if (stats.indexedChunks === 0) {
6328
- lines.push(`Indexed. ${stats.totalFiles} files, removed ${stats.removedChunks} stale chunks, ${stats.existingChunks} chunks remain.`);
6329
- } else {
6330
- let main = `Indexed. ${stats.totalFiles} files processed, ${stats.indexedChunks} new chunks embedded.`;
6331
- if (stats.existingChunks > 0) {
6332
- main += ` ${stats.existingChunks} unchanged chunks skipped.`;
6333
- }
6334
- lines.push(main);
6335
- if (stats.removedChunks > 0) {
6336
- lines.push(`Removed ${stats.removedChunks} stale chunks.`);
6337
- }
6338
- if (stats.failedChunks > 0) {
6339
- lines.push(`Failed: ${stats.failedChunks} chunks.`);
6340
- }
6341
- lines.push(`Tokens: ${stats.tokensUsed.toLocaleString()}, Duration: ${(stats.durationMs / 1e3).toFixed(1)}s`);
6342
- }
6343
- if (verbose) {
6344
- if (stats.skippedFiles.length > 0) {
6345
- const tooLarge = stats.skippedFiles.filter((f) => f.reason === "too_large");
6346
- const excluded = stats.skippedFiles.filter((f) => f.reason === "excluded");
6347
- const gitignored = stats.skippedFiles.filter((f) => f.reason === "gitignore");
6348
- lines.push("");
6349
- lines.push(`Skipped files: ${stats.skippedFiles.length}`);
6350
- if (tooLarge.length > 0) {
6351
- lines.push(` Too large (${tooLarge.length}): ${tooLarge.slice(0, 5).map((f) => f.path).join(", ")}${tooLarge.length > 5 ? "..." : ""}`);
6352
- }
6353
- if (excluded.length > 0) {
6354
- lines.push(` Excluded (${excluded.length}): ${excluded.slice(0, 5).map((f) => f.path).join(", ")}${excluded.length > 5 ? "..." : ""}`);
6355
- }
6356
- if (gitignored.length > 0) {
6357
- lines.push(` Gitignored (${gitignored.length}): ${gitignored.slice(0, 5).map((f) => f.path).join(", ")}${gitignored.length > 5 ? "..." : ""}`);
6358
- }
6359
- }
6360
- if (stats.parseFailures.length > 0) {
6361
- lines.push("");
6362
- lines.push(`Files with no extractable chunks (${stats.parseFailures.length}): ${stats.parseFailures.slice(0, 10).join(", ")}${stats.parseFailures.length > 10 ? "..." : ""}`);
6725
+ var codebase_search = tool({
6726
+ 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.",
6727
+ args: {
6728
+ query: z.string().describe("Natural language description of what code you're looking for. Describe behavior, not syntax."),
6729
+ limit: z.number().optional().default(5).describe("Maximum number of results to return"),
6730
+ fileType: z.string().optional().describe("Filter by file extension (e.g., 'ts', 'py', 'rs')"),
6731
+ directory: z.string().optional().describe("Filter by directory path (e.g., 'src/utils', 'lib')"),
6732
+ chunkType: z.enum(["function", "class", "method", "interface", "type", "enum", "struct", "impl", "trait", "module", "other"]).optional().describe("Filter by code chunk type"),
6733
+ contextLines: z.number().optional().describe("Number of extra lines to include before/after each match (default: 0)")
6734
+ },
6735
+ async execute(args) {
6736
+ const indexer = getIndexer();
6737
+ const results = await indexer.search(args.query, args.limit ?? 5, {
6738
+ fileType: args.fileType,
6739
+ directory: args.directory,
6740
+ chunkType: args.chunkType,
6741
+ contextLines: args.contextLines
6742
+ });
6743
+ if (results.length === 0) {
6744
+ return "No matching code found. Try a different query or run index_codebase first.";
6363
6745
  }
6746
+ return `Found ${results.length} results for "${args.query}":
6747
+
6748
+ ${formatSearchResults(results, "score")}`;
6364
6749
  }
6365
- return lines.join("\n");
6366
- }
6367
- function formatStatus(status) {
6368
- if (!status.indexed) {
6369
- return "Codebase is not indexed. Run index_codebase to create an index.";
6370
- }
6371
- const lines = [
6372
- `Index status:`,
6373
- ` Indexed chunks: ${status.vectorCount.toLocaleString()}`,
6374
- ` Provider: ${status.provider}`,
6375
- ` Model: ${status.model}`,
6376
- ` Location: ${status.indexPath}`
6377
- ];
6378
- if (status.currentBranch !== "default") {
6379
- lines.push(` Current branch: ${status.currentBranch}`);
6380
- lines.push(` Base branch: ${status.baseBranch}`);
6381
- }
6382
- return lines.join("\n");
6383
- }
6384
- function formatProgressTitle(progress) {
6385
- switch (progress.phase) {
6386
- case "scanning":
6387
- return "Scanning files...";
6388
- case "parsing":
6389
- return `Parsing: ${progress.filesProcessed}/${progress.totalFiles} files`;
6390
- case "embedding":
6391
- return `Embedding: ${progress.chunksProcessed}/${progress.totalChunks} chunks`;
6392
- case "storing":
6393
- return "Storing index...";
6394
- case "complete":
6395
- return "Indexing complete";
6396
- default:
6397
- return "Indexing...";
6398
- }
6399
- }
6400
- function calculatePercentage(progress) {
6401
- if (progress.phase === "scanning") return 0;
6402
- if (progress.phase === "complete") return 100;
6403
- if (progress.phase === "parsing") {
6404
- if (progress.totalFiles === 0) return 5;
6405
- return Math.round(5 + progress.filesProcessed / progress.totalFiles * 15);
6406
- }
6407
- if (progress.phase === "embedding") {
6408
- if (progress.totalChunks === 0) return 20;
6409
- return Math.round(20 + progress.chunksProcessed / progress.totalChunks * 70);
6410
- }
6411
- if (progress.phase === "storing") return 95;
6412
- return 0;
6413
- }
6750
+ });
6414
6751
 
6415
6752
  // src/commands/loader.ts
6416
6753
  import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
@@ -6489,14 +6826,20 @@ var plugin = async ({ directory }) => {
6489
6826
  const config = parseConfig(rawConfig);
6490
6827
  initializeTools(projectRoot, config);
6491
6828
  const indexer = new Indexer(projectRoot, config);
6492
- if (config.indexing.autoIndex) {
6829
+ const isValidProject = !config.indexing.requireProjectMarker || hasProjectMarker(projectRoot);
6830
+ if (!isValidProject) {
6831
+ console.warn(
6832
+ `[codebase-index] Skipping file watching and auto-indexing: no project marker found in "${projectRoot}". Set "indexing.requireProjectMarker": false in config to override.`
6833
+ );
6834
+ }
6835
+ if (config.indexing.autoIndex && isValidProject) {
6493
6836
  indexer.initialize().then(() => {
6494
6837
  indexer.index().catch(() => {
6495
6838
  });
6496
6839
  }).catch(() => {
6497
6840
  });
6498
6841
  }
6499
- if (config.indexing.watchFiles) {
6842
+ if (config.indexing.watchFiles && isValidProject) {
6500
6843
  createWatcherWithIndexer(indexer, projectRoot, config);
6501
6844
  }
6502
6845
  return {