opencode-codebase-index 0.5.0 → 0.5.1

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/README.md CHANGED
@@ -16,8 +16,8 @@
16
16
  - ⚡ **Blazing Fast Indexing**: Powered by a Rust native module using `tree-sitter` and `usearch`. Incremental updates take milliseconds.
17
17
  - 🌿 **Branch-Aware**: Seamlessly handles git branch switches — reuses embeddings, filters stale results.
18
18
  - 🔒 **Privacy Focused**: Your vector index is stored locally in your project.
19
- 🔌 **Model Agnostic**: Works out-of-the-box with GitHub Copilot, OpenAI, Gemini, or local Ollama models.
20
- 🌐 **MCP Server**: Use with Cursor, Claude Code, Windsurf, or any MCP-compatible client — index once, search from anywhere.
19
+ - 🔌 **Model Agnostic**: Works out-of-the-box with GitHub Copilot, OpenAI, Gemini, or local Ollama models.
20
+ - 🌐 **MCP Server**: Use with Cursor, Claude Code, Windsurf, or any MCP-compatible client — index once, search from anywhere.
21
21
 
22
22
  ## ⚡ Quick Start
23
23
 
@@ -315,7 +315,7 @@ Zero-config by default (uses `auto` mode). Customize in `.opencode/codebase-inde
315
315
 
316
316
  | Option | Default | Description |
317
317
  |--------|---------|-------------|
318
- | `embeddingProvider` | `"auto"` | Which AI to use: `auto`, `github-copilot`, `openai`, `google`, `ollama` |
318
+ | `embeddingProvider` | `"auto"` | Which AI to use: `auto`, `github-copilot`, `openai`, `google`, `ollama`, `custom` |
319
319
  | `scope` | `"project"` | `project` = index per repo, `global` = shared index across repos |
320
320
  | **indexing** | | |
321
321
  | `autoIndex` | `false` | Automatically index on plugin load |
@@ -351,6 +351,8 @@ The plugin automatically detects available credentials in this order:
351
351
  3. **Google** (Gemini Embeddings)
352
352
  4. **Ollama** (Local/Private - requires `nomic-embed-text`)
353
353
 
354
+ You can also use **Custom** to connect any OpenAI-compatible embedding endpoint (llama.cpp, vLLM, text-embeddings-inference, LiteLLM, etc.).
355
+
354
356
  ### Rate Limits by Provider
355
357
 
356
358
  Each provider has different rate limits. The plugin automatically adjusts concurrency and delays:
@@ -361,6 +363,7 @@ Each provider has different rate limits. The plugin automatically adjusts concur
361
363
  | **OpenAI** | 3 | 500ms | Medium codebases |
362
364
  | **Google** | 5 | 200ms | Medium-large codebases |
363
365
  | **Ollama** | 5 | None | Large codebases (10k+ files) |
366
+ | **Custom** | 3 | 1s | Any OpenAI-compatible endpoint |
364
367
 
365
368
  **For large codebases**, use Ollama locally to avoid rate limits:
366
369
 
@@ -460,6 +463,7 @@ Use this decision tree to pick the right embedding provider:
460
463
  | **GitHub Copilot** | Slow (rate limited) | Free* | Cloud | Small codebases, existing subscribers |
461
464
  | **OpenAI** | Medium | ~$0.0001/1K tokens | Cloud | General use |
462
465
  | **Google** | Fast | Free tier available | Cloud | Medium-large codebases |
466
+ | **Custom** | Varies | Varies | Varies | Self-hosted or third-party endpoints |
463
467
 
464
468
  *Requires active Copilot subscription
465
469
 
@@ -495,6 +499,23 @@ No setup needed if you have an active Copilot subscription.
495
499
  { "embeddingProvider": "github-copilot" }
496
500
  ```
497
501
 
