opencode-rag-plugin 1.17.2 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ReadMe.md CHANGED
@@ -125,14 +125,16 @@ The agent maintains all wiki pages during normal coding sessions (ingest on lear
125
125
 
126
126
  See [Plugin documentation](doc/plugin.md#6-wiki-mode--slash-command-wiki) for the full protocol.
127
127
 
128
- ## MCP Server
128
+ ## MCP Server (Optional)
129
129
 
130
- 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, OpenCode, Cursor, etc.).
130
+ 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.).
131
131
 
132
132
  ```bash
133
133
  opencode-rag mcp
134
134
  ```
135
135
 
136
+ > **Note:** The MCP server is **optional**. When running as an OpenCode plugin, the four tools are registered **in-process** and work without the MCP server. The `chat.message` hook for hotkey injection also runs in-process. The MCP server is only needed when an **external** MCP client connects to OpenCodeRAG. The plugin auto-starts the server only if `mcp.enabled` is `true` in your config (default: `false`).
137
+
136
138
  ### MCP Tools
137
139
 
138
140
  | Tool | Description |
@@ -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
@@ -165,6 +165,8 @@ export function generateSkillFile() {
165
165
  "3. User mentions a function/class/variable to edit → `find_usages(symbolName)` THEN `search_semantic` THEN `edit`",
166
166
  "4. User asks a code question → `search_semantic` to gather context before answering",
167
167
  "5. User asks about an image or visual asset → `describe_image(filePath)` to retrieve its generated description, then optionally `search_semantic` for related code",
168
+ "6. You encounter an error or need a known pitfall → `recall_quirks(query)`",
169
+ "7. You discover a non-obvious fact or workaround → `add_quirk(content)` to persist it",
168
170
  "",
169
171
  "### When to use each tool",
170
172
  "",
@@ -174,6 +176,8 @@ export function generateSkillFile() {
174
176
  "| `get_file_skeleton` | You have a file path but need to orient before reading | `\"src/plugin.ts\"` |",
175
177
  "| `find_usages` | Before editing any function, class, or variable — check all call sites | `\"createRagHooks\"` |",
176
178
  "| `describe_image` | When the user refers to an image or asks \"what's in this screenshot/diagram?\" | `\"assets/login-screen.png\"` |",
179
+ "| `recall_quirks` | You hit an error or need to remember a gotcha, preference, or decision from past sessions | `\"lancedb type casting\"` |",
180
+ "| `add_quirk` | You just discovered a non-obvious fact, workaround, or convention worth remembering | `'\"npm needs --legacy-peer-deps\" --type gotcha --tag installation'` |",
177
181
  "",
178
182
  "### Workflow",
179
183
  "",
@@ -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
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.
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @fileoverview Persisted state for the background auto-update mechanism.
3
+ * Tracks when an install was last attempted, for which version,
4
+ * and consecutive failure counts to implement cooldown and backoff.
5
+ */
6
+ /** Persisted state of a background auto-update attempt. */
7
+ export interface AutoUpdateState {
8
+ /** Epoch ms of the most recent attempt. */
9
+ lastAttemptAt: number;
10
+ /** The latestVersion we tried to install (or checked). */
11
+ lastAttemptedVersion: string;
12
+ /** Outcome of the last attempt. */
13
+ lastResult: "success" | "failure";
14
+ /** Consecutive failures so far (reset on success). */
15
+ consecutiveFailures: number;
16
+ }
17
+ /** Full path to the state file for a given store path. */
18
+ export declare function statePath(storePath: string): string;
19
+ /** Load persisted auto-update state, or return null if missing / corrupt. */
20
+ export declare function loadAutoUpdateState(storePath: string): AutoUpdateState | null;
21
+ /** Persist auto-update state to disk (best-effort, never throws). */
22
+ export declare function saveAutoUpdateState(storePath: string, state: AutoUpdateState): void;
23
+ /**
24
+ * Determine whether an auto-install attempt should proceed, based on
25
+ * the persisted state, the version we just discovered, and configured
26
+ * limits.
27
+ *
28
+ * @returns An object with `attempt: boolean` and `reason: string`.
29
+ */
30
+ export declare function shouldAttemptInstall(state: AutoUpdateState | null, latestVersion: string, cooldownMs: number, maxConsecutiveFailures: number): {
31
+ attempt: boolean;
32
+ reason: string;
33
+ };
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @fileoverview Persisted state for the background auto-update mechanism.
3
+ * Tracks when an install was last attempted, for which version,
4
+ * and consecutive failure counts to implement cooldown and backoff.
5
+ */
6
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
7
+ import path from "node:path";
8
+ /** Name of the state file written to the store path. */
9
+ const STATE_FILE_NAME = ".auto-update-state.json";
10
+ /** Full path to the state file for a given store path. */
11
+ export function statePath(storePath) {
12
+ return path.join(storePath, STATE_FILE_NAME);
13
+ }
14
+ /** Load persisted auto-update state, or return null if missing / corrupt. */
15
+ export function loadAutoUpdateState(storePath) {
16
+ try {
17
+ const p = statePath(storePath);
18
+ if (!existsSync(p))
19
+ return null;
20
+ const raw = readFileSync(p, "utf-8");
21
+ const parsed = JSON.parse(raw);
22
+ if (typeof parsed.lastAttemptAt !== "number" ||
23
+ typeof parsed.lastAttemptedVersion !== "string" ||
24
+ (parsed.lastResult !== "success" && parsed.lastResult !== "failure") ||
25
+ typeof parsed.consecutiveFailures !== "number") {
26
+ return null;
27
+ }
28
+ return parsed;
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ /** Persist auto-update state to disk (best-effort, never throws). */
35
+ export function saveAutoUpdateState(storePath, state) {
36
+ try {
37
+ writeFileSync(statePath(storePath), JSON.stringify(state, null, 2), "utf-8");
38
+ }
39
+ catch {
40
+ /* best-effort */
41
+ }
42
+ }
43
+ /**
44
+ * Determine whether an auto-install attempt should proceed, based on
45
+ * the persisted state, the version we just discovered, and configured
46
+ * limits.
47
+ *
48
+ * @returns An object with `attempt: boolean` and `reason: string`.
49
+ */
50
+ export function shouldAttemptInstall(state, latestVersion, cooldownMs, maxConsecutiveFailures) {
51
+ if (!state) {
52
+ return { attempt: true, reason: "first run" };
53
+ }
54
+ const elapsed = Date.now() - state.lastAttemptAt;
55
+ if (state.lastAttemptedVersion === latestVersion && elapsed < cooldownMs) {
56
+ return {
57
+ attempt: false,
58
+ reason: `version ${latestVersion} already attempted ${Math.round(elapsed / 1000)}s ago (cooldown ${Math.round(cooldownMs / 1000)}s)`,
59
+ };
60
+ }
61
+ if (state.lastResult === "failure" && state.consecutiveFailures >= maxConsecutiveFailures && elapsed < cooldownMs) {
62
+ return {
63
+ attempt: false,
64
+ reason: `${state.consecutiveFailures} consecutive failures within cooldown window`,
65
+ };
66
+ }
67
+ return { attempt: true, reason: "proceeding" };
68
+ }
69
+ //# sourceMappingURL=auto-update-state.js.map
@@ -121,6 +121,24 @@ 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
+ /** Decay settings for aging quirks. */
135
+ decay: {
136
+ /** Whether confidence decays over time. */
137
+ enabled: boolean;
138
+ /** Number of days after which confidence halves. */
139
+ halfLifeDays: number;
140
+ };
141
+ }
124
142
  /** Configuration for the terminal UI (TUI) keybindings. */
125
143
  export interface TuiConfig {
126
144
  /** Keybinding to toggle the file list panel. */
@@ -128,15 +146,21 @@ export interface TuiConfig {
128
146
  /** Keybinding to toggle the chunk viewer panel. */
129
147
  chunksKeybinding: string;
130
148
  }
131
- /** Configuration for MCP (Model Context Protocol) server integration. */
149
+ /** Configuration for the standalone MCP (Model Context Protocol) server. */
132
150
  export interface McpConfig {
133
- /** Whether the MCP server is enabled. */
151
+ /** Whether to auto-start the MCP server on plugin load. Default false — agent tools run in-process regardless. Set true only when an external MCP client needs to connect via `opencode-rag mcp`. */
134
152
  enabled: boolean;
135
153
  }
136
154
  /** Configuration for automatic self-updates. */
137
155
  export interface AutoUpdateConfig {
138
156
  /** Whether update checking is enabled (defaults to true). */
139
157
  enabled: boolean;
158
+ /** When true, automatically install a newer release on startup (default false). */
159
+ autoInstall?: boolean;
160
+ /** Cooldown window (ms) before re-attempting the same version (default 3 600 000 = 1h). */
161
+ cooldownMs?: number;
162
+ /** Back off after this many consecutive failures (default 3). */
163
+ maxConsecutiveFailures?: number;
140
164
  }
141
165
  /** Configuration for post-retrieval context window optimization (adjacent merge, similarity dedup, file diversity cap). */
142
166
  export interface ContextOptimizationConfig {
@@ -264,6 +288,8 @@ export interface RagConfig {
264
288
  mcp?: McpConfig;
265
289
  /** Auto-update checking config. */
266
290
  autoUpdate?: AutoUpdateConfig;
291
+ /** Quirk memory config for experiential agent memory. */
292
+ memory?: MemoryConfig;
267
293
  /** Web dashboard UI config. */
268
294
  ui?: UiConfig;
269
295
  /** Terminal UI keybinding config. */
@@ -245,10 +245,23 @@ export const DEFAULT_CONFIG = {
245
245
  "- The wiki is git-trackable in `.opencode/` — every edit is a diff",
246
246
  },
247
247
  mcp: {
248
- enabled: true,
248
+ enabled: false,
249
249
  },
250
250
  autoUpdate: {
251
251
  enabled: true,
252
+ autoInstall: false,
253
+ cooldownMs: 3_600_000,
254
+ maxConsecutiveFailures: 3,
255
+ },
256
+ memory: {
257
+ enabled: true,
258
+ autoInject: false,
259
+ minConfidence: 0.5,
260
+ recallMinScore: 0.72,
261
+ decay: {
262
+ enabled: false,
263
+ halfLifeDays: 30,
264
+ },
252
265
  },
253
266
  ui: {
254
267
  port: 3210,
@@ -276,7 +289,7 @@ export function validateConfig(config) {
276
289
  const KNOWN_TOP_KEYS = new Set([
277
290
  "embedding", "indexing", "vectorStore", "retrieval",
278
291
  "openCode", "chunkers", "chunking", "description",
279
- "imageDescription", "documentationMode", "wikiMode", "mcp", "autoUpdate", "ui", "tui", "logging",
292
+ "imageDescription", "documentationMode", "wikiMode", "mcp", "autoUpdate", "memory", "ui", "tui", "logging",
280
293
  ]);
281
294
  const topKeys = new Set(Object.keys(config));
282
295
  for (const key of topKeys) {
@@ -327,6 +340,11 @@ export function validateConfig(config) {
327
340
  warnings.push("retrieval.hybridSearch.keywordWeight must be between 0 and 1");
328
341
  }
329
342
  }
343
+ if (config.memory?.recallMinScore != null) {
344
+ const r = config.memory.recallMinScore;
345
+ if (r < 0 || r > 1)
346
+ warnings.push("memory.recallMinScore must be between 0 and 1");
347
+ }
330
348
  if (config.openCode.maxContextChunks <= 0) {
331
349
  warnings.push("openCode.maxContextChunks must be > 0");
332
350
  }
@@ -474,6 +492,10 @@ export function loadConfig(filePath, validate = true) {
474
492
  ...DEFAULT_CONFIG.autoUpdate,
475
493
  ...(safeObj(parsed.autoUpdate) ?? {}),
476
494
  },
495
+ memory: {
496
+ ...DEFAULT_CONFIG.memory,
497
+ ...(safeObj(parsed.memory) ?? {}),
498
+ },
477
499
  ui: {
478
500
  ...DEFAULT_CONFIG.ui,
479
501
  ...(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. */
@@ -130,6 +140,9 @@ export interface ChunkSummary {
130
140
  endLine: number;
131
141
  content: string;
132
142
  description: string;
143
+ kind: string;
144
+ quirkType: string;
145
+ tags: string;
133
146
  }
134
147
  /** A file summary for list operations. */
135
148
  export interface FileSummary {
@@ -164,12 +177,14 @@ export interface VectorStore {
164
177
  /** Re-open the store, optionally pointing at a new database path. */
165
178
  reopen?(newPath?: string): Promise<void>;
166
179
  }
167
- /** Filter criteria for narrowing search results by file path patterns or language. */
180
+ /** Filter criteria for narrowing search results by file path, language, or kind. */
168
181
  export interface MetadataFilter {
169
182
  /** Glob-style path patterns (e.g. "src/**", "lib/auth/*"). */
170
183
  pathPatterns?: string[];
171
184
  /** Language identifiers (e.g. ["typescript", "tsx"]). */
172
185
  languages?: string[];
186
+ /** Synthetic kind filters (e.g. ["quirk"]). */
187
+ kinds?: string[];
173
188
  }
174
189
  /** Callback interface for reporting indexing progress to the UI or CLI. */
175
190
  export interface IndexProgress {
@@ -67,3 +67,41 @@ export declare function createDescribeImageTool(options: DescribeImageToolOption
67
67
  * @returns A tool definition suitable for OpenCode plugin registration.
68
68
  */
69
69
  export declare function createFindUsagesTool(options: FindUsagesToolOptions): ToolDefinition;
70
+ /** Options for creating the `recall_quirks` tool. */
71
+ export interface RecallQuirksToolOptions {
72
+ store: VectorStore;
73
+ embedder: EmbeddingProvider;
74
+ cfg: RagConfig;
75
+ keywordIndex: KeywordIndex;
76
+ storePath: string;
77
+ }
78
+ /**
79
+ * Create the `recall_quirks` tool.
80
+ *
81
+ * Queries experiential quirk memory (gotchas, preferences, decisions,
82
+ * environment constraints) via hybrid vector-keyword search filtered to
83
+ * `kind === "quirk"`. Results are re-weighted by confidence.
84
+ *
85
+ * @param options - Store, embedder, config, keyword index, store path.
86
+ * @returns A tool definition suitable for OpenCode plugin registration.
87
+ */
88
+ export declare function createRecallQuirksTool(options: RecallQuirksToolOptions): ToolDefinition;
89
+ /** Options for creating the `add_quirk` tool. */
90
+ export interface AddQuirkToolOptions {
91
+ store: VectorStore;
92
+ embedder: EmbeddingProvider;
93
+ cfg: RagConfig;
94
+ keywordIndex: KeywordIndex;
95
+ storePath: string;
96
+ }
97
+ /**
98
+ * Create the `add_quirk` tool.
99
+ *
100
+ * Stores a new quirk into experiential memory. The quirk is embedded as a
101
+ * synthetic chunk (kind="quirk") in the vector store and indexed in the
102
+ * keyword index. An audit trail is written to quirks.jsonl.
103
+ *
104
+ * @param options - Store, embedder, config, keyword index, store path.
105
+ * @returns A tool definition suitable for OpenCode plugin registration.
106
+ */
107
+ export declare function createAddQuirkTool(options: AddQuirkToolOptions): ToolDefinition;
@@ -20,6 +20,7 @@ import { Parser } from "web-tree-sitter";
20
20
  import { initParser, loadLanguage, walkTree } from "../chunker/grammar.js";
21
21
  import { readFileSync } from "node:fs";
22
22
  import { resolveWorkspacePath } from "./tool-args.js";
23
+ import { addQuirk, recallQuirks } from "../quirks/quirk-store.js";
23
24
  const SKELETON_CONFIGS = {
24
25
  ".ts": { grammarName: "typescript", nodeTypes: ["function_declaration", "method_definition", "class_declaration", "interface_declaration", "type_alias_declaration", "enum_declaration"] },
25
26
  ".tsx": { grammarName: "typescript", nodeTypes: ["function_declaration", "method_definition", "class_declaration", "interface_declaration", "type_alias_declaration", "enum_declaration", "arrow_function"] },
@@ -493,4 +494,130 @@ export function createFindUsagesTool(options) {
493
494
  },
494
495
  });
495
496
  }
497
+ /**
498
+ * Create the `recall_quirks` tool.
499
+ *
500
+ * Queries experiential quirk memory (gotchas, preferences, decisions,
501
+ * environment constraints) via hybrid vector-keyword search filtered to
502
+ * `kind === "quirk"`. Results are re-weighted by confidence.
503
+ *
504
+ * @param options - Store, embedder, config, keyword index, store path.
505
+ * @returns A tool definition suitable for OpenCode plugin registration.
506
+ */
507
+ export function createRecallQuirksTool(options) {
508
+ const { store, embedder, cfg, keywordIndex, storePath } = options;
509
+ return tool({
510
+ description: "Query experiential quirk memory — gotchas, preferences, decisions, and " +
511
+ "environment constraints discovered during previous sessions. " +
512
+ "Leverage when you encounter an error, need to recall how something works, " +
513
+ "or want to avoid known pitfalls. Results are filtered by confidence and " +
514
+ "relevance.",
515
+ args: {
516
+ query: tool.schema.string().min(1, "A search query is required."),
517
+ topK: tool.schema.number().int().min(1).max(25).optional(),
518
+ quirkType: tool.schema.string().optional(),
519
+ tags: tool.schema.array(tool.schema.string().min(1)).max(10).optional(),
520
+ },
521
+ async execute(args) {
522
+ try {
523
+ const deps = { embedder, store, keywordIndex, cfg, storePath };
524
+ const results = await recallQuirks(deps, args.query, {
525
+ topK: args.topK ?? 10,
526
+ quirkType: args.quirkType,
527
+ tags: args.tags,
528
+ });
529
+ if (results.length === 0) {
530
+ return {
531
+ title: "Quirk memory recall",
532
+ output: "No quirks matched your query.",
533
+ metadata: { tool: "recall_quirks", query: args.query, matches: 0 },
534
+ };
535
+ }
536
+ const lines = [];
537
+ lines.push(`**${results.length} quirk(s) found:**\n`);
538
+ for (const r of results) {
539
+ const m = r.chunk.metadata;
540
+ const badge = m.quirkType ? `[\`${m.quirkType}\`] ` : "";
541
+ const tags = m.tags && m.tags.length > 0 ? ` _(${m.tags.join(", ")})_` : "";
542
+ const confidence = m.confidence != null
543
+ ? ` confidence=${(m.confidence * 100).toFixed(0)}%`
544
+ : "";
545
+ const score = r.score.toFixed(3);
546
+ lines.push(`- ${badge}${r.chunk.content}${tags}`, ` _score=${score}${confidence}_`);
547
+ }
548
+ return {
549
+ title: `Quirk memory recall (${results.length})`,
550
+ output: lines.join("\n"),
551
+ metadata: {
552
+ tool: "recall_quirks",
553
+ query: args.query,
554
+ matches: results.length,
555
+ },
556
+ };
557
+ }
558
+ catch (err) {
559
+ return {
560
+ title: "Quirk memory recall",
561
+ output: `Recall failed: ${err instanceof Error ? err.message : String(err)}`,
562
+ metadata: { tool: "recall_quirks", error: String(err) },
563
+ };
564
+ }
565
+ },
566
+ });
567
+ }
568
+ /**
569
+ * Create the `add_quirk` tool.
570
+ *
571
+ * Stores a new quirk into experiential memory. The quirk is embedded as a
572
+ * synthetic chunk (kind="quirk") in the vector store and indexed in the
573
+ * keyword index. An audit trail is written to quirks.jsonl.
574
+ *
575
+ * @param options - Store, embedder, config, keyword index, store path.
576
+ * @returns A tool definition suitable for OpenCode plugin registration.
577
+ */
578
+ export function createAddQuirkTool(options) {
579
+ const { store, embedder, cfg, keywordIndex, storePath } = options;
580
+ return tool({
581
+ description: "Store a new experiential memory (quirk) — a gotcha, preference, decision, or " +
582
+ "environment constraint you discovered. The quirk is embedded and indexed so " +
583
+ "future sessions can recall it via `recall_quirks`. Use when you hit a " +
584
+ "non-obvious behavior, workaround, or coding convention.",
585
+ args: {
586
+ content: tool.schema.string().min(1, "Quirk content is required."),
587
+ quirkType: tool.schema
588
+ .string()
589
+ .optional(),
590
+ tags: tool.schema.array(tool.schema.string().min(1)).max(10).optional(),
591
+ sourceRef: tool.schema.string().optional(),
592
+ },
593
+ async execute(args) {
594
+ try {
595
+ const deps = { embedder, store, keywordIndex, cfg, storePath };
596
+ const quirk = await addQuirk(deps, {
597
+ content: args.content,
598
+ quirkType: args.quirkType,
599
+ tags: args.tags,
600
+ sourceRef: args.sourceRef,
601
+ });
602
+ return {
603
+ title: "Quirk added",
604
+ output: `**Quirk recorded** (id: \`${quirk.id}\`)\n\`${args.quirkType ?? "general"}\` | confidence=${(quirk.confidence * 100).toFixed(0)}% | tags=${(quirk.tags ?? []).join(", ") || "none"}`,
605
+ metadata: {
606
+ tool: "add_quirk",
607
+ quirkId: quirk.id,
608
+ quirkType: quirk.quirkType,
609
+ confidence: quirk.confidence,
610
+ },
611
+ };
612
+ }
613
+ catch (err) {
614
+ return {
615
+ title: "Quirk add",
616
+ output: `Failed to add quirk: ${err instanceof Error ? err.message : String(err)}`,
617
+ metadata: { tool: "add_quirk", error: String(err) },
618
+ };
619
+ }
620
+ },
621
+ });
622
+ }
496
623
  //# sourceMappingURL=tools.js.map