Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .commandcode/taste/taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Taste (Continuously Learned by [CommandCode][cmd])

[cmd]: https://commandcode.ai/

31 changes: 30 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ MVP implemented. All core modules are built and tested:
- Retrieval pipeline (vector + hybrid keyword/vector)
- CLI (index, query, clear, status, list, show, dump)
- OpenCode plugin (chat.message hook + auto-context injection + background auto-indexing + read-override)
- Test suite (511 tests, 0 failures)
- TUI settings menu (model selection for embedding/description providers)
- Runtime overrides system (`runtime-overrides.json`) for live config changes
- API key auto-resolution from OpenCode provider config
- Manifest schema versioning with corruption detection
- Test suite (589 tests, 1 integration test requiring opencode binary)

Design docs: `ReadMe.md` (project docs), `PLANNING.md` (roadmap + brainstorming),
`docs/designs/2026-05-28-rag-plugin-mvp-design.md` (architecture design).
Expand Down Expand Up @@ -318,6 +322,30 @@ OpenCode config. Instead, rely on `.opencode/plugins/*.js` auto-discovery:
- Re-export syntax (`export { X as default } from ...`) produces the same result
but is harder to inspect with DevTools or stack traces.

### Runtime overrides (`runtime-overrides.json`)
- The TUI settings menu writes to `${storePath}/runtime-overrides.json`. The plugin and `createRagHooks()` periodically reload these overrides (TTL: 5s) via `loadRuntimeOverrides()` + `applyRuntimeOverrides()`.
- Override values take precedence over `opencode-rag.json` config values. Supported overrides: retrieval settings (`topK`, `minScore`, `maxChunks`), description settings (`enabled`, `provider`, `model`, `baseUrl`), and embedding settings (`provider`, `model`, `baseUrl`).
- `saveRuntimeOverride()` in `src/core/runtime-overrides.ts` supports `boolean`, `number`, and `string` values.
- The TUI prompt for numeric settings (`maxChunks`) and boolean toggles all persist to both `runtime-overrides.json` AND `opencode-rag.json` for consistency.

### TUI Settings Menu
- The TUI plugin (`src/tui.ts`) registers a settings panel accessed from the OpenCode sidebar.
- Categories: Retrieval, Embedding, LLM Descriptions.
- Embedding and Description settings include a **model picker** dropdown populated from OpenCode's registered providers (reads `api.state.provider`). Models are grouped by provider name, sorted alphabetically, with a "Custom…" option for manual entry.
- Selecting a model auto-sets the corresponding provider (`ollama`/`openai`) and base URL (derived from the OpenCode provider config).
- The TUI also provides a prompt-based editor for string/number settings and toggle switches for booleans.

### Manifest schema versioning
- `src/core/manifest.ts` now includes `SCHEMA_VERSION = 1` and a `schemaVersion` field in `FileManifest`.
- `loadManifest()` checks `parsed.schemaVersion === SCHEMA_VERSION`. If the version doesn't match, it returns `status: "corrupt"`, triggering a full index rebuild.
- `createEmptyManifest()` and `saveManifest()` always set `schemaVersion = SCHEMA_VERSION`.
- This prevents data corruption issues when the manifest format changes between versions.

### API key resolution from OpenCode provider config
- `resolveApiKeyFromProviderConfig()` in `src/plugin.ts` reads OpenCode config files (`.opencode/opencode.json`, `opencode.json`, `~/.config/opencode/opencode.jsonc`) to find an `apiKey` for the `openai` provider.
- If the embedding or description provider is `"openai"` but no `apiKey` is set in `opencode-rag.json`, the plugin auto-resolves it from the OpenCode config.
- Config files may contain JSONC comments — they are stripped before parsing.

## Adding a New Language Chunker