502
+ **Custom (OpenAI-compatible)**
503
+ Works with any server that implements the OpenAI `/v1/embeddings` API format (llama.cpp, vLLM, text-embeddings-inference, LiteLLM, etc.).
504
+ ```json
505
+ {
506
+ "embeddingProvider": "custom",
507
+ "customProvider": {
508
+ "baseUrl": "http://localhost:11434/v1",
509
+ "model": "nomic-embed-text",
510
+ "dimensions": 768,
511
+ "apiKey": "optional-api-key",
512
+ "maxTokens": 8192,
513
+ "timeoutMs": 30000
514
+ }
515
+ }
516
+ ```
517
+ Required fields: `baseUrl`, `model`, `dimensions` (positive integer). Optional: `apiKey`, `maxTokens`, `timeoutMs` (default: 30000).
518
+
498
519
  ## ⚠️ Tradeoffs
499
520
 
500
521
  Be aware of these characteristics:
@@ -570,6 +591,20 @@ The Rust native module handles performance-critical operations:
570
591
 
571
592
  Rebuild with: `npm run build:native` (requires Rust toolchain)
572
593
 
594
+ ### Platform Support
595
+
596
+ Pre-built native binaries are published for:
597
+
598
+ | Platform | Architecture | SIMD Acceleration |
599
+ |----------|-------------|--------------------|
600
+ | macOS | x86_64 | ✅ simsimd |
601
+ | macOS | ARM64 (Apple Silicon) | ✅ simsimd |
602
+ | Linux | x86_64 (GNU) | ✅ simsimd |
603
+ | Linux | ARM64 (GNU) | ✅ simsimd |
604
+ | Windows | x86_64 (MSVC) | ❌ scalar fallback |
605
+
606
+ Windows builds use scalar distance functions instead of SIMD — functionally identical, marginally slower for very large indexes. This is due to MSVC lacking support for certain AVX-512 intrinsics used by simsimd.
607
+
573
608
  ## License
574
609
 
575
610
  MIT
package/dist/cli.cjs CHANGED
@@ -848,7 +848,32 @@ function parseConfig(raw) {
848
848
  };
849
849
  let embeddingProvider;
850
850
  let embeddingModel = void 0;
