opencode-rag-plugin 1.17.3 → 1.18.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 +41 -1
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.js +1 -0
- package/dist/cli/commands/init-helpers.d.ts +5 -1
- package/dist/cli/commands/init-helpers.js +12 -19
- package/dist/cli/commands/init.js +42 -8
- package/dist/cli/commands/quirk.d.ts +10 -0
- package/dist/cli/commands/quirk.js +141 -0
- package/dist/cli/commands/setup.js +15 -6
- package/dist/cli/commands/ui.js +1 -1
- package/dist/cli/index.js +2 -1
- package/dist/core/config.d.ts +30 -0
- package/dist/core/config.js +26 -2
- package/dist/core/interfaces.d.ts +25 -1
- package/dist/core/runtime-overrides.d.ts +7 -0
- package/dist/core/runtime-overrides.js +15 -0
- package/dist/describer/anthropic.d.ts +4 -0
- package/dist/describer/anthropic.js +9 -2
- package/dist/describer/describer.d.ts +4 -0
- package/dist/describer/describer.js +8 -0
- package/dist/describer/gemini.d.ts +3 -0
- package/dist/describer/gemini.js +8 -2
- package/dist/indexer/pipeline.js +40 -0
- package/dist/opencode/system-guidance.d.ts +34 -0
- package/dist/opencode/system-guidance.js +127 -0
- package/dist/opencode/tools.d.ts +38 -0
- package/dist/opencode/tools.js +127 -0
- package/dist/plugin.js +204 -31
- package/dist/quirks/auto-capture.d.ts +14 -0
- package/dist/quirks/auto-capture.js +112 -0
- package/dist/quirks/monitor.d.ts +5 -0
- package/dist/quirks/monitor.js +23 -0
- package/dist/quirks/prompts.d.ts +1 -0
- package/dist/quirks/prompts.js +13 -0
- package/dist/quirks/quirk-store.d.ts +27 -0
- package/dist/quirks/quirk-store.js +217 -0
- package/dist/quirks/types.d.ts +20 -0
- package/dist/quirks/types.js +2 -0
- package/dist/retriever/keyword-index.js +16 -0
- package/dist/vectorstore/lancedb.d.ts +21 -11
- package/dist/vectorstore/lancedb.js +174 -18
- package/dist/vectorstore/memory.d.ts +2 -0
- package/dist/vectorstore/memory.js +6 -0
- package/dist/web/api.d.ts +3 -1
- package/dist/web/api.js +50 -1
- package/dist/web/server.d.ts +2 -1
- package/dist/web/server.js +3 -2
- package/dist/web/ui/index.html +163 -0
- package/package.json +1 -1
package/ReadMe.md
CHANGED
|
@@ -41,13 +41,14 @@ opencode-rag query "authentication middleware"
|
|
|
41
41
|
| **OpenCode plugin** | Auto-inject context, read-tool override, TUI settings, Ctrl+Enter to add RAG context, MCP registration on `init` |
|
|
42
42
|
| **Incremental indexing** | File-hash manifest, background watcher, auto-rebuild on corruption |
|
|
43
43
|
| **Privacy-first** | All processing stays local (when using Ollama) |
|
|
44
|
-
| **CLI Tools** | `init`, `index`, `query`, `status`, `list`, `show`, `dump`, `clear`, `describe-image`, `ui`, `mcp`, `setup`, `eval:sessions`, `eval:analyze`, `eval:compare` |
|
|
44
|
+
| **CLI Tools** | `init`, `index`, `query`, `status`, `list`, `show`, `dump`, `clear`, `describe-image`, `ui`, `mcp`, `setup`, `quirk`, `eval:sessions`, `eval:analyze`, `eval:compare` |
|
|
45
45
|
| **Proxy-aware** | Corporate proxy support with raw-socket localhost bypass |
|
|
46
46
|
| **OpenAI / Anthropic / Cohere** | Use alternate embedding providers with API key auto-resolution |
|
|
47
47
|
| **Evaluation** | Session-level token tracking, RAG-on vs RAG-off comparison, tiktoken BPE counting |
|
|
48
48
|
| **Documentation mode** | `/doc` slash command: agent adds JSDoc/TSDoc to undocumented files, progress tracked per subdirectory |
|
|
49
49
|
| **Wiki mode** | `/wiki` slash command: agent maintains a persistent knowledge wiki at `.opencode/wiki/` (ingest, query, lint, seed) |
|
|
50
50
|
| **Context optimization** | Post-retrieval dedup, per-file chunk limits, adjacent-merge to fit the context window |
|
|
51
|
+
| **Quirk memory** | Persistent experiential memory — agents recall/persist gotchas, preferences, decisions across sessions (`add_quirk` / `recall_quirks`, `opencode-rag quirk`) |
|
|
51
52
|
| **AGENTS.md directive** | `opencode-rag init` merges a tool-usage directive into `AGENTS.md` via sentinel markers |
|
|
52
53
|
|
|
53
54
|
## Web UI
|
|
@@ -125,6 +126,45 @@ The agent maintains all wiki pages during normal coding sessions (ingest on lear
|
|
|
125
126
|
|
|
126
127
|
See [Plugin documentation](doc/plugin.md#6-wiki-mode--slash-command-wiki) for the full protocol.
|
|
127
128
|
|
|
129
|
+
## Quirk Memory
|
|
130
|
+
|
|
131
|
+
OpenCodeRAG gives your agent **persistent, cross-session memory** of non-obvious facts — gotchas, preferences, decisions, and environment constraints — that it can recall and extend over time. Quirks are embedded and stored in the same vector store as your code, then recalled via semantic search whenever they're relevant. This lets the agent avoid repeating mistakes and accumulate project knowledge across sessions.
|
|
132
|
+
|
|
133
|
+
**Enabled by default.** Turn it on/off in `opencode-rag.json`:
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"memory": {
|
|
138
|
+
"enabled": true,
|
|
139
|
+
"autoInject": true,
|
|
140
|
+
"minConfidence": 0.5,
|
|
141
|
+
"recallMinScore": 0.3,
|
|
142
|
+
"decay": { "enabled": true, "halfLifeDays": 30 }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Agent tools:**
|
|
148
|
+
|
|
149
|
+
| Tool | Use when |
|
|
150
|
+
|------|----------|
|
|
151
|
+
| `recall_quirks(query)` | You hit an error or need to remember a gotcha, preference, or decision from past sessions |
|
|
152
|
+
| `add_quirk(content, { type, tags })` | You just discovered a non-obvious fact, workaround, or convention worth remembering |
|
|
153
|
+
|
|
154
|
+
**CLI — manage quirks directly:**
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
opencode-rag quirk add "npm needs --legacy-peer-deps" --type gotcha --tag installation
|
|
158
|
+
opencode-rag quirk list
|
|
159
|
+
opencode-rag quirk lint # flag low-confidence / stale / duplicate quirks
|
|
160
|
+
opencode-rag quirk test "npm needs --legacy-peer-deps"
|
|
161
|
+
# ✓ Quirk has been appended:
|
|
162
|
+
# [gotcha] npm needs --legacy-peer-deps (installation)
|
|
163
|
+
# 99% confidence
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
When `memory.autoInject` is `true`, relevant quirks are automatically injected into the prompt during retrieval (via the `chat.message` hook). Every `add_quirk` is vetted by an immutable trust monitor that rejects destructive patterns (e.g. `rm -rf`, `force push`, `bypass security`). See [Plugin documentation](doc/plugin.md#9-quirk-memory-experiential-memory) and [CLI Reference: `quirk`](doc/cli.md#quirk).
|
|
167
|
+
|
|
128
168
|
## MCP Server (Optional)
|
|
129
169
|
|
|
130
170
|
OpenCodeRAG ships a CLI-based [MCP (Model Context Protocol)](https://spec.modelcontextprotocol.io/) server that exposes semantic code tools to any MCP-compatible client (Claude Desktop, Cursor, etc.).
|
|
@@ -15,3 +15,4 @@ export { registerSetupCommand } from "./setup.js";
|
|
|
15
15
|
export { registerUpdateCommand } from "./update.js";
|
|
16
16
|
export { registerEvalSessionsCommand, registerEvalAnalyzeCommand, registerEvalCompareCommand } from "./eval.js";
|
|
17
17
|
export { registerDescribeImageCommand } from "./describe-image.js";
|
|
18
|
+
export { registerQuirkCommand } from "./quirk.js";
|
|
@@ -15,4 +15,5 @@ export { registerSetupCommand } from "./setup.js";
|
|
|
15
15
|
export { registerUpdateCommand } from "./update.js";
|
|
16
16
|
export { registerEvalSessionsCommand, registerEvalAnalyzeCommand, registerEvalCompareCommand } from "./eval.js";
|
|
17
17
|
export { registerDescribeImageCommand } from "./describe-image.js";
|
|
18
|
+
export { registerQuirkCommand } from "./quirk.js";
|
|
18
19
|
//# sourceMappingURL=index.js.map
|
|
@@ -83,9 +83,13 @@ export declare function mergeGitignoreContent(existingContent?: string): string;
|
|
|
83
83
|
* provided, the section alone is returned as a new file.
|
|
84
84
|
*
|
|
85
85
|
* @param existingContent - The current `AGENTS.md` content, or `undefined` if absent.
|
|
86
|
+
* @param opts - Optional settings. `promptEnforcement` controls whether the
|
|
87
|
+
* quirk-capture enforcement rules are included (default `true`).
|
|
86
88
|
* @returns The merged `AGENTS.md` content with a trailing newline.
|
|
87
89
|
*/
|
|
88
|
-
export declare function mergeAgentsMdContent(existingContent?: string
|
|
90
|
+
export declare function mergeAgentsMdContent(existingContent?: string, opts?: {
|
|
91
|
+
promptEnforcement?: boolean;
|
|
92
|
+
}): string;
|
|
89
93
|
/**
|
|
90
94
|
* Get the runtime directory path (`~/.opencode`).
|
|
91
95
|
*
|
|
@@ -11,6 +11,7 @@ import { existsSync, mkdirSync, rmSync, symlinkSync, } from "node:fs";
|
|
|
11
11
|
import { execSync } from "node:child_process";
|
|
12
12
|
import { DEFAULT_CONFIG } from "../../core/config.js";
|
|
13
13
|
import { c } from "../format.js";
|
|
14
|
+
import { BEGIN_MARKER, END_MARKER, buildAgentsMdDirective } from "../../opencode/system-guidance.js";
|
|
14
15
|
import { getStringRecord, readJsonObject, writeJsonFile, } from "../helpers.js";
|
|
15
16
|
/**
|
|
16
17
|
* Build the workspace-local `.opencode/package.json` content.
|
|
@@ -165,6 +166,8 @@ export function generateSkillFile() {
|
|
|
165
166
|
"3. User mentions a function/class/variable to edit → `find_usages(symbolName)` THEN `search_semantic` THEN `edit`",
|
|
166
167
|
"4. User asks a code question → `search_semantic` to gather context before answering",
|
|
167
168
|
"5. User asks about an image or visual asset → `describe_image(filePath)` to retrieve its generated description, then optionally `search_semantic` for related code",
|
|
169
|
+
"6. You encounter an error or need a known pitfall → `recall_quirks(query)`",
|
|
170
|
+
"7. You discover a non-obvious fact or workaround → `add_quirk(content)` to persist it",
|
|
168
171
|
"",
|
|
169
172
|
"### When to use each tool",
|
|
170
173
|
"",
|
|
@@ -174,6 +177,8 @@ export function generateSkillFile() {
|
|
|
174
177
|
"| `get_file_skeleton` | You have a file path but need to orient before reading | `\"src/plugin.ts\"` |",
|
|
175
178
|
"| `find_usages` | Before editing any function, class, or variable — check all call sites | `\"createRagHooks\"` |",
|
|
176
179
|
"| `describe_image` | When the user refers to an image or asks \"what's in this screenshot/diagram?\" | `\"assets/login-screen.png\"` |",
|
|
180
|
+
"| `recall_quirks` | You hit an error or need to remember a gotcha, preference, or decision from past sessions | `\"lancedb type casting\"` |",
|
|
181
|
+
"| `add_quirk` | You just discovered a non-obvious fact, workaround, or convention worth remembering | `'\"npm needs --legacy-peer-deps\" --type gotcha --tag installation'` |",
|
|
177
182
|
"",
|
|
178
183
|
"### Workflow",
|
|
179
184
|
"",
|
|
@@ -248,21 +253,6 @@ export function mergeGitignoreContent(existingContent) {
|
|
|
248
253
|
merged.push("# OpenCodeRAG workspace state", ...missing, "");
|
|
249
254
|
return merged.join("\n");
|
|
250
255
|
}
|
|
251
|
-
const BEGIN_MARKER = "<!-- BEGIN opencode-rag -->";
|
|
252
|
-
const END_MARKER = "<!-- END opencode-rag -->";
|
|
253
|
-
const AGENTS_MD_SECTION = [
|
|
254
|
-
BEGIN_MARKER,
|
|
255
|
-
"## Code Navigation",
|
|
256
|
-
"",
|
|
257
|
-
"ALWAYS use OpenCodeRAG tools before reading or editing:",
|
|
258
|
-
"- **Search first** — `search_semantic(query)` instead of grep/glob",
|
|
259
|
-
"- **Skeleton before read** — `get_file_skeleton(filePath)` then read specific lines",
|
|
260
|
-
"- **Usages before edit** — `find_usages(symbolName)` before modifying any symbol",
|
|
261
|
-
"- **Images via describe** — `describe_image(filePath)` — never read raw bytes",
|
|
262
|
-
"",
|
|
263
|
-
"If no results, run `opencode-rag index`.",
|
|
264
|
-
END_MARKER,
|
|
265
|
-
].join("\n");
|
|
266
256
|
/**
|
|
267
257
|
* Merge the OpenCodeRAG directive section into an existing `AGENTS.md`.
|
|
268
258
|
*
|
|
@@ -272,23 +262,26 @@ const AGENTS_MD_SECTION = [
|
|
|
272
262
|
* provided, the section alone is returned as a new file.
|
|
273
263
|
*
|
|
274
264
|
* @param existingContent - The current `AGENTS.md` content, or `undefined` if absent.
|
|
265
|
+
* @param opts - Optional settings. `promptEnforcement` controls whether the
|
|
266
|
+
* quirk-capture enforcement rules are included (default `true`).
|
|
275
267
|
* @returns The merged `AGENTS.md` content with a trailing newline.
|
|
276
268
|
*/
|
|
277
|
-
export function mergeAgentsMdContent(existingContent) {
|
|
269
|
+
export function mergeAgentsMdContent(existingContent, opts) {
|
|
270
|
+
const section = buildAgentsMdDirective({ promptEnforcement: opts?.promptEnforcement ?? true });
|
|
278
271
|
if (!existingContent) {
|
|
279
|
-
return `${
|
|
272
|
+
return `${section}\n`;
|
|
280
273
|
}
|
|
281
274
|
const beginIdx = existingContent.indexOf(BEGIN_MARKER);
|
|
282
275
|
const endIdx = existingContent.indexOf(END_MARKER);
|
|
283
276
|
if (beginIdx !== -1 && endIdx !== -1 && endIdx > beginIdx) {
|
|
284
277
|
const before = existingContent.slice(0, beginIdx);
|
|
285
278
|
const after = existingContent.slice(endIdx + END_MARKER.length);
|
|
286
|
-
const merged = `${before}${
|
|
279
|
+
const merged = `${before}${section}${after}`;
|
|
287
280
|
return merged.endsWith("\n") ? merged : `${merged}\n`;
|
|
288
281
|
}
|
|
289
282
|
const trimmed = existingContent.trimEnd();
|
|
290
283
|
const separator = trimmed.endsWith(BEGIN_MARKER) ? "" : "\n\n";
|
|
291
|
-
return `${trimmed}${separator}${
|
|
284
|
+
return `${trimmed}${separator}${section}\n`;
|
|
292
285
|
}
|
|
293
286
|
/**
|
|
294
287
|
* Get the runtime directory path (`~/.opencode`).
|
|
@@ -54,6 +54,11 @@ export function registerInitCommand(program) {
|
|
|
54
54
|
else {
|
|
55
55
|
console.log(` ${c.exists("Exists:")} .opencode/`);
|
|
56
56
|
}
|
|
57
|
+
// Wiki safety: warn if wiki exists but would be affected (currently init never touches it)
|
|
58
|
+
const wikiDir = path.join(opencodeDir, "wiki");
|
|
59
|
+
if (existsSync(wikiDir)) {
|
|
60
|
+
console.log(` ${c.exists("Preserved:")} .opencode/wiki/ (never modified by init)`);
|
|
61
|
+
}
|
|
57
62
|
if (!existsSync(pluginsDir)) {
|
|
58
63
|
mkdirSync(pluginsDir, { recursive: true });
|
|
59
64
|
console.log(` ${c.created("Created:")} .opencode/plugins/`);
|
|
@@ -144,7 +149,18 @@ export function registerInitCommand(program) {
|
|
|
144
149
|
}
|
|
145
150
|
const agentsMdPath = path.join(cwd, "AGENTS.md");
|
|
146
151
|
const agentsMdExists = existsSync(agentsMdPath);
|
|
147
|
-
|
|
152
|
+
// Read effective promptEnforcement from existing config (if present)
|
|
153
|
+
let promptEnforcement = true;
|
|
154
|
+
try {
|
|
155
|
+
if (existsSync(configPath)) {
|
|
156
|
+
const existingCfg = loadConfig(configPath, false);
|
|
157
|
+
promptEnforcement = existingCfg.memory?.promptEnforcement ?? true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Malformed or missing config — use default
|
|
162
|
+
}
|
|
163
|
+
const nextAgentsMd = mergeAgentsMdContent(agentsMdExists ? readFileSync(agentsMdPath, "utf-8") : undefined, { promptEnforcement });
|
|
148
164
|
if (!agentsMdExists || options.force) {
|
|
149
165
|
writeFileSync(agentsMdPath, nextAgentsMd, "utf-8");
|
|
150
166
|
console.log(` ${agentsMdExists ? c.updated("Updated:") : c.created("Created:")} AGENTS.md`);
|
|
@@ -170,16 +186,34 @@ export function registerInitCommand(program) {
|
|
|
170
186
|
console.log(` ${c.exists("Exists:")} .opencode/package.json`);
|
|
171
187
|
}
|
|
172
188
|
const configExists = existsSync(configPath);
|
|
173
|
-
if (!configExists
|
|
174
|
-
if (configExists && options.force) {
|
|
175
|
-
copyFileSync(configPath, configPath + ".bak");
|
|
176
|
-
console.log(` ${c.dim("Backup:")} ${configPath}.bak`);
|
|
177
|
-
}
|
|
189
|
+
if (!configExists) {
|
|
178
190
|
writeFileSync(configPath, generateDefaultConfigJson(), "utf-8");
|
|
179
|
-
console.log(` ${
|
|
191
|
+
console.log(` ${c.created("Created:")} opencode-rag.json`);
|
|
180
192
|
}
|
|
181
193
|
else {
|
|
182
|
-
|
|
194
|
+
// NEVER overwrite existing config without interactive confirmation
|
|
195
|
+
let overwrite = false;
|
|
196
|
+
if (process.stdin.isTTY) {
|
|
197
|
+
const readline = await import("node:readline");
|
|
198
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
199
|
+
const answer = await new Promise((resolve) => {
|
|
200
|
+
rl.question(` ${c.warn("opencode-rag.json already exists.")} Overwrite with default config? A backup will be saved. (y/N) `, resolve);
|
|
201
|
+
});
|
|
202
|
+
rl.close();
|
|
203
|
+
overwrite = /^y(?:es)?$/i.test(answer.trim());
|
|
204
|
+
}
|
|
205
|
+
if (overwrite) {
|
|
206
|
+
copyFileSync(configPath, `${configPath}.bak`);
|
|
207
|
+
console.log(` ${c.dim("Backup:")} opencode-rag.json.bak`);
|
|
208
|
+
writeFileSync(configPath, generateDefaultConfigJson(), "utf-8");
|
|
209
|
+
console.log(` ${c.updated("Updated:")} opencode-rag.json`);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
console.log(` ${c.exists("Exists:")} opencode-rag.json (preserved)`);
|
|
213
|
+
if (!process.stdin.isTTY && options.force) {
|
|
214
|
+
console.log(` ${c.warn("Skipped overwrite:")} non-interactive shell. Edit opencode-rag.json manually or re-run in a TTY.`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
183
217
|
}
|
|
184
218
|
// ── Provider health check + dependency install (parallel) ──
|
|
185
219
|
const healthPromise = options.skipHealthCheck
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Quirk command — manage experiential agent memory (add, list, rm, lint).
|
|
3
|
+
*/
|
|
4
|
+
import type { Command } from "commander";
|
|
5
|
+
/**
|
|
6
|
+
* Register the `quirk` command on the given Commander program.
|
|
7
|
+
*
|
|
8
|
+
* @param program - The Commander `Command` instance to register on.
|
|
9
|
+
*/
|
|
10
|
+
export declare function registerQuirkCommand(program: Command): void;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { resolveCliContext, cleanupContext, logCliInfo, logCliError, c } from "../format.js";
|
|
2
|
+
import { addQuirk, listQuirks, lintQuirks, recallQuirks, removeQuirk } from "../../quirks/quirk-store.js";
|
|
3
|
+
/**
|
|
4
|
+
* Register the `quirk` command on the given Commander program.
|
|
5
|
+
*
|
|
6
|
+
* @param program - The Commander `Command` instance to register on.
|
|
7
|
+
*/
|
|
8
|
+
export function registerQuirkCommand(program) {
|
|
9
|
+
const quirkCmd = program
|
|
10
|
+
.command("quirk")
|
|
11
|
+
.description("Manage experiential quirk memory (gotchas, preferences, decisions, constraints)");
|
|
12
|
+
quirkCmd
|
|
13
|
+
.command("add")
|
|
14
|
+
.description("Add a new quirk")
|
|
15
|
+
.argument("<content>", "quirk text")
|
|
16
|
+
.option("-t, --type <type>", "quirk type: gotcha, preference, decision, environment-constraint")
|
|
17
|
+
.option("--tag <tags...>", "tags for filtering")
|
|
18
|
+
.option("--source-ref <path>", "source file path reference")
|
|
19
|
+
.action(async (content, options) => {
|
|
20
|
+
try {
|
|
21
|
+
const ctx = await resolveCliContext(options, resolveLogPath());
|
|
22
|
+
const { config, embedder, store, keywordIndex } = ctx;
|
|
23
|
+
const quirk = await addQuirk({ embedder, store, keywordIndex: keywordIndex, cfg: config, storePath: ctx.storePath }, {
|
|
24
|
+
content,
|
|
25
|
+
quirkType: options.type,
|
|
26
|
+
tags: options.tag ? (Array.isArray(options.tag) ? options.tag : [options.tag]) : undefined,
|
|
27
|
+
sourceRef: options.sourceRef,
|
|
28
|
+
});
|
|
29
|
+
logCliInfo(ctx.logFilePath, "quirk add", `\n${c.success("Quirk added:")}`);
|
|
30
|
+
logCliInfo(ctx.logFilePath, "quirk add", ` ${c.label("ID:")} ${quirk.id}`);
|
|
31
|
+
logCliInfo(ctx.logFilePath, "quirk add", ` ${c.label("Type:")} ${quirk.quirkType ?? "general"}`);
|
|
32
|
+
logCliInfo(ctx.logFilePath, "quirk add", ` ${c.label("Confidence:")} ${(quirk.confidence * 100).toFixed(0)}%`);
|
|
33
|
+
await cleanupContext(ctx);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
logCliError(resolveLogPath(), "quirk add", `Failed to add quirk: ${err.message}`, err);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
quirkCmd
|
|
41
|
+
.command("list")
|
|
42
|
+
.description("List all quirks")
|
|
43
|
+
.option("-c, --config <path>", "path to config file")
|
|
44
|
+
.action(async (options) => {
|
|
45
|
+
try {
|
|
46
|
+
const ctx = await resolveCliContext(options, resolveLogPath());
|
|
47
|
+
const { config, embedder, store, keywordIndex } = ctx;
|
|
48
|
+
const quirks = await listQuirks({ embedder, store, keywordIndex: keywordIndex, cfg: config, storePath: ctx.storePath });
|
|
49
|
+
logCliInfo(ctx.logFilePath, "quirk list", `\n${c.num(quirks.length)} quirk(s):\n`);
|
|
50
|
+
for (const q of quirks) {
|
|
51
|
+
const badge = q.quirkType ? `[${q.quirkType}] ` : "";
|
|
52
|
+
const tags = q.tags.length > 0 ? ` (${q.tags.join(", ")})` : "";
|
|
53
|
+
const date = q.lastObserved ? new Date(q.lastObserved).toLocaleDateString() : "";
|
|
54
|
+
logCliInfo(ctx.logFilePath, "quirk list", ` ${c.value(badge)}${c.file(q.content.slice(0, 120))}${c.dim(tags)} ${c.dim(date)}`);
|
|
55
|
+
}
|
|
56
|
+
await cleanupContext(ctx);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
logCliError(resolveLogPath(), "quirk list", `Failed to list quirks: ${err.message}`, err);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
quirkCmd
|
|
64
|
+
.command("rm")
|
|
65
|
+
.description("Remove a quirk by ID")
|
|
66
|
+
.argument("<id>", "quirk ID")
|
|
67
|
+
.option("-c, --config <path>", "path to config file")
|
|
68
|
+
.action(async (id, options) => {
|
|
69
|
+
try {
|
|
70
|
+
const ctx = await resolveCliContext(options, resolveLogPath());
|
|
71
|
+
const { config, embedder, store, keywordIndex } = ctx;
|
|
72
|
+
await removeQuirk({ embedder, store, keywordIndex: keywordIndex, cfg: config, storePath: ctx.storePath }, id);
|
|
73
|
+
logCliInfo(ctx.logFilePath, "quirk rm", `\n${c.success("Quirk removed:")} ${id}`);
|
|
74
|
+
await cleanupContext(ctx);
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
logCliError(resolveLogPath(), "quirk rm", `Failed to remove quirk: ${err.message}`, err);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
quirkCmd
|
|
82
|
+
.command("lint")
|
|
83
|
+
.description("Health-check quirks (low confidence, stale, duplicates)")
|
|
84
|
+
.option("-c, --config <path>", "path to config file")
|
|
85
|
+
.action(async (options) => {
|
|
86
|
+
try {
|
|
87
|
+
const ctx = await resolveCliContext(options, resolveLogPath());
|
|
88
|
+
const { config, embedder, store, keywordIndex } = ctx;
|
|
89
|
+
const issues = await lintQuirks({ embedder, store, keywordIndex: keywordIndex, cfg: config, storePath: ctx.storePath });
|
|
90
|
+
if (issues.length === 0) {
|
|
91
|
+
logCliInfo(ctx.logFilePath, "quirk lint", c.success("\nNo issues found. All quirks look healthy."));
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
logCliInfo(ctx.logFilePath, "quirk lint", `\n${c.warn(`${issues.length} issue(s) found:`)}\n`);
|
|
95
|
+
for (const issue of issues) {
|
|
96
|
+
logCliInfo(ctx.logFilePath, "quirk lint", ` • ${issue}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
await cleanupContext(ctx);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
logCliError(resolveLogPath(), "quirk lint", `Failed to lint quirks: ${err.message}`, err);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
quirkCmd
|
|
107
|
+
.command("test")
|
|
108
|
+
.description("Test whether a quirk with similar content already exists")
|
|
109
|
+
.argument("<content>", "quirk text to test")
|
|
110
|
+
.option("-c, --config <path>", "path to config file")
|
|
111
|
+
.action(async (content, options) => {
|
|
112
|
+
try {
|
|
113
|
+
const ctx = await resolveCliContext(options, resolveLogPath());
|
|
114
|
+
const { config, embedder, store, keywordIndex } = ctx;
|
|
115
|
+
const results = await recallQuirks({ embedder, store, keywordIndex: keywordIndex, cfg: config, storePath: ctx.storePath }, content, { topK: 5 });
|
|
116
|
+
if (results.length > 0) {
|
|
117
|
+
logCliInfo(ctx.logFilePath, "quirk test", c.success("\n✓ Quirk has been appended:\n"));
|
|
118
|
+
for (const r of results) {
|
|
119
|
+
const badge = r.chunk.metadata.quirkType ? `[${r.chunk.metadata.quirkType}] ` : "";
|
|
120
|
+
const tags = r.chunk.metadata.tags?.length ? ` (${r.chunk.metadata.tags.join(", ")})` : "";
|
|
121
|
+
const confidence = r.chunk.metadata.confidence ? `${(r.chunk.metadata.confidence * 100).toFixed(0)}% confidence` : "";
|
|
122
|
+
logCliInfo(ctx.logFilePath, "quirk test", ` ${c.value(badge)}${c.file(r.chunk.content.slice(0, 120))}${c.dim(tags)}`);
|
|
123
|
+
logCliInfo(ctx.logFilePath, "quirk test", ` ${c.dim(confidence)}`);
|
|
124
|
+
logCliInfo(ctx.logFilePath, "quirk test", "");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
logCliInfo(ctx.logFilePath, "quirk test", c.warn("\n✗ No matching quirk found — quirk has not been appended\n"));
|
|
129
|
+
}
|
|
130
|
+
await cleanupContext(ctx);
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
logCliError(resolveLogPath(), "quirk test", `Failed to test quirk: ${err.message}`, err);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function resolveLogPath() {
|
|
139
|
+
return "./.opencode/opencode-rag.log";
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=quirk.js.map
|
|
@@ -12,16 +12,25 @@ function removeIfExists(targetPath) {
|
|
|
12
12
|
}
|
|
13
13
|
function checkOpenCodeRunning() {
|
|
14
14
|
try {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
if (process.platform === "win32") {
|
|
16
|
+
execSync('tasklist /FI "IMAGENAME eq opencode.exe" 2>nul | find /I "opencode.exe" >nul', {
|
|
17
|
+
timeout: 3_000,
|
|
18
|
+
stdio: "ignore",
|
|
19
|
+
});
|
|
20
20
|
console.log(`\n ${c.warn("OpenCode is currently running.")} Restart it to load the updated plugin.`);
|
|
21
21
|
}
|
|
22
|
+
else {
|
|
23
|
+
const pids = execSync("pgrep -x opencode 2>/dev/null", {
|
|
24
|
+
encoding: "utf-8",
|
|
25
|
+
timeout: 3_000,
|
|
26
|
+
}).trim();
|
|
27
|
+
if (pids) {
|
|
28
|
+
console.log(`\n ${c.warn("OpenCode is currently running.")} Restart it to load the updated plugin.`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
22
31
|
}
|
|
23
32
|
catch {
|
|
24
|
-
//
|
|
33
|
+
// OpenCode not found or tools unavailable
|
|
25
34
|
}
|
|
26
35
|
}
|
|
27
36
|
export function registerSetupCommand(program) {
|
package/dist/cli/commands/ui.js
CHANGED
|
@@ -32,7 +32,7 @@ export function registerUiCommand(program) {
|
|
|
32
32
|
const port = parseInt(options.port ?? String(config.ui?.port ?? 3210), 10);
|
|
33
33
|
const openBrowser = options.open !== false && (config.ui?.openBrowser ?? true);
|
|
34
34
|
const { startWebUi } = await import("../../web/server.js");
|
|
35
|
-
const server = await startWebUi(storePath, port, cwd);
|
|
35
|
+
const server = await startWebUi(storePath, port, cwd, config.embedding.vectorDimension ?? 384, config);
|
|
36
36
|
const url = `http://127.0.0.1:${server.port}`;
|
|
37
37
|
logCliInfo(logFilePath, "ui", `\n${c.heading("OpenCodeRAG Web UI")}`);
|
|
38
38
|
logCliInfo(logFilePath, "ui", ` ${c.label("URL:")} ${c.value(url)}`);
|
package/dist/cli/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import { realpathSync } from "node:fs";
|
|
|
14
14
|
import { basename, dirname } from "node:path";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
import { getPackageMetadata } from "./helpers.js";
|
|
17
|
-
import { registerIndexCommand, registerQueryCommand, registerClearCommand, registerStatusCommand, registerListCommand, registerShowCommand, registerDumpCommand, registerInitCommand, registerUiCommand, registerMcpCommand, registerSetupCommand, registerUpdateCommand, registerEvalSessionsCommand, registerEvalAnalyzeCommand, registerEvalCompareCommand, registerDescribeImageCommand, } from "./commands/index.js";
|
|
17
|
+
import { registerIndexCommand, registerQueryCommand, registerClearCommand, registerStatusCommand, registerListCommand, registerShowCommand, registerDumpCommand, registerInitCommand, registerUiCommand, registerMcpCommand, registerSetupCommand, registerUpdateCommand, registerEvalSessionsCommand, registerEvalAnalyzeCommand, registerEvalCompareCommand, registerDescribeImageCommand, registerQuirkCommand, } from "./commands/index.js";
|
|
18
18
|
/**
|
|
19
19
|
* The top-level Commander program instance that defines the `opencode-rag` CLI.
|
|
20
20
|
*
|
|
@@ -44,6 +44,7 @@ registerEvalSessionsCommand(program);
|
|
|
44
44
|
registerEvalAnalyzeCommand(program);
|
|
45
45
|
registerEvalCompareCommand(program);
|
|
46
46
|
registerInitCommand(program);
|
|
47
|
+
registerQuirkCommand(program);
|
|
47
48
|
// ── Auto-run detection ──────────────────────────────────────────
|
|
48
49
|
/**
|
|
49
50
|
* Determine whether the CLI should auto-run for the current module.
|
package/dist/core/config.d.ts
CHANGED
|
@@ -121,6 +121,34 @@ export interface WikiModeConfig {
|
|
|
121
121
|
/** System prompt for the wiki maintainer agent. */
|
|
122
122
|
systemPrompt: string;
|
|
123
123
|
}
|
|
124
|
+
/** Configuration for the quirk/experiential memory system. */
|
|
125
|
+
export interface MemoryConfig {
|
|
126
|
+
/** Whether quirk memory is enabled. */
|
|
127
|
+
enabled: boolean;
|
|
128
|
+
/** Whether relevant quirks should be auto-injected into the prompt during retrieval. */
|
|
129
|
+
autoInject: boolean;
|
|
130
|
+
/** Minimum confidence (0-1) for a quirk to be returned. */
|
|
131
|
+
minConfidence: number;
|
|
132
|
+
/** Minimum query-relevance score (0-1) for a quirk to be recalled. */
|
|
133
|
+
recallMinScore: number;
|
|
134
|
+
/** Automatically extract quirks from each completed agent turn. */
|
|
135
|
+
passiveCapture: boolean;
|
|
136
|
+
/** Upgrade the system-prompt nudge into a mandatory trigger. */
|
|
137
|
+
promptEnforcement: boolean;
|
|
138
|
+
/** Summarize the full session transcript into quirks at session end. */
|
|
139
|
+
sessionEndExtraction: boolean;
|
|
140
|
+
/** Max quirks to auto-capture per turn/session-end pass. */
|
|
141
|
+
autoCaptureMaxPerTurn: number;
|
|
142
|
+
/** Lexical similarity threshold (0-1) above which a candidate is considered a duplicate and skipped. */
|
|
143
|
+
autoCaptureDedupThreshold: number;
|
|
144
|
+
/** Decay settings for aging quirks. */
|
|
145
|
+
decay: {
|
|
146
|
+
/** Whether confidence decays over time. */
|
|
147
|
+
enabled: boolean;
|
|
148
|
+
/** Number of days after which confidence halves. */
|
|
149
|
+
halfLifeDays: number;
|
|
150
|
+
};
|
|
151
|
+
}
|
|
124
152
|
/** Configuration for the terminal UI (TUI) keybindings. */
|
|
125
153
|
export interface TuiConfig {
|
|
126
154
|
/** Keybinding to toggle the file list panel. */
|
|
@@ -270,6 +298,8 @@ export interface RagConfig {
|
|
|
270
298
|
mcp?: McpConfig;
|
|
271
299
|
/** Auto-update checking config. */
|
|
272
300
|
autoUpdate?: AutoUpdateConfig;
|
|
301
|
+
/** Quirk memory config for experiential agent memory. */
|
|
302
|
+
memory?: MemoryConfig;
|
|
273
303
|
/** Web dashboard UI config. */
|
|
274
304
|
ui?: UiConfig;
|
|
275
305
|
/** Terminal UI keybinding config. */
|
package/dist/core/config.js
CHANGED
|
@@ -184,7 +184,7 @@ export const DEFAULT_CONFIG = {
|
|
|
184
184
|
"Return your changes as a list of file paths with the full new content of the comment block for each modified symbol. Do NOT output the entire file unless asked.",
|
|
185
185
|
},
|
|
186
186
|
wikiMode: {
|
|
187
|
-
enabled:
|
|
187
|
+
enabled: true,
|
|
188
188
|
systemPrompt: "You are a wiki maintainer for this codebase. You incrementally build and maintain " +
|
|
189
189
|
"a persistent knowledge wiki at `.opencode/wiki/` — a structured, interlinked collection " +
|
|
190
190
|
"of markdown files that synthesizes knowledge from the codebase, documentation, and your conversations.\n\n" +
|
|
@@ -253,6 +253,21 @@ export const DEFAULT_CONFIG = {
|
|
|
253
253
|
cooldownMs: 3_600_000,
|
|
254
254
|
maxConsecutiveFailures: 3,
|
|
255
255
|
},
|
|
256
|
+
memory: {
|
|
257
|
+
enabled: true,
|
|
258
|
+
autoInject: false,
|
|
259
|
+
minConfidence: 0.5,
|
|
260
|
+
recallMinScore: 0.72,
|
|
261
|
+
passiveCapture: false,
|
|
262
|
+
promptEnforcement: true,
|
|
263
|
+
sessionEndExtraction: true,
|
|
264
|
+
autoCaptureMaxPerTurn: 2,
|
|
265
|
+
autoCaptureDedupThreshold: 0.85,
|
|
266
|
+
decay: {
|
|
267
|
+
enabled: false,
|
|
268
|
+
halfLifeDays: 30,
|
|
269
|
+
},
|
|
270
|
+
},
|
|
256
271
|
ui: {
|
|
257
272
|
port: 3210,
|
|
258
273
|
openBrowser: true,
|
|
@@ -279,7 +294,7 @@ export function validateConfig(config) {
|
|
|
279
294
|
const KNOWN_TOP_KEYS = new Set([
|
|
280
295
|
"embedding", "indexing", "vectorStore", "retrieval",
|
|
281
296
|
"openCode", "chunkers", "chunking", "description",
|
|
282
|
-
"imageDescription", "documentationMode", "wikiMode", "mcp", "autoUpdate", "ui", "tui", "logging",
|
|
297
|
+
"imageDescription", "documentationMode", "wikiMode", "mcp", "autoUpdate", "memory", "ui", "tui", "logging",
|
|
283
298
|
]);
|
|
284
299
|
const topKeys = new Set(Object.keys(config));
|
|
285
300
|
for (const key of topKeys) {
|
|
@@ -330,6 +345,11 @@ export function validateConfig(config) {
|
|
|
330
345
|
warnings.push("retrieval.hybridSearch.keywordWeight must be between 0 and 1");
|
|
331
346
|
}
|
|
332
347
|
}
|
|
348
|
+
if (config.memory?.recallMinScore != null) {
|
|
349
|
+
const r = config.memory.recallMinScore;
|
|
350
|
+
if (r < 0 || r > 1)
|
|
351
|
+
warnings.push("memory.recallMinScore must be between 0 and 1");
|
|
352
|
+
}
|
|
333
353
|
if (config.openCode.maxContextChunks <= 0) {
|
|
334
354
|
warnings.push("openCode.maxContextChunks must be > 0");
|
|
335
355
|
}
|
|
@@ -477,6 +497,10 @@ export function loadConfig(filePath, validate = true) {
|
|
|
477
497
|
...DEFAULT_CONFIG.autoUpdate,
|
|
478
498
|
...(safeObj(parsed.autoUpdate) ?? {}),
|
|
479
499
|
},
|
|
500
|
+
memory: {
|
|
501
|
+
...DEFAULT_CONFIG.memory,
|
|
502
|
+
...(safeObj(parsed.memory) ?? {}),
|
|
503
|
+
},
|
|
480
504
|
ui: {
|
|
481
505
|
...DEFAULT_CONFIG.ui,
|
|
482
506
|
...(safeObj(parsed.ui) ?? {}),
|
|
@@ -14,6 +14,16 @@ export interface Chunk {
|
|
|
14
14
|
endLine: number;
|
|
15
15
|
language: string;
|
|
16
16
|
contentType?: string;
|
|
17
|
+
/** Synthetic chunk kind — e.g. "quirk" for experiential memory entries. */
|
|
18
|
+
kind?: string;
|
|
19
|
+
/** Quirk sub-type when kind === "quirk": gotcha, preference, decision, environment-constraint. */
|
|
20
|
+
quirkType?: string;
|
|
21
|
+
/** Arbitrary tags for filtering/queries. */
|
|
22
|
+
tags?: string[];
|
|
23
|
+
/** Confidence score 0-1 (quirk memory). */
|
|
24
|
+
confidence?: number;
|
|
25
|
+
/** ISO timestamp of last confirmation (quirk memory). */
|
|
26
|
+
lastObserved?: string;
|
|
17
27
|
};
|
|
18
28
|
}
|
|
19
29
|
/** Logger interface for description generation progress messages. */
|
|
@@ -42,6 +52,13 @@ export interface DescriptionProvider {
|
|
|
42
52
|
* @param opts - Optional progress reporting options.
|
|
43
53
|
*/
|
|
44
54
|
generateBatchDescriptions(chunks: Chunk[], logger?: DescriptionLogger, opts?: BatchDescriptionOptions): Promise<Map<string, string>>;
|
|
55
|
+
/**
|
|
56
|
+
* Generate free-form text from a system prompt and user message.
|
|
57
|
+
* Used by auto-capture to extract quirks from agent transcripts.
|
|
58
|
+
*/
|
|
59
|
+
generateText(system: string, user: string, opts?: {
|
|
60
|
+
timeoutMs?: number;
|
|
61
|
+
}): Promise<string>;
|
|
45
62
|
}
|
|
46
63
|
/** Explains how a search result score was computed, including vector and keyword contributions. */
|
|
47
64
|
export interface SearchExplanation {
|
|
@@ -130,6 +147,9 @@ export interface ChunkSummary {
|
|
|
130
147
|
endLine: number;
|
|
131
148
|
content: string;
|
|
132
149
|
description: string;
|
|
150
|
+
kind: string;
|
|
151
|
+
quirkType: string;
|
|
152
|
+
tags: string;
|
|
133
153
|
}
|
|
134
154
|
/** A file summary for list operations. */
|
|
135
155
|
export interface FileSummary {
|
|
@@ -163,13 +183,17 @@ export interface VectorStore {
|
|
|
163
183
|
getChunksByFilePath(filePath: string): Promise<Chunk[]>;
|
|
164
184
|
/** Re-open the store, optionally pointing at a new database path. */
|
|
165
185
|
reopen?(newPath?: string): Promise<void>;
|
|
186
|
+
/** Compact fragments and prune old versions to prevent version-manifest accumulation. */
|
|
187
|
+
optimize?(): Promise<void>;
|
|
166
188
|
}
|
|
167
|
-
/** Filter criteria for narrowing search results by file path
|
|
189
|
+
/** Filter criteria for narrowing search results by file path, language, or kind. */
|
|
168
190
|
export interface MetadataFilter {
|
|
169
191
|
/** Glob-style path patterns (e.g. "src/**", "lib/auth/*"). */
|
|
170
192
|
pathPatterns?: string[];
|
|
171
193
|
/** Language identifiers (e.g. ["typescript", "tsx"]). */
|
|
172
194
|
languages?: string[];
|
|
195
|
+
/** Synthetic kind filters (e.g. ["quirk"]). */
|
|
196
|
+
kinds?: string[];
|
|
173
197
|
}
|
|
174
198
|
/** Callback interface for reporting indexing progress to the UI or CLI. */
|
|
175
199
|
export interface IndexProgress {
|
|
@@ -42,6 +42,13 @@ export interface RuntimeOverrides {
|
|
|
42
42
|
model?: string;
|
|
43
43
|
provider?: string;
|
|
44
44
|
};
|
|
45
|
+
memory?: {
|
|
46
|
+
passiveCapture?: boolean;
|
|
47
|
+
promptEnforcement?: boolean;
|
|
48
|
+
sessionEndExtraction?: boolean;
|
|
49
|
+
autoCaptureMaxPerTurn?: number;
|
|
50
|
+
autoCaptureDedupThreshold?: number;
|
|
51
|
+
};
|
|
45
52
|
tui?: {
|
|
46
53
|
fileListKeybinding?: string;
|
|
47
54
|
chunksKeybinding?: string;
|