1. Create `src/chunker/<lang>.ts` extending `TreeSitterChunker`
Expand Down Expand Up @@ -360,6 +388,7 @@ both semantic meaning and code-level similarity.
- On LLM failure, falls back to embedding raw content and logs a warning
- Set `description.enabled: false` in config to disable and embed raw code instead
- Config is in `src/core/config.ts` (`DescriptionConfig`), provider in `src/describer/`
- Chunk descriptions now include relative path and line ranges (e.g. `src/foo.ts, lines 10-42`) even when LLM description is disabled, improving context

## OpenCodeRAG Plugin

Expand Down
26 changes: 20 additions & 6 deletions PLANNING.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
- [x] Line-based fallback chunking for unsupported formats
- [x] Pluggable chunkers via `Chunker` interface and config-loaded custom chunkers (`loadChunkersFromConfig()`)
- [x] Incremental indexing (file-hash-based, manifest-backed, diff-aware)
- [x] File watching and background re-indexing with debounced, serialized passes
- [x] File watching and background re-indexing with debounced, serialized passes, watcher status file
- [x] Enhanced chunk descriptions with relative paths and line numbers in both LLM and non-LLM modes

### Embedding & Storage

Expand All @@ -21,6 +22,8 @@
- [x] Pluggable storage via `VectorStore` interface
- [x] Pluggable embedders via `EmbeddingProvider` interface
- [x] Batch embedding (configurable batch size)
- [x] Auto-detection of LanceDB schema (`tableHasDescriptionColumn()`) for seamless upgrades
- [x] Robust `clear()` via `dropDatabase()`

### Retrieval

Expand All @@ -35,24 +38,29 @@
- [x] `opencode-rag-context` tool for chunk-level retrieval
- [x] `chat.message` hook with file suggestions and auto-injection
- [x] RAG-backed read override tool — shadows OpenCode's built-in read, appends related code chunks and suggests related files when retrieval finds relevant results
- [x] TUI plugin module (OpenTUI + Solid.js sidebar panel)
- [x] TUI plugin module (OpenTUI + Solid.js sidebar panel) with model picker dropdowns for embedding/description providers
- [x] `PluginModule` export pattern for OpenCode v1.17.0 compatibility
- [x] Background auto-indexing via `createBackgroundIndexer()`
- [x] Background auto-indexing via `createBackgroundIndexer()` with watcher status file
- [x] API key auto-resolution from OpenCode provider config files

### CLI & Distribution

- [x] CLI (`init`, `index`, `query`, `clear`, `status` via commander)
- [x] CLI (`init`, `index`, `query`, `clear`, `status`, `list`, `show`, `dump` via commander)
- [x] Full `init` command lifecycle: generates `.opencode/plugins/rag-plugin.js` + `rag-tui.js`, `.gitignore`, `package.json`; runs `npm install`; cleans stale global plugin registrations; `--skip-install` flag
- [x] Install scripts (`install.ps1` / `install.sh`) — build, pack, install to `~/.opencode/`, register in `opencode.jsonc`, CLI wrapper, full uninstall mode
- [x] Release automation script (`scripts/release-patch.js` with `--dry` support)
- [x] Multi-entry package exports: plugin, server, library, TUI
- [x] Published npm package: `opencode-rag-plugin`
- [x] CLI query results deduplication
- [x] `clear` command uses `store.dropDatabase()` for clean slate

### Config & Quality

- [x] JSON config with deep-merged partial overrides
- [x] Runtime overrides system (`runtime-overrides.json`) for live TUI config changes with 5s TTL
- [x] Configurable file logging
- [x] Expanded automated test suite (511+ tests, Node built-in runner)
- [x] Manifest schema versioning with corruption detection and automatic rebuild
- [x] Expanded automated test suite (589+ tests, Node built-in runner)

## Short Term

Expand Down Expand Up @@ -94,7 +102,8 @@ and safely rebuilds if manifest is missing or corrupt.