851
- if (isValidProvider(input.embeddingProvider)) {
851
+ let customProvider = void 0;
852
+ if (input.embeddingProvider === "custom") {
853
+ embeddingProvider = "custom";
854
+ const rawCustom = input.customProvider && typeof input.customProvider === "object" ? input.customProvider : null;
855
+ if (rawCustom && typeof rawCustom.baseUrl === "string" && rawCustom.baseUrl.trim().length > 0 && typeof rawCustom.model === "string" && rawCustom.model.trim().length > 0 && typeof rawCustom.dimensions === "number" && Number.isInteger(rawCustom.dimensions) && rawCustom.dimensions > 0) {
856
+ customProvider = {
857
+ baseUrl: rawCustom.baseUrl.trim().replace(/\/+$/, ""),
858
+ model: rawCustom.model,
859
+ dimensions: rawCustom.dimensions,
860
+ apiKey: typeof rawCustom.apiKey === "string" ? rawCustom.apiKey : void 0,
861
+ maxTokens: typeof rawCustom.maxTokens === "number" ? rawCustom.maxTokens : void 0,
862
+ timeoutMs: typeof rawCustom.timeoutMs === "number" ? Math.max(1e3, rawCustom.timeoutMs) : void 0,
863
+ concurrency: typeof rawCustom.concurrency === "number" ? Math.max(1, Math.floor(rawCustom.concurrency)) : void 0,
864
+ requestIntervalMs: typeof rawCustom.requestIntervalMs === "number" ? Math.max(0, Math.floor(rawCustom.requestIntervalMs)) : void 0
865
+ };
866
+ if (!/\/v\d+\/?$/.test(customProvider.baseUrl)) {
867
+ console.warn(
868
+ `[codebase-index] Warning: customProvider.baseUrl ("${customProvider.baseUrl}") does not end with an API version path like /v1. The plugin appends /embeddings automatically, so the full URL will be "${customProvider.baseUrl}/embeddings". If your provider expects /v1/embeddings, set baseUrl to "${customProvider.baseUrl}/v1".`
869
+ );
870
+ }
871
+ } else {
872
+ throw new Error(
873
+ "embeddingProvider is 'custom' but customProvider config is missing or invalid. Required fields: baseUrl (string), model (string), dimensions (positive integer)."
874
+ );
875
+ }
876
+ } else if (isValidProvider(input.embeddingProvider)) {
852
877
  embeddingProvider = input.embeddingProvider;
853
878
  if (input.embeddingModel) {
854
879
  embeddingModel = isValidModel(input.embeddingModel, embeddingProvider) ? input.embeddingModel : DEFAULT_PROVIDER_MODELS[embeddingProvider];
@@ -859,6 +884,7 @@ function parseConfig(raw) {
859
884
  return {
860
885
  embeddingProvider,
861
886
  embeddingModel,
887
+ customProvider,
862
888
  scope: isValidScope(input.scope) ? input.scope : "project",
863
889
  include: isStringArray(input.include) ? input.include : DEFAULT_INCLUDE,
864
890
  exclude: isStringArray(input.exclude) ? input.exclude : DEFAULT_EXCLUDE,
@@ -2028,12 +2054,39 @@ function getProviderDisplayName(provider) {
2028
2054
  return "Google (Gemini)";
2029
2055
  case "ollama":
2030
2056
  return "Ollama (Local)";
2057
+ case "custom":
2058
+ return "Custom (OpenAI-compatible)";
2031
2059
  default:
2032
2060
  return provider;
2033
2061
  }
2034
2062
  }
2063
+ function createCustomProviderInfo(config) {
2064
+ const baseUrl = config.baseUrl.replace(/\/+$/, "");
2065
+ return {
2066
+ provider: "custom",
2067
+ credentials: {
2068
+ provider: "custom",
2069
+ baseUrl,
2070
+ apiKey: config.apiKey
2071
+ },
2072
+ modelInfo: {
2073
+ provider: "custom",
2074
+ model: config.model,
2075
+ dimensions: config.dimensions,
2076
+ maxTokens: config.maxTokens ?? 8192,
2077
+ costPer1MTokens: 0,
2078
+ timeoutMs: config.timeoutMs ?? 3e4
2079
+ }
2080
+ };
2081
+ }
2035
2082
 
2036
2083
  // src/embeddings/provider.ts
2084
+ var CustomProviderNonRetryableError = class extends Error {
2085
+ constructor(message) {
2086
+ super(message);
2087
+ this.name = "CustomProviderNonRetryableError";
2088
+ }
2089
+ };
2037
2090
  function createEmbeddingProvider(configuredProviderInfo) {
2038
2091
  switch (configuredProviderInfo.provider) {
2039
2092
  case "github-copilot":
@@ -2044,6 +2097,8 @@ function createEmbeddingProvider(configuredProviderInfo) {
2044
2097
  return new GoogleEmbeddingProvider(configuredProviderInfo.credentials, configuredProviderInfo.modelInfo);
2045
2098
  case "ollama":
2046
2099
  return new OllamaEmbeddingProvider(configuredProviderInfo.credentials, configuredProviderInfo.modelInfo);
2100
+ case "custom":
2101
+ return new CustomEmbeddingProvider(configuredProviderInfo.credentials, configuredProviderInfo.modelInfo);
2047
2102
  default: {
2048
2103
  const _exhaustive = configuredProviderInfo;
2049
2104
  throw new Error(`Unsupported embedding provider: ${_exhaustive.provider}`);
@@ -2278,6 +2333,90 @@ var OllamaEmbeddingProvider = class {
2278
2333
  return this.modelInfo;
2279
2334
  }
2280
2335
  };
2336
+ var CustomEmbeddingProvider = class {
2337
+ constructor(credentials, modelInfo) {
2338
+ this.credentials = credentials;
2339
+ this.modelInfo = modelInfo;
2340
+ }
2341
+ async embedQuery(query) {
2342
+ const result = await this.embedBatch([query]);
2343
+ return {
2344
+ embedding: result.embeddings[0],
2345
+ tokensUsed: result.totalTokensUsed
2346
+ };
2347
+ }
2348
+ async embedDocument(document) {
2349
+ const result = await this.embedBatch([document]);
2350
+ return {
2351
+ embedding: result.embeddings[0],
2352
+ tokensUsed: result.totalTokensUsed
2353
+ };
2354
+ }
2355
+ async embedBatch(texts) {
2356
+ const headers = {
2357
+ "Content-Type": "application/json"
2358
+ };
2359
+ if (this.credentials.apiKey) {
2360
+ headers["Authorization"] = `Bearer ${this.credentials.apiKey}`;
2361
+ }
2362
+ const baseUrl = this.credentials.baseUrl ?? "";
2363
+ const timeoutMs = this.modelInfo.timeoutMs;
2364
+ const controller = new AbortController();
2365
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
2366
+ let response;
2367
+ try {
2368
+ response = await fetch(`${baseUrl}/embeddings`, {
2369
+ method: "POST",
2370
+ headers,
2371
+ body: JSON.stringify({
2372
+ model: this.modelInfo.model,
2373
+ input: texts
2374
+ }),
2375
+ signal: controller.signal
2376
+ });
2377
+ } catch (error) {
2378
+ if (error instanceof Error && error.name === "AbortError") {
2379
+ throw new Error(`Custom embedding API request timed out after ${timeoutMs}ms for ${baseUrl}/embeddings`);
2380
+ }
2381
+ throw error;
2382
+ } finally {
2383
+ clearTimeout(timeout);
2384
+ }
2385
+ if (!response.ok) {
2386
+ const errorText = await response.text();
2387
+ if (response.status >= 400 && response.status < 500 && response.status !== 429) {
2388
+ throw new CustomProviderNonRetryableError(`Custom embedding API error (non-retryable): ${response.status} - ${errorText}`);
2389
+ }
2390
+ throw new Error(`Custom embedding API error: ${response.status} - ${errorText}`);
2391
+ }
2392
+ const data = await response.json();
2393
+ if (data.data && Array.isArray(data.data)) {
2394
+ if (data.data.length > 0) {
2395
+ const actualDims = data.data[0].embedding.length;
2396
+ if (actualDims !== this.modelInfo.dimensions) {
2397
+ throw new Error(
2398
+ `Dimension mismatch: customProvider.dimensions is ${this.modelInfo.dimensions}, but the API returned vectors with ${actualDims} dimensions. Update your config to match the model's actual output dimensions.`
2399
+ );
2400
+ }
2401
+ }
2402
+ if (data.data.length !== texts.length) {
2403
+ throw new Error(
2404
+ `Embedding count mismatch: sent ${texts.length} texts but received ${data.data.length} embeddings. The custom embedding server may not support batch input.`
2405
+ );
2406
+ }
2407
+ return {
2408
+ embeddings: data.data.map((d) => d.embedding),
2409
+ // Rough estimate: ~4 chars per token. Used as fallback when the server
2410
+ // doesn't return usage.total_tokens (e.g. llama.cpp, some vLLM configs).
2411
+ totalTokensUsed: data.usage?.total_tokens ?? texts.reduce((sum, t) => sum + Math.ceil(t.length / 4), 0)
2412
+ };
2413
+ }
2414
+ throw new Error("Custom embedding API returned unexpected response format. Expected OpenAI-compatible format with data[].embedding.");
2415
+ }
2416
+ getModelInfo() {
2417
+ return this.modelInfo;
2418
+ }
2419
+ };
2281
2420
 
2282
2421
  // src/utils/files.ts
2283
2422
  var import_ignore = __toESM(require_ignore(), 1);
@@ -3415,19 +3554,33 @@ var Indexer = class {
3415
3554
  return { concurrency: 5, intervalMs: 200, minRetryMs: 1e3, maxRetryMs: 3e4 };
3416
3555
  case "ollama":
3417
3556
  return { concurrency: 5, intervalMs: 0, minRetryMs: 500, maxRetryMs: 5e3 };
3557
+ case "custom": {
3558
+ const customConfig = this.config.customProvider;
3559
+ return {
3560
+ concurrency: customConfig?.concurrency ?? 3,
3561
+ intervalMs: customConfig?.requestIntervalMs ?? 1e3,
3562
+ minRetryMs: 1e3,
3563
+ maxRetryMs: 3e4
3564
+ };
3565
+ }
3418
3566
  default:
3419
3567
  return { concurrency: 3, intervalMs: 1e3, minRetryMs: 1e3, maxRetryMs: 3e4 };
3420
3568
  }
3421
3569
  }
3422
3570
  async initialize() {
3423
- if (this.config.embeddingProvider === "auto") {
3571
+ if (this.config.embeddingProvider === "custom") {
3572
+ if (!this.config.customProvider) {
3573
+ throw new Error("embeddingProvider is 'custom' but customProvider config is missing.");
3574
+ }
3575
+ this.configuredProviderInfo = createCustomProviderInfo(this.config.customProvider);
3576
+ } else if (this.config.embeddingProvider === "auto") {
3424
3577
  this.configuredProviderInfo = await tryDetectProvider();
3425
3578
  } else {
3426
3579
  this.configuredProviderInfo = await detectEmbeddingProvider(this.config.embeddingProvider, this.config.embeddingModel);
3427
3580
  }
3428
3581
  if (!this.configuredProviderInfo) {
3429
3582
  throw new Error(
3430
- "No embedding provider available. Configure GitHub, OpenAI, Google, or Ollama."
3583
+ "No embedding provider available. Configure GitHub Copilot, OpenAI, Google, Ollama, or a custom OpenAI-compatible endpoint."
3431
3584
  );
3432
3585
  }
3433
3586
  this.logger.info("Initializing indexer", {
@@ -3437,9 +3590,6 @@ var Indexer = class {
3437
3590
  });
3438
3591
  this.provider = createEmbeddingProvider(this.configuredProviderInfo);
3439
3592
  await import_fs4.promises.mkdir(this.indexPath, { recursive: true });
3440
- if (this.checkForInterruptedIndexing()) {
3441
- await this.recoverFromInterruptedIndexing();
3442
- }
3443
3593
  const dimensions = this.configuredProviderInfo.modelInfo.dimensions;
3444
3594
  const storePath = path5.join(this.indexPath, "vectors");
3445
3595
  this.store = new VectorStore(storePath, dimensions);
@@ -3460,6 +3610,9 @@ var Indexer = class {
3460
3610
  const dbPath = path5.join(this.indexPath, "codebase.db");
3461
3611
  const dbIsNew = !(0, import_fs4.existsSync)(dbPath);
3462
3612
  this.database = new Database(dbPath);
3613
+ if (this.checkForInterruptedIndexing()) {
3614
+ await this.recoverFromInterruptedIndexing();
3615
+ }
3463
3616
  if (dbIsNew && this.store.count() > 0) {
3464
3617
  this.migrateFromLegacyIndex();
3465
3618
  }
@@ -3880,6 +4033,7 @@ var Indexer = class {
3880
4033
  minTimeout: Math.max(this.config.indexing.retryDelayMs, providerRateLimits.minRetryMs),
3881
4034
  maxTimeout: providerRateLimits.maxRetryMs,
3882
4035
  factor: 2,
4036
+ shouldRetry: (error) => !(error.error instanceof CustomProviderNonRetryableError),
3883
4037
  onFailedAttempt: (error) => {
3884
4038
  const message = getErrorMessage(error);
3885
4039
  if (isRateLimitError(error)) {
@@ -4517,7 +4671,7 @@ var CHUNK_TYPE_ENUM = [
4517
4671
  function createMcpServer(projectRoot, config) {
4518
4672
  const server = new import_mcp.McpServer({
4519
4673
  name: "opencode-codebase-index",
4520
- version: "0.5.0"
4674
+ version: "0.5.1"
4521
4675
  });
4522
4676
  const indexer = new Indexer(projectRoot, config);
4523
4677
  let initialized = false;