@javierkaiser/opencode-indexing 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +101 -1
- package/README.md +167 -6
- package/index.ts +78 -4
- package/install.sh +0 -0
- package/package.json +1 -1
- package/src/config.ts +31 -5
- package/src/embedder.ts +339 -99
- package/src/indexer.ts +12 -1
- package/src/lancedb-store.ts +2 -3
- package/src/lancedb-support.ts +83 -0
- package/src/registry.ts +105 -19
- package/src/rpc.ts +1 -2
- package/src/search.ts +41 -2
- package/src/settings-commands.ts +6 -4
- package/src/settings.ts +28 -13
- package/src/status.ts +53 -5
- package/src/types.ts +41 -0
- package/tui/settings-page-data.ts +7 -8
- package/tui.ts +50 -9
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,104 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.0] - 2026-10-14
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`gemini`, `voyage` and `openrouter` are implemented.** They were selectable in
|
|
15
|
+
the provider list, passed every validation gate, and then threw
|
|
16
|
+
`Unsupported embedder provider` once indexing had already started and been billed
|
|
17
|
+
for a probe. A test asserted that they were unimplemented, which pinned the gap in
|
|
18
|
+
place.
|
|
19
|
+
- **The model catalog is visible.** The TUI model field is now a chooser built from
|
|
20
|
+
the catalog, with a *Custom* entry for anything not listed and for providers with
|
|
21
|
+
no catalog at all. `indexing_config` lists every model with its dimension and
|
|
22
|
+
whether Kilo also knows it — which is the whole answer to whether a free import
|
|
23
|
+
will work.
|
|
24
|
+
- **Per-model input ceilings and per-provider batch limits.** The ceilings differ by
|
|
25
|
+
more than an order of magnitude (`mxbai-embed-large` takes 512 tokens,
|
|
26
|
+
`voyage-code-4` takes 32,000), and one global constant truncated every chunk past
|
|
27
|
+
the smallest model's limit. Batch limits are now per provider: OpenAI caps a
|
|
28
|
+
request at 300,000 tokens whatever the item count, Gemini accepts a fixed number of
|
|
29
|
+
items, Voyage has both.
|
|
30
|
+
- `voyage-code-4`, `voyage-4`, `voyage-4-large`, `voyage-4-lite`,
|
|
31
|
+
`gemini-embedding-2` and Ollama's `bge-m3` in the catalog, each marked as not
|
|
32
|
+
present in Kilo's registry so it is clear they cannot be imported.
|
|
33
|
+
- `indexing_status` warns before a model change re-embeds the workspace, with the
|
|
34
|
+
point count, rather than after the embeddings have been paid for.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- **A search that cannot read part of the corpus now fails instead of shrinking.**
|
|
39
|
+
An embedding failure per profile was pushed onto a list rendered as "Warnings", and
|
|
40
|
+
the results that came back looked exactly like a complete set whose missing half
|
|
41
|
+
ranked lower. An index holding points whose profile cannot be embedded is now an
|
|
42
|
+
error naming the profile. An index with no points is not an error, so the common
|
|
43
|
+
case is unaffected.
|
|
44
|
+
- Changing provider clears the model, because model ids are namespaced per provider.
|
|
45
|
+
The previous code claimed to do this and did not, so switching to Voyage produced
|
|
46
|
+
`voyage` + `codestral-embed-2505`, which outranks the provider default.
|
|
47
|
+
- The stored dimension of a previous index is no longer inherited across a provider
|
|
48
|
+
change. A 1536-wide Mistral index made any new model look 1536-dimensional, and the
|
|
49
|
+
inherited number was then persisted as though it had been measured, making the
|
|
50
|
+
mismatch self-consistent and invisible on every later run.
|
|
51
|
+
- The `apiKeys` merge is derived from the incoming keys instead of a hand-written list
|
|
52
|
+
of provider names, which silently dropped any credential added to the shape but not
|
|
53
|
+
to the list.
|
|
54
|
+
- The settings page reports which providers have credentials instead of deriving it
|
|
55
|
+
from two booleans, which reported "none configured" for a Gemini, Voyage,
|
|
56
|
+
OpenRouter, Ollama or OpenAI-compatible setup.
|
|
57
|
+
- `indexing_config` and `settings test kilo` pass a target profile to the Kilo source
|
|
58
|
+
scan. Without it every source was reported as compatible, including ones that would
|
|
59
|
+
be refused a moment later.
|
|
60
|
+
- Ollama and Voyage are sent `truncate` / `truncation: false`, so an over-long chunk
|
|
61
|
+
fails instead of being silently shortened into a valid-looking wrong vector.
|
|
62
|
+
- Embedding results are placed by `data[].index` where the provider supplies it.
|
|
63
|
+
OpenAI documents the field rather than promising response order, and its own samples
|
|
64
|
+
fetch by it; a repeated or out-of-range index is now an error.
|
|
65
|
+
- The catalog's `openrouter` Gemini entry names `google/gemini-embedding-2-preview`,
|
|
66
|
+
which is what Kilo writes. It said `google/gemini-embedding-001`, and since the
|
|
67
|
+
profile comparison is exact string equality the drift was invisible and made every
|
|
68
|
+
such index un-importable.
|
|
69
|
+
|
|
70
|
+
### Fixed
|
|
71
|
+
|
|
72
|
+
- The model field is cleared on a provider change in the TUI as well as the chat
|
|
73
|
+
command; both paths had the same stale-model bug.
|
|
74
|
+
- `searchCode` and `getStatus` had no unit tests — only the e2e and cli scripts
|
|
75
|
+
exercised them, and those assert nothing. Both have tests now.
|
|
76
|
+
|
|
77
|
+
### Documentation
|
|
78
|
+
|
|
79
|
+
- The README states how far each provider has actually been tested. `mistral` has
|
|
80
|
+
touched its real API; `openrouter` reached the API and authenticated but was
|
|
81
|
+
refused by the key's own spend limit; `openai`, `gemini`, `voyage`,
|
|
82
|
+
`openai-compatible` and `ollama` have never contacted a real endpoint and are
|
|
83
|
+
covered only by fixtures written alongside their adapters. The same fact is
|
|
84
|
+
surfaced in the model chooser and in `indexing_config`, and a test pins the list so
|
|
85
|
+
the claim cannot quietly widen.
|
|
86
|
+
|
|
87
|
+
## [0.1.1] - 2026-10-14
|
|
88
|
+
|
|
89
|
+
### Added
|
|
90
|
+
|
|
91
|
+
- `src/lancedb-support.ts` reports up front that LanceDB ships one native binary per
|
|
92
|
+
platform and that there is no `lancedb-darwin-x64` build (there is no WASM
|
|
93
|
+
fallback), so the embedded store is unavailable on macOS on Intel — instead of
|
|
94
|
+
failing later with a module-not-found error.
|
|
95
|
+
|
|
96
|
+
### Changed
|
|
97
|
+
|
|
98
|
+
- **The own index now defaults to embedded LanceDB** instead of inheriting Kilo's
|
|
99
|
+
`vectorStore`. Kilo's setting is read as `kiloVectorStore`: it still decides where
|
|
100
|
+
an *import* reads from, and it is still reported, but it no longer decides where
|
|
101
|
+
the own index writes to. Dropping that inheritance is reported as a warning rather
|
|
102
|
+
than applied silently, so an existing Qdrant own index is not discovered to be
|
|
103
|
+
empty.
|
|
104
|
+
- npm releases are now automated: pushing a `v*` tag triggers a GitHub Actions
|
|
105
|
+
workflow that publishes through npm trusted publishing (OIDC), with no
|
|
106
|
+
long-lived token.
|
|
107
|
+
|
|
10
108
|
## [0.1.0] - 2026-10-09
|
|
11
109
|
|
|
12
110
|
First public release.
|
|
@@ -29,5 +127,7 @@ First public release.
|
|
|
29
127
|
- Cross-platform installer (`install.mjs`, plus `install.sh` / `install.ps1`
|
|
30
128
|
wrappers).
|
|
31
129
|
|
|
32
|
-
[Unreleased]: https://github.com/Javierkaiser/opencode-indexing/compare/v0.
|
|
130
|
+
[Unreleased]: https://github.com/Javierkaiser/opencode-indexing/compare/v0.2.0...HEAD
|
|
131
|
+
[0.2.0]: https://github.com/Javierkaiser/opencode-indexing/releases/tag/v0.2.0
|
|
132
|
+
[0.1.1]: https://github.com/Javierkaiser/opencode-indexing/releases/tag/v0.1.1
|
|
33
133
|
[0.1.0]: https://github.com/Javierkaiser/opencode-indexing/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](./LICENSE)
|
|
5
5
|
[](https://nodejs.org)
|
|
6
6
|
|
|
7
|
-
OpenCode plugin that adds **semantic code search** by reusing the code index created by [Kilo Code](https://kilo.ai) and maintaining its **own independent index** (
|
|
7
|
+
OpenCode plugin that adds **semantic code search** by reusing the code index created by [Kilo Code](https://kilo.ai) and maintaining its **own independent index** (embedded LanceDB by default, or Qdrant if you prefer), so search keeps working even when Kilo isn't installed, is disabled, or its index is stale.
|
|
8
8
|
|
|
9
9
|
## TL;DR
|
|
10
10
|
|
|
@@ -69,7 +69,7 @@ If no package manager is available the copy still happens and the plugin runs wi
|
|
|
69
69
|
|
|
70
70
|
```
|
|
71
71
|
OpenCode ──► indexing_search ──┬──► Kilo's index (read-only) Qdrant ws-* · Kilo LanceDB
|
|
72
|
-
└──► own index (read/write)
|
|
72
|
+
└──► own index (read/write) own LanceDB (default) · Qdrant oc-*
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
- **Read-only reuse**: the plugin computes the same collection name Kilo uses (`ws-` + sha256 of the workspace path, first 16 hex chars), validates the embedding profile stored in the metadata point, and queries with an embedding from the *same* model (e.g. Mistral `codestral-embed-2505`, 1536d).
|
|
@@ -176,12 +176,126 @@ Configurable from all three:
|
|
|
176
176
|
- **Vector store** for the own index: Qdrant or LanceDB.
|
|
177
177
|
- **Qdrant URL + API key** (local or remote) with connection test.
|
|
178
178
|
- **LanceDB directory** with availability test.
|
|
179
|
-
- **Embedding provider/model** (mistral, openai, ollama, openai-compatible, gemini, voyage, openrouter) and **API key**.
|
|
179
|
+
- **Embedding provider/model** (mistral, openai, ollama, openai-compatible, gemini, voyage, openrouter) and **API key**. The model is chosen from a catalog rather than typed blind. The model is chosen from a catalog rather than typed blind.
|
|
180
180
|
- **Import from Kilo** toggle with live source detection ("Qdrant ws-… (31,261 pts) · LanceDB …").
|
|
181
181
|
- **Auto-refresh on search** toggle.
|
|
182
182
|
- Build / refresh / import actions and a status summary.
|
|
183
183
|
|
|
184
|
-
|
|
184
|
+
### Embedding providers
|
|
185
|
+
|
|
186
|
+
All seven have an adapter. Every model below is in the catalog, which is what
|
|
187
|
+
supplies the dimension, the score threshold and the input ceiling; a model that
|
|
188
|
+
is not listed still works, and its dimension is probed from the provider on
|
|
189
|
+
first use. See the next section for how far each one has actually been tested.
|
|
190
|
+
|
|
191
|
+
| Provider | Default model | Also in the catalog | Input ceiling |
|
|
192
|
+
| --- | --- | --- | --- |
|
|
193
|
+
| `mistral` | `codestral-embed-2505` | `codestral-embed`, `mistral-embed` | 8,192 |
|
|
194
|
+
| `openai` | `text-embedding-3-small` | `text-embedding-3-large`, `text-embedding-ada-002` | 8,192 |
|
|
195
|
+
| `voyage` | `voyage-code-3` | **`voyage-code-4`**, `voyage-4`, `voyage-4-large`, `voyage-4-lite`, `voyage-3-lite` | **32,000** |
|
|
196
|
+
| `gemini` | `gemini-embedding-001` | **`gemini-embedding-2`**, `text-embedding-004`, `embedding-001` | 2,048 (001) / 8,192 (2) |
|
|
197
|
+
| `openrouter` | `openai/text-embedding-3-small` | `openai/text-embedding-3-large`, `google/gemini-embedding-2-preview` | 8,192 |
|
|
198
|
+
| `ollama` | `nomic-embed-text` | `mxbai-embed-large`, `all-minilm`, `bge-m3` | 512 – 8,192 |
|
|
199
|
+
| `openai-compatible` | none | whatever your endpoint serves | probed |
|
|
200
|
+
|
|
201
|
+
#### Getting an API key (Mistral)
|
|
202
|
+
|
|
203
|
+
Mistral is the default provider and the only one tested end to end here, so it is
|
|
204
|
+
the shortest path to a working index. API access is enabled by default on a free
|
|
205
|
+
account — **no credit card** — with a usage limit that is enough to index a
|
|
206
|
+
repository.
|
|
207
|
+
|
|
208
|
+
1. Create an account at <https://console.mistral.ai> if you do not have one.
|
|
209
|
+
2. Open the **Studio** console and click **API Keys** in the left sidebar.
|
|
210
|
+
3. Click **Create new key**: give it a name, set an expiration date, and choose the
|
|
211
|
+
connector access scope (*Shared connectors only* is enough for an API key).
|
|
212
|
+
4. Click **Create new key** and **copy the key right away** — it is shown once and
|
|
213
|
+
cannot be retrieved later.
|
|
214
|
+
|
|
215
|
+
Then hand it to the plugin. The simplest way keeps the key out of any file:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
export KILO_INDEX_MISTRAL_KEY=... # Linux / macOS
|
|
219
|
+
# PowerShell: $env:KILO_INDEX_MISTRAL_KEY = "..."
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Equivalently, set it from **`/indexing-config`** in any client or the `/indexing`
|
|
223
|
+
page in the terminal (the field is masked), or through the plugin's options in
|
|
224
|
+
`opencode.json` — see
|
|
225
|
+
[Configuration](#configuration-optional-opencodejson). Settings resolve in the
|
|
226
|
+
order *plugin options → `indexing.json` → environment → defaults*, so an
|
|
227
|
+
environment variable is overridden by anything set in the file or in the options.
|
|
228
|
+
|
|
229
|
+
To check the key before indexing:
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
curl -s https://api.mistral.ai/v1/models \
|
|
233
|
+
-H "Authorization: Bearer $KILO_INDEX_MISTRAL_KEY" | head -c 200
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
A list of models means it works; a `401` means the key is wrong or the header is
|
|
237
|
+
missing.
|
|
238
|
+
|
|
239
|
+
#### How far each provider has actually been tested
|
|
240
|
+
|
|
241
|
+
Worth stating plainly, because "implemented" and "seen working" are not the
|
|
242
|
+
same claim and only one of them is safe to rely on.
|
|
243
|
+
|
|
244
|
+
| Provider | Against the real API | What that means |
|
|
245
|
+
| --- | --- | --- |
|
|
246
|
+
| `mistral` | **Yes** | Two inputs, 1536 dimensions, distinct vectors, ~680 ms |
|
|
247
|
+
| `openrouter` | **Partly** | The request reached the API and authenticated; it was refused by the key's own $0 spend limit, so the response parser is unverified |
|
|
248
|
+
| `ollama` | **No** | The local server was not running during the check |
|
|
249
|
+
| `openai`, `gemini`, `voyage`, `openai-compatible` | **No** | No credentials available; only mock-based tests |
|
|
250
|
+
|
|
251
|
+
The adapters for the last four were written from each provider's documentation
|
|
252
|
+
and tested against fixtures written alongside them, which means the tests encode
|
|
253
|
+
the same reading of the docs as the code. A wrong assumption about a field name
|
|
254
|
+
or a response shape would pass every test and fail on first use.
|
|
255
|
+
|
|
256
|
+
Three specific things this leaves open, in descending order of how quietly they
|
|
257
|
+
would fail:
|
|
258
|
+
|
|
259
|
+
- **Gemini's aggregation.** `gemini-embedding-2` returns a single aggregated
|
|
260
|
+
vector for a plain list of inputs, where `gemini-embedding-001` returns one per
|
|
261
|
+
input. The adapter wraps each input in its own `Content` to make both behave
|
|
262
|
+
the same way, and a test asserts the count — but the test is against a fixture,
|
|
263
|
+
not the model. If the wrapping is wrong, N inputs silently become one vector.
|
|
264
|
+
- **Voyage's response shape.** It is treated as OpenAI-shaped
|
|
265
|
+
(`data[].embedding` with an `index`). This matches its published API reference
|
|
266
|
+
and was not confirmed against the service.
|
|
267
|
+
- **Voyage and Ollama truncation.** Both are sent `truncation`/`truncate: false`
|
|
268
|
+
so that an over-long chunk fails instead of being silently shortened. If a
|
|
269
|
+
provider rejects that field rather than honouring it, indexing stops with an
|
|
270
|
+
error rather than degrading — which is the intended direction to fail, but it
|
|
271
|
+
is a guess about the field being accepted.
|
|
272
|
+
|
|
273
|
+
If you use one of these providers and it fails, the error message names the
|
|
274
|
+
provider and the endpoint; please report it, since this is the part of the
|
|
275
|
+
plugin most likely to be wrong.
|
|
276
|
+
|
|
277
|
+
Two things worth knowing before switching:
|
|
278
|
+
|
|
279
|
+
- **Model ids are namespaced per provider.** Changing provider clears the model
|
|
280
|
+
so the new provider's default applies. A leftover id like
|
|
281
|
+
`codestral-embed-2505` under `provider: "voyage"` would be sent to Voyage and
|
|
282
|
+
rejected.
|
|
283
|
+
- **Changing the model invalidates the own index.** A different model means a
|
|
284
|
+
different vector space, so the next build deletes the existing points and
|
|
285
|
+
re-embeds the whole workspace. `indexing_status` warns with the point count
|
|
286
|
+
*before* that happens. Re-embedding a repository is cheap — well under a
|
|
287
|
+
couple of dollars at any paid provider — but a large one takes a while.
|
|
288
|
+
|
|
289
|
+
**Which models can import from Kilo.** Kilo's registry is mirrored entry for
|
|
290
|
+
entry, because an import is accepted only when provider, model id and dimension
|
|
291
|
+
match exactly. Models marked *ours* in the table above are not in Kilo's
|
|
292
|
+
registry and can never be imported; `indexing_config` says which is which. If
|
|
293
|
+
your Kilo index uses a different model, set the same provider and model here and
|
|
294
|
+
the import costs nothing.
|
|
295
|
+
|
|
296
|
+
Settings are stored in `~/.config/opencode/indexing.json` (plugin options → `indexing.json` → environment → defaults). Plugin options in `opencode.json` still work and take precedence. API keys are never written to `indexing.json` in plain text; set them through the environment or plugin options.
|
|
297
|
+
|
|
298
|
+
`~/.config/kilo/kilo.jsonc` is still read, and still supplies the embedding provider, model, dimensions and API keys — and it still says where *Kilo* keeps its index. It no longer decides where this plugin's own index is written; that stopped being coupled so that the default could actually take effect for people running Kilo. See [Where each index lives](#where-each-index-lives).
|
|
185
299
|
|
|
186
300
|
## Configuration (optional, `opencode.json`)
|
|
187
301
|
|
|
@@ -200,7 +314,7 @@ Settings are stored in `~/.config/opencode/indexing.json` (plugin options → `i
|
|
|
200
314
|
|
|
201
315
|
| Variable | Meaning |
|
|
202
316
|
| --- | --- |
|
|
203
|
-
| `KILO_INDEX_VECTOR_STORE` | `
|
|
317
|
+
| `KILO_INDEX_VECTOR_STORE` | `lancedb` (default) or `qdrant` for the own index |
|
|
204
318
|
| `KILO_INDEX_QDRANT_URL` / `KILO_INDEX_QDRANT_API_KEY` | Qdrant connection |
|
|
205
319
|
| `KILO_INDEX_LANCEDB_DIR` | Base directory for the own LanceDB stores |
|
|
206
320
|
| `KILO_INDEX_KILO_LANCEDB_DIR` | Override Kilo's LanceDB directory (import source) |
|
|
@@ -211,9 +325,56 @@ Word about data safety: Kilo's indexes (`ws-*` collections and its LanceDB direc
|
|
|
211
325
|
## Requirements
|
|
212
326
|
|
|
213
327
|
- OpenCode v2 (plugin API `@opencode/plugin@2.x`).
|
|
214
|
-
- For
|
|
328
|
+
- **For the own index: nothing to run.** The default store is embedded LanceDB
|
|
329
|
+
(`@lancedb/lancedb@0.26.2`), so there is no server to start, no port to free and
|
|
330
|
+
no container to keep alive.
|
|
331
|
+
- **A Qdrant server is only needed if you ask for it**, either by setting
|
|
332
|
+
`vectorStore=qdrant` or because a Kilo index you want to import lives in Qdrant.
|
|
333
|
+
In that case the default URL is `http://localhost:6333`.
|
|
215
334
|
- Embedding credentials compatible with the collection you search (the profile is read from the metadata point).
|
|
216
335
|
|
|
336
|
+
### Where each index lives
|
|
337
|
+
|
|
338
|
+
The own index and Kilo's index are independent, and each one is chosen on its
|
|
339
|
+
own:
|
|
340
|
+
|
|
341
|
+
| Index | Default | Configured by |
|
|
342
|
+
| --- | --- | --- |
|
|
343
|
+
| Own index | embedded LanceDB, under `~/.local/state/opencode-indexing/lancedb` | `vectorStore` |
|
|
344
|
+
| Kilo index (read-only) | whatever `kilo.jsonc` says | Kilo itself |
|
|
345
|
+
|
|
346
|
+
Kilo's `vectorStore` used to decide where this plugin's own index was written,
|
|
347
|
+
because both were read from the same setting chain. That is no longer the case:
|
|
348
|
+
Kilo decides where an import **reads from**, and your own index decides where it
|
|
349
|
+
**writes to**. If Kilo is on Qdrant and you have not set `vectorStore` yourself,
|
|
350
|
+
the own index still goes to LanceDB and `indexing_status` says so.
|
|
351
|
+
|
|
352
|
+
### Platform support for the embedded store
|
|
353
|
+
|
|
354
|
+
`@lancedb/lancedb` is a JavaScript wrapper around a native library, with one
|
|
355
|
+
prebuilt binary per platform and no WebAssembly fallback. The platforms it ships
|
|
356
|
+
a binary for are:
|
|
357
|
+
|
|
358
|
+
| Platform | `platform-arch` | Own index on LanceDB |
|
|
359
|
+
| --- | --- | --- |
|
|
360
|
+
| Windows x64 / arm64 | `win32-x64`, `win32-arm64` | yes |
|
|
361
|
+
| Linux x64 / arm64 (glibc and musl) | `linux-x64`, `linux-arm64` | yes |
|
|
362
|
+
| macOS Apple Silicon | `darwin-arm64` | yes |
|
|
363
|
+
| **macOS Intel** | `darwin-x64` | **no binary is published** |
|
|
364
|
+
|
|
365
|
+
On macOS Intel the default store cannot load, and installing it again will not
|
|
366
|
+
help. The plugin says so explicitly and asks you to pick Qdrant:
|
|
367
|
+
|
|
368
|
+
```
|
|
369
|
+
LanceDB is the default store for the own index but has no native binary for
|
|
370
|
+
darwin-x64. It cannot run on this platform and reinstalling will not change
|
|
371
|
+
that. Set vectorStore=qdrant explicitly in
|
|
372
|
+
~/.config/opencode/indexing.json to use a Qdrant server instead.
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Set `vectorStore=qdrant` in `~/.config/opencode/indexing.json` (or run
|
|
376
|
+
`/indexing-config set vectorstore=qdrant`) to use a Qdrant server instead.
|
|
377
|
+
|
|
217
378
|
## Semantic search vs. code graphs (roadmap)
|
|
218
379
|
|
|
219
380
|
`opencode-indexing` implements **semantic (vector) search**. It is worth being
|
package/index.ts
CHANGED
|
@@ -32,7 +32,8 @@ import {
|
|
|
32
32
|
parseIndexingCommand,
|
|
33
33
|
settingReport,
|
|
34
34
|
} from "./src/settings-commands.ts"
|
|
35
|
-
import
|
|
35
|
+
import { getModelDimension, isKiloMirrored, isLiveTested, listModelIds } from "./src/registry.ts"
|
|
36
|
+
import type { EmbeddingProfile, IndexingSettings, KVStore, ProviderCredentials } from "./src/types.ts"
|
|
36
37
|
import type { Qdrant } from "./src/qdrant.ts"
|
|
37
38
|
import { describeError } from "./src/errors.ts"
|
|
38
39
|
|
|
@@ -114,13 +115,76 @@ function settingsView(settings: IndexingSettings, homeDir?: string): Record<stri
|
|
|
114
115
|
autoRefresh: settings.autoRefresh,
|
|
115
116
|
enabled: settings.enabled,
|
|
116
117
|
searchMaxResults: settings.searchMaxResults,
|
|
117
|
-
|
|
118
|
-
|
|
118
|
+
// Which providers actually have a credential, as a list rather than one
|
|
119
|
+
// boolean per provider. The booleans only ever covered Mistral and OpenAI,
|
|
120
|
+
// so a settings page with a Gemini key configured reported "none
|
|
121
|
+
// configured" - and every provider added afterwards would have needed
|
|
122
|
+
// another field threaded through the RPC, the schema and the page.
|
|
123
|
+
configuredProviders: configuredProviders(settings.credentials),
|
|
119
124
|
settingsFile: settingsFilePath(homeDir),
|
|
120
125
|
warnings: settings.warnings,
|
|
121
126
|
}
|
|
122
127
|
}
|
|
123
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Providers with a usable credential.
|
|
131
|
+
*
|
|
132
|
+
* A base URL counts for the two providers that authenticate with one, since a
|
|
133
|
+
* configured Ollama or OpenAI-compatible server needs no key.
|
|
134
|
+
*/
|
|
135
|
+
function configuredProviders(credentials: ProviderCredentials): string[] {
|
|
136
|
+
const configured: string[] = []
|
|
137
|
+
if (credentials.mistralApiKey) configured.push("mistral")
|
|
138
|
+
if (credentials.openAiApiKey) configured.push("openai")
|
|
139
|
+
if (credentials.geminiApiKey) configured.push("gemini")
|
|
140
|
+
if (credentials.voyageApiKey) configured.push("voyage")
|
|
141
|
+
if (credentials.openRouterApiKey) configured.push("openrouter")
|
|
142
|
+
if (credentials.ollamaBaseUrl) configured.push("ollama")
|
|
143
|
+
if (credentials.openAiCompatibleBaseUrl) configured.push("openai-compatible")
|
|
144
|
+
return configured
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The profile the settings describe.
|
|
149
|
+
*
|
|
150
|
+
* Only the identity of the model is needed to compare against a Kilo index;
|
|
151
|
+
* the dimension is part of the comparison, so it has to come from somewhere.
|
|
152
|
+
* `settings.dimension` is 0 for a model the catalog does not know, and a
|
|
153
|
+
* comparison against a zero dimension would report every source as
|
|
154
|
+
* incompatible, which is as wrong as reporting every one as compatible.
|
|
155
|
+
*/
|
|
156
|
+
function profileFor(settings: IndexingSettings): EmbeddingProfile | undefined {
|
|
157
|
+
const dimension = settings.dimension || getModelDimension(settings.provider, settings.modelId) || 0
|
|
158
|
+
if (!dimension) return undefined
|
|
159
|
+
return { provider: settings.provider, modelId: settings.modelId, dimension }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The models this provider offers, and which of them can import from Kilo.
|
|
164
|
+
*
|
|
165
|
+
* The catalog was only ever used to look up a dimension, so it was invisible:
|
|
166
|
+
* a model id had to be known in advance to be used. Which entries Kilo also
|
|
167
|
+
* knows is the one thing that decides whether an import will be accepted, so
|
|
168
|
+
* it is stated rather than left to be discovered by a refused build.
|
|
169
|
+
*/
|
|
170
|
+
function describeAvailableModels(settings: IndexingSettings): string[] {
|
|
171
|
+
const models = listModelIds(settings.provider)
|
|
172
|
+
const note = isLiveTested(settings.provider)
|
|
173
|
+
? []
|
|
174
|
+
: [
|
|
175
|
+
`- **Note:** the ${settings.provider} adapter has not been exercised against the real API; it is built from the provider's documentation. See "How far each provider has actually been tested" in the README.`,
|
|
176
|
+
]
|
|
177
|
+
if (models.length === 0) {
|
|
178
|
+
return [...note, `- **Models:** this provider has no catalog; set \`model\` to whatever your endpoint serves`]
|
|
179
|
+
}
|
|
180
|
+
const render = (modelId: string) => {
|
|
181
|
+
const dimension = getModelDimension(settings.provider, modelId)
|
|
182
|
+
const importer = isKiloMirrored(settings.provider, modelId) === false ? " — not importable from Kilo" : ""
|
|
183
|
+
return `\`${modelId}\`${dimension ? ` (${dimension}d)` : ""}${importer}`
|
|
184
|
+
}
|
|
185
|
+
return [...note, `- **Models for ${settings.provider}:** ${models.map(render).join(" · ")}`]
|
|
186
|
+
}
|
|
187
|
+
|
|
124
188
|
function summarizeReport(report: {
|
|
125
189
|
durationMs: number
|
|
126
190
|
chunksUpserted: number
|
|
@@ -315,7 +379,11 @@ Behavior:
|
|
|
315
379
|
return { ok: true, message: `${settings.provider}/${settings.modelId} OK (${vector?.length ?? 0} dimensions)` }
|
|
316
380
|
}
|
|
317
381
|
if (target === "kilo") {
|
|
318
|
-
const sources = await discoverKiloSources(root, {
|
|
382
|
+
const sources = await discoverKiloSources(root, {
|
|
383
|
+
qdrant: createSettingsQdrant(settings),
|
|
384
|
+
homeDir: undefined,
|
|
385
|
+
targetProfile: profileFor(settings),
|
|
386
|
+
})
|
|
319
387
|
const list = sources.map((s) => `${s.kind} ${s.name} (${s.pointsCount ?? "?"} pts, ${s.compatible ? "compatible" : "INCOMPATIBLE"})`)
|
|
320
388
|
return {
|
|
321
389
|
ok: sources.length > 0,
|
|
@@ -462,6 +530,7 @@ class ForgetRefusedError extends Error {}
|
|
|
462
530
|
"",
|
|
463
531
|
`- **Vector store (own index):** ${settings.vectorStore}${settings.vectorStore === "qdrant" ? ` — ${settings.qdrantUrl}` : ` — ${settings.lancedbDirectory}`}`,
|
|
464
532
|
`- **Embedding model:** ${settings.provider}/${settings.modelId}${settings.dimension ? ` (${settings.dimension}d)` : ""}`,
|
|
533
|
+
...describeAvailableModels(settings),
|
|
465
534
|
`- **Import from Kilo:** ${settings.importFromKilo ? "ON (build imports existing Kilo indexes without embedding cost)" : "OFF (always index from scratch)"}`,
|
|
466
535
|
`- **Auto-refresh on search:** ${settings.autoRefresh ? "ON" : "OFF"}`,
|
|
467
536
|
`- **Indexing:** ${settings.enabled === false ? "PAUSED (nothing is written or refreshed; search still reads Kilo's index)" : "ON"}`,
|
|
@@ -482,6 +551,11 @@ class ForgetRefusedError extends Error {}
|
|
|
482
551
|
const sources = await discoverKiloSources(root, {
|
|
483
552
|
qdrant: createSettingsQdrant(settings),
|
|
484
553
|
homeDir: undefined,
|
|
554
|
+
// Without this every source reported "compatible", including ones whose
|
|
555
|
+
// profile differs: the check returns true when it has no target to
|
|
556
|
+
// compare against, so the report claimed an import would work when it
|
|
557
|
+
// would be refused a moment later.
|
|
558
|
+
targetProfile: profileFor(settings),
|
|
485
559
|
}).catch(() => [])
|
|
486
560
|
lines.push(
|
|
487
561
|
"",
|
package/install.sh
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@javierkaiser/opencode-indexing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "OpenCode plugin: semantic code search that reuses Kilo Code indexes (Qdrant/LanceDB) and maintains its own independent index",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Carlos Javier Lust <javierkaiser@gmail.com>",
|
package/src/config.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as os from "node:os"
|
|
2
2
|
|
|
3
3
|
import type { EmbedderProvider, IndexingSettings, ProviderCredentials } from "./types.ts"
|
|
4
|
-
import { defaultLanceDbDirectory, readSettingsFile, type SettingsFile } from "./settings.ts"
|
|
4
|
+
import { defaultLanceDbDirectory, readSettingsFile, settingsFilePath, type SettingsFile } from "./settings.ts"
|
|
5
|
+
import { lanceDbSupport } from "./lancedb-support.ts"
|
|
5
6
|
import { stripJsonComments } from "./jsonc.ts"
|
|
6
7
|
import {
|
|
7
8
|
DEFAULT_EMBEDDING_BATCH_SIZE,
|
|
@@ -62,7 +63,7 @@ interface KiloConfigIndexing {
|
|
|
62
63
|
openrouter?: { apiKey?: string }
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
const VALID_PROVIDERS: readonly EmbedderProvider[] = [
|
|
66
|
+
export const VALID_PROVIDERS: readonly EmbedderProvider[] = [
|
|
66
67
|
"mistral",
|
|
67
68
|
"openai",
|
|
68
69
|
"ollama",
|
|
@@ -184,9 +185,33 @@ export function resolveSettings(options: PluginOptionsInput): IndexingSettings {
|
|
|
184
185
|
nonEmpty(kilo?.openrouter?.apiKey),
|
|
185
186
|
}
|
|
186
187
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
188
|
+
// Kilo decides where an import reads FROM; it does not decide where the own
|
|
189
|
+
// index writes TO. Those were coupled through this one chain, and that is
|
|
190
|
+
// what made "LanceDB by default" unreachable for exactly the people most
|
|
191
|
+
// likely to have Kilo installed: kilo.jsonc's vectorStore sat above the
|
|
192
|
+
// fallback, so a Kilo-on-Qdrant setup silently kept pinning the own index to
|
|
193
|
+
// Qdrant. The coupling is reported below rather than dropped in silence.
|
|
194
|
+
const kiloVectorStore = pickVectorStore(kilo?.vectorStore)
|
|
195
|
+
const ownVectorStore = pickVectorStore(
|
|
196
|
+
options.vectorStore,
|
|
197
|
+
file?.vectorStore,
|
|
198
|
+
process.env.KILO_INDEX_VECTOR_STORE,
|
|
199
|
+
)
|
|
200
|
+
if (kiloVectorStore && !ownVectorStore && kiloVectorStore !== "lancedb") {
|
|
201
|
+
warnings.push(
|
|
202
|
+
`Kilo indexes with ${kiloVectorStore}; your own index no longer inherits that. ` +
|
|
203
|
+
`Set vectorStore explicitly in ${settingsFilePath(options.homeDir)} to change it.`,
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
const vectorStore = ownVectorStore ?? "lancedb"
|
|
207
|
+
|
|
208
|
+
if (vectorStore === "lancedb" && !lanceDbSupport().supported) {
|
|
209
|
+
const { id } = lanceDbSupport()
|
|
210
|
+
warnings.push(
|
|
211
|
+
`LanceDB is the default store but has no native binary for ${id}. ` +
|
|
212
|
+
`Indexing will fail until you set vectorStore=qdrant in ${settingsFilePath(options.homeDir)}.`,
|
|
213
|
+
)
|
|
214
|
+
}
|
|
190
215
|
|
|
191
216
|
const qdrantUrl =
|
|
192
217
|
nonEmpty(options.qdrantUrl) ??
|
|
@@ -264,6 +289,7 @@ export function resolveSettings(options: PluginOptionsInput): IndexingSettings {
|
|
|
264
289
|
modelId,
|
|
265
290
|
dimension: dimension ?? 0,
|
|
266
291
|
vectorStore,
|
|
292
|
+
kiloVectorStore: kiloVectorStore ?? null,
|
|
267
293
|
qdrantUrl,
|
|
268
294
|
qdrantApiKey,
|
|
269
295
|
lancedbDirectory,
|