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 +38 -3
- package/dist/cli.cjs +161 -7
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +161 -7
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +160 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +160 -6
- package/dist/index.js.map +1 -1
- package/native/codebase-index-native.linux-x64-gnu.node +0 -0
- package/native/codebase-index-native.win32-x64-msvc.node +0 -0
- package/package.json +1 -1
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
|
-
|
|
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 === "
|
|
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
|
|
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.
|
|
4674
|
+
version: "0.5.1"
|
|
4521
4675
|
});
|
|
4522
4676
|
const indexer = new Indexer(projectRoot, config);
|
|
4523
4677
|
let initialized = false;
|