opencode-rag-plugin 1.17.3 → 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.
@@ -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.
@@ -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. */
@@ -270,6 +288,8 @@ export interface RagConfig {
270
288
  mcp?: McpConfig;
271
289
  /** Auto-update checking config. */
272
290
  autoUpdate?: AutoUpdateConfig;
291
+ /** Quirk memory config for experiential agent memory. */
292
+ memory?: MemoryConfig;
273
293
  /** Web dashboard UI config. */
274
294
  ui?: UiConfig;
275
295
  /** Terminal UI keybinding config. */
@@ -253,6 +253,16 @@ 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
+ decay: {
262
+ enabled: false,
263
+ halfLifeDays: 30,
264
+ },
265
+ },
256
266
  ui: {
257
267
  port: 3210,
258
268
  openBrowser: true,
@@ -279,7 +289,7 @@ export function validateConfig(config) {
279
289
  const KNOWN_TOP_KEYS = new Set([
280
290
  "embedding", "indexing", "vectorStore", "retrieval",
281
291
  "openCode", "chunkers", "chunking", "description",
282
- "imageDescription", "documentationMode", "wikiMode", "mcp", "autoUpdate", "ui", "tui", "logging",
292
+ "imageDescription", "documentationMode", "wikiMode", "mcp", "autoUpdate", "memory", "ui", "tui", "logging",
283
293
  ]);
284
294
  const topKeys = new Set(Object.keys(config));
285
295
  for (const key of topKeys) {
@@ -330,6 +340,11 @@ export function validateConfig(config) {
330
340
  warnings.push("retrieval.hybridSearch.keywordWeight must be between 0 and 1");
331
341
  }
332
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
+ }
333
348
  if (config.openCode.maxContextChunks <= 0) {
334
349
  warnings.push("openCode.maxContextChunks must be > 0");
335
350
  }
@@ -477,6 +492,10 @@ export function loadConfig(filePath, validate = true) {
477
492
  ...DEFAULT_CONFIG.autoUpdate,
478
493
  ...(safeObj(parsed.autoUpdate) ?? {}),
479
494
  },
495
+ memory: {
496
+ ...DEFAULT_CONFIG.memory,
497
+ ...(safeObj(parsed.memory) ?? {}),
498
+ },
480
499
  ui: {
481
500
  ...DEFAULT_CONFIG.ui,
482
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