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.
Files changed (49) hide show
  1. package/ReadMe.md +41 -1
  2. package/dist/cli/commands/index.d.ts +1 -0
  3. package/dist/cli/commands/index.js +1 -0
  4. package/dist/cli/commands/init-helpers.d.ts +5 -1
  5. package/dist/cli/commands/init-helpers.js +12 -19
  6. package/dist/cli/commands/init.js +42 -8
  7. package/dist/cli/commands/quirk.d.ts +10 -0
  8. package/dist/cli/commands/quirk.js +141 -0
  9. package/dist/cli/commands/setup.js +15 -6
  10. package/dist/cli/commands/ui.js +1 -1
  11. package/dist/cli/index.js +2 -1
  12. package/dist/core/config.d.ts +30 -0
  13. package/dist/core/config.js +26 -2
  14. package/dist/core/interfaces.d.ts +25 -1
  15. package/dist/core/runtime-overrides.d.ts +7 -0
  16. package/dist/core/runtime-overrides.js +15 -0
  17. package/dist/describer/anthropic.d.ts +4 -0
  18. package/dist/describer/anthropic.js +9 -2
  19. package/dist/describer/describer.d.ts +4 -0
  20. package/dist/describer/describer.js +8 -0
  21. package/dist/describer/gemini.d.ts +3 -0
  22. package/dist/describer/gemini.js +8 -2
  23. package/dist/indexer/pipeline.js +40 -0
  24. package/dist/opencode/system-guidance.d.ts +34 -0
  25. package/dist/opencode/system-guidance.js +127 -0
  26. package/dist/opencode/tools.d.ts +38 -0
  27. package/dist/opencode/tools.js +127 -0
  28. package/dist/plugin.js +204 -31
  29. package/dist/quirks/auto-capture.d.ts +14 -0
  30. package/dist/quirks/auto-capture.js +112 -0
  31. package/dist/quirks/monitor.d.ts +5 -0
  32. package/dist/quirks/monitor.js +23 -0
  33. package/dist/quirks/prompts.d.ts +1 -0
  34. package/dist/quirks/prompts.js +13 -0
  35. package/dist/quirks/quirk-store.d.ts +27 -0
  36. package/dist/quirks/quirk-store.js +217 -0
  37. package/dist/quirks/types.d.ts +20 -0
  38. package/dist/quirks/types.js +2 -0
  39. package/dist/retriever/keyword-index.js +16 -0
  40. package/dist/vectorstore/lancedb.d.ts +21 -11
  41. package/dist/vectorstore/lancedb.js +174 -18
  42. package/dist/vectorstore/memory.d.ts +2 -0
  43. package/dist/vectorstore/memory.js +6 -0
  44. package/dist/web/api.d.ts +3 -1
  45. package/dist/web/api.js +50 -1
  46. package/dist/web/server.d.ts +2 -1
  47. package/dist/web/server.js +3 -2
  48. package/dist/web/ui/index.html +163 -0
  49. 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): 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 `${AGENTS_MD_SECTION}\n`;
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}${AGENTS_MD_SECTION}${after}`;
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}${AGENTS_MD_SECTION}\n`;
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
- const nextAgentsMd = mergeAgentsMdContent(agentsMdExists ? readFileSync(agentsMdPath, "utf-8") : undefined);
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 || options.force) {
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(` ${configExists ? c.updated("Updated:") : c.created("Created:")} opencode-rag.json`);
191
+ console.log(` ${c.created("Created:")} opencode-rag.json`);
180
192
  }
181
193
  else {
182
- console.log(` ${c.exists("Exists:")} opencode-rag.json`);
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
- const pids = execSync("pgrep -x opencode 2>/dev/null || (Get-Process -Name opencode -ErrorAction SilentlyContinue 2>nul | ForEach-Object { $_.Id })", {
16
- encoding: "utf-8",
17
- timeout: 3_000,
18
- }).trim();
19
- if (pids) {
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
- // pgrep/Get-Process aren't available or succeeded silently
33
+ // OpenCode not found or tools unavailable
25
34
  }
26
35
  }
27
36
  export function registerSetupCommand(program) {
@@ -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.
@@ -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. */
@@ -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: false,
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 patterns or language. */
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;