Watch mode (`index --watch`) uses chokidar for debounced incremental passes.
Passes are serialized. The plugin uses the same scheduling for background
auto-indexing inside OpenCode.
auto-indexing inside OpenCode, now writing `watcher-status.json` to the store
path for observability of background indexing state.

## 2. 🧠 Query Enhancement

Expand Down Expand Up @@ -218,6 +227,10 @@ Key strengths:
- Broad source and document coverage without native grammar build tools
- RAG-backed read tool that enriches file reads with related code chunks
- Hybrid keyword + vector search with configurable fusion weights
- TUI settings menu with model picker for embedding and description providers
- Runtime overrides system for live config changes without editing JSON files
- API key auto-resolution from OpenCode provider config
- Manifest schema versioning with auto-rebuild on format changes
- Install scripts for one-command global setup and uninstall

Key next steps:
Expand All @@ -227,3 +240,4 @@ Key next steps:
3. Context window optimization for better prompt packing
4. Query rewriting and retrieval explainability
5. Persistent session memory across coding sessions
6. Web UI for index inspection and search result browsing
26 changes: 21 additions & 5 deletions ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,28 @@ Running `opencode-rag init` creates the config file `opencode-rag.json` in your
| `openCode.autoInject.minScore` | `0.75` | Minimum relevance score to inject actual code (0–1). |
| `retrieval.topK` | `10` | Default number of chunks fetched per query. |
| `retrieval.hybridSearch.enabled` | `true` | Enables combined TF×IDF + vector search. |
### TUI Settings Menu

### Description-Based Embedding (Optional)
When running inside OpenCode, the plugin provides a settings panel accessible from the OpenCode sidebar:
- **Retrieval**: `topK`, `minScore`, `maxChunks`
- **Embedding**: Model picker dropdown (populated from OpenCode's registered providers) with custom manual entry
- **LLM Descriptions**: Enable/disable toggle, model picker dropdown

When enabled, the indexer uses an LLM to generate natural-language descriptions of code chunks, then combines the description with the raw code for embedding. This captures both semantic meaning (from the description) and code-level similarity (from the code itself), dramatically improving search quality for natural language and code-based queries alike.
Settings are persisted to `${storePath}/runtime-overrides.json` and take precedence over `opencode-rag.json`. The plugin reloads these on a 5-second TTL.

As this needs more processing power, it is recommended to keep this disabled if you don't use a dedicated GPU for inference.
### API Key Auto-Resolution

If you set `embedding.provider` or `description.provider` to `"openai"` but don't specify an `apiKey` in `opencode-rag.json`, the plugin automatically resolves the key from OpenCode's own provider configuration (`.opencode/opencode.json`, `opencode.json`, or `~/.config/opencode/opencode.jsonc`).

### Manifest Schema Versioning

The manifest file now contains a `schemaVersion` field. If the stored manifest has a mismatched version, a full index rebuild is triggered automatically — this prevents silent corruption when the manifest format changes between plugin versions.

### Description-Based Embedding (Enabled by Default)

The indexer uses an LLM to generate natural-language descriptions of code chunks, then combines the description with the raw code for embedding. This captures both semantic meaning (from the description) and code-level similarity (from the code itself), dramatically improving search quality for natural language and code-based queries alike.

> As this needs more processing power, it is recommended to disable it (`description.enabled: false`) if you don't have a dedicated GPU for inference or want to reduce latency during indexing.

```json
{
Expand All @@ -125,7 +141,7 @@ As this needs more processing power, it is recommended to keep this disabled if
"baseUrl": "http://localhost:11434/api",
"model": "qwen2.5:3b",
"timeoutMs": 60000,
"systemPrompt": "You are a code analysis assistant. Given a code snippet, write a short (2-3 sentence) description of what the code does, its purpose, and key functionality. Focus on semantic meaning that would help someone searching for this code. Do not include code in your response."
"systemPrompt": "Describe code for semantic search in short simple words, simple grammar, no code, no comments."
}
}
```
Expand All @@ -138,7 +154,7 @@ As this needs more processing power, it is recommended to keep this disabled if
| `description.systemPrompt` | *(see above)* | Customizable system prompt for the LLM. |
| `description.timeoutMs` | `60000` | Timeout per LLM call. |

The embedded text is formed as `description + "\n\n" + code content`. The description and code are still stored as separate fields in LanceDB. Keyword search continues to use the raw code content. Set `description.enabled` to `false` to disable and embed raw code content instead. If the LLM call fails during indexing, the chunk falls back to embedding raw content with a warning logged.
The embedded text is formed as `description + "\n\n" + code content`. The description and code are still stored as separate fields in LanceDB. Keyword search continues to use the raw code content. Even when LLM descriptions are disabled, chunk descriptions still include the file path and line range (e.g. `src/foo.ts, lines 10-42`). If the LLM call fails during indexing, the chunk falls back to embedding raw content with a warning logged.

<details>
<summary>View Logging Configuration</summary>
Expand Down
15 changes: 8 additions & 7 deletions opencode-rag.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"embedding": {
"provider": "ollama",
"baseUrl": "http://127.0.0.1:11434/api",
"model": "embeddinggemma",
"provider": "nvidia",
"baseUrl": "https://integrate.api.nvidia.com/v1",
"model": "nvidia/llama-nemotron-embed-vl-1b-v2",
"timeoutMs": 60000,
"documentPrefix": "",
"queryPrefix": ""
Expand Down Expand Up @@ -91,11 +91,12 @@
},
"description": {
"enabled": true,
"provider": "ollama",
"baseUrl": "http://127.0.0.1:11434/api",
"model": "qwen2.5:3b",
"provider": "openai",
"baseUrl": "https://api.openai.com/v1",
"model": "north-mini-code-free",
"timeoutMs": 60000,
"systemPrompt": "Describe code for semantic search in short simple words, simple grammar, no code, no comments. If user message contains multiple chunks labeled === CHUNK N ===, describe each one separately, starting each with CHUNK N: followed by the description. For a single chunk, output description directly."
"systemPrompt": "Describe code for semantic search in short simple words, simple grammar, no code, no comments. If user message contains multiple chunks labeled === CHUNK N ===, describe each one separately, starting each with CHUNK N: followed by the description. For a single chunk, output description directly.",
"apiKey": "public"
},
"logging": {
"level": "debug",
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "opencode-rag-plugin",
"version": "1.4.2",
"version": "1.5.1",
"description": "OpenCode plugin for local-first RAG-based semantic code search",
"type": "module",
"main": "./dist/plugin-entry.js",
Expand Down
90 changes: 90 additions & 0 deletions src/__tests__/core/runtime-overrides.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,35 @@ describe("saveRuntimeOverride", () => {
const result = loadRuntimeOverrides(tmpDir);
assert.equal(result.description?.enabled, false);
});

it("handles string values", () => {
saveRuntimeOverride(tmpDir, ["embedding", "model"], "nomic-embed-text");
const result = loadRuntimeOverrides(tmpDir);
assert.equal(result.embedding?.model, "nomic-embed-text");
});

it("handles string enum values", () => {
saveRuntimeOverride(tmpDir, ["embedding", "provider"], "openai");
const result = loadRuntimeOverrides(tmpDir);
assert.equal(result.embedding?.provider, "openai");
});

it("overwrites string value with another string", () => {
saveRuntimeOverride(tmpDir, ["embedding", "baseUrl"], "http://localhost:11434/api");
saveRuntimeOverride(tmpDir, ["embedding", "baseUrl"], "http://custom:8080/api");
const result = loadRuntimeOverrides(tmpDir);
assert.equal(result.embedding?.baseUrl, "http://custom:8080/api");
});

it("mixes string and boolean overrides", () => {
saveRuntimeOverride(tmpDir, ["embedding", "provider"], "openai");
saveRuntimeOverride(tmpDir, ["embedding", "model"], "text-embedding-3-small");
saveRuntimeOverride(tmpDir, ["description", "enabled"], false);
const result = loadRuntimeOverrides(tmpDir);
assert.equal(result.embedding?.provider, "openai");
assert.equal(result.embedding?.model, "text-embedding-3-small");
assert.equal(result.description?.enabled, false);
});
});

describe("applyRuntimeOverrides", () => {
Expand Down Expand Up @@ -221,4 +250,65 @@ describe("applyRuntimeOverrides", () => {
});
assert.equal(result.description?.enabled, false);
});

it("applies embedding.provider override", () => {
const result = applyRuntimeOverrides(DEFAULT_CONFIG, {
embedding: { provider: "openai" },
});
assert.equal(result.embedding.provider, "openai");
});

it("applies embedding.model override", () => {
const result = applyRuntimeOverrides(DEFAULT_CONFIG, {
embedding: { model: "text-embedding-3-small" },
});
assert.equal(result.embedding.model, "text-embedding-3-small");
});

it("applies embedding.baseUrl override", () => {
const result = applyRuntimeOverrides(DEFAULT_CONFIG, {
embedding: { baseUrl: "https://custom.api.com/v1" },
});
assert.equal(result.embedding.baseUrl, "https://custom.api.com/v1");
});

it("applies all embedding overrides simultaneously", () => {
const result = applyRuntimeOverrides(DEFAULT_CONFIG, {
embedding: { provider: "openai", model: "text-embedding-3-small", baseUrl: "https://api.openai.com/v1" },
});
assert.equal(result.embedding.provider, "openai");
assert.equal(result.embedding.model, "text-embedding-3-small");
assert.equal(result.embedding.baseUrl, "https://api.openai.com/v1");
});

it("applies description.provider override", () => {
const result = applyRuntimeOverrides(DEFAULT_CONFIG, {
description: { provider: "openai" },
});
assert.equal(result.description?.provider, "openai");
});

it("applies description.model override", () => {
const result = applyRuntimeOverrides(DEFAULT_CONFIG, {
description: { model: "gpt-4o-mini" },
});
assert.equal(result.description?.model, "gpt-4o-mini");
});

it("applies description.baseUrl override", () => {
const result = applyRuntimeOverrides(DEFAULT_CONFIG, {
description: { baseUrl: "https://custom.api.com/v1" },
});
assert.equal(result.description?.baseUrl, "https://custom.api.com/v1");
});

it("applies description.provider and model together with enabled", () => {
const result = applyRuntimeOverrides(DEFAULT_CONFIG, {
description: { provider: "openai", model: "gpt-4o-mini", enabled: true },
});
assert.equal(result.description?.provider, "openai");
assert.equal(result.description?.model, "gpt-4o-mini");
assert.equal(result.description?.enabled, true);
});

});
19 changes: 16 additions & 3 deletions src/__tests__/embedder/factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,11 @@ describe("createEmbedder", () => {
},
});
assert.throws(() => createEmbedder(config), {
message: "OpenAI provider requires an apiKey",
message: /openai provider requires an apiKey/,
});
});

it("throws for unknown provider", () => {
it("treats unknown provider as OpenAI-compatible and requires apiKey", () => {
const config = makeConfig({
embedding: {
provider: "unknown" as "ollama",
Expand All @@ -85,7 +85,20 @@ describe("createEmbedder", () => {
},
});
assert.throws(() => createEmbedder(config), {
message: /Unknown embedding provider/,
message: /requires an apiKey/,
});
});

it("creates OpenAIProvider for unknown provider with apiKey", () => {
const config = makeConfig({
embedding: {
provider: "custom" as "ollama",
baseUrl: "https://custom.api/v1",
model: "custom-model",
apiKey: "custom-key",
},
});
const embedder = createEmbedder(config);
assert.equal(embedder.name, "openai");
});
});
Loading
Loading