opencode-rag-plugin 1.19.2 → 1.19.4
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 +5 -1
- package/dist/api.d.ts +1 -1
- package/dist/api.js +2 -0
- package/dist/cli/commands/init-helpers.js +7 -0
- package/dist/cli/commands/query.js +2 -0
- package/dist/cli/commands/quirk.js +35 -1
- package/dist/content/reader.js +10 -9
- package/dist/core/interfaces.d.ts +8 -0
- package/dist/core/interfaces.js +8 -1
- package/dist/mcp/handlers.d.ts +1 -1
- package/dist/mcp/handlers.js +7 -1
- package/dist/opencode/create-read-tool.d.ts +1 -1
- package/dist/opencode/create-read-tool.js +3 -2
- package/dist/opencode/system-guidance.js +11 -3
- package/dist/opencode/tools.d.ts +39 -1
- package/dist/opencode/tools.js +117 -2
- package/dist/plugin.d.ts +1 -1
- package/dist/plugin.js +38 -1
- package/dist/quirks/quirk-store.d.ts +14 -1
- package/dist/quirks/quirk-store.js +105 -1
- package/dist/web/api.d.ts +1 -1
- package/dist/web/api.js +13 -2
- package/package.json +1 -1
package/ReadMe.md
CHANGED
|
@@ -153,12 +153,16 @@ OpenCodeRAG gives your agent **persistent, cross-session memory** of non-obvious
|
|
|
153
153
|
|------|----------|
|
|
154
154
|
| `recall_quirks(query)` | You hit an error or need to remember a gotcha, preference, or decision from past sessions |
|
|
155
155
|
| `add_quirk(content, { type, tags })` | You just discovered a non-obvious fact, workaround, or convention worth remembering |
|
|
156
|
+
| `update_quirk(id, { content, type, tags })` | A recalled quirk is outdated or wrong — fix it instead of adding a duplicate |
|
|
157
|
+
| `delete_quirk(id)` | A quirk is fixed, obsolete, or no longer applies — remove it |
|
|
156
158
|
|
|
157
159
|
**CLI — manage quirks directly:**
|
|
158
160
|
|
|
159
161
|
```bash
|
|
160
162
|
opencode-rag quirk add "npm needs --legacy-peer-deps" --type gotcha --tag installation
|
|
161
163
|
opencode-rag quirk list
|
|
164
|
+
opencode-rag quirk update <id> --content "..." --type decision
|
|
165
|
+
opencode-rag quirk rm <id>
|
|
162
166
|
opencode-rag quirk lint # flag low-confidence / stale / duplicate quirks
|
|
163
167
|
opencode-rag quirk test "npm needs --legacy-peer-deps"
|
|
164
168
|
# ✓ Quirk has been appended:
|
|
@@ -166,7 +170,7 @@ opencode-rag quirk test "npm needs --legacy-peer-deps"
|
|
|
166
170
|
# 99% confidence
|
|
167
171
|
```
|
|
168
172
|
|
|
169
|
-
When `memory.autoInject` is `true`, the plugin checks for relevant quirks on every user message using the combined agent-response + user-query as the search query. Quirks are only injected when their relevance score exceeds the threshold — `recallMinScore` (default 0.72) for the user message, `autoInjectMinScore` (default 0.45) for the system prompt. A latency budget (`autoInjectLatencyBudgetMs`, default 2000ms) prevents slow embedders from blocking message processing. To avoid polluting the context window, each quirk is injected **at most once per session** — once recalled, it is filtered out from all subsequent auto-injections. 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).
|
|
173
|
+
When `memory.autoInject` is `true`, the plugin checks for relevant quirks on every user message using the combined agent-response + user-query as the search query. Quirks are only injected when their relevance score exceeds the threshold — `recallMinScore` (default 0.72) for the user message, `autoInjectMinScore` (default 0.45) for the system prompt. A latency budget (`autoInjectLatencyBudgetMs`, default 2000ms) prevents slow embedders from blocking message processing. To avoid polluting the context window, each quirk is injected **at most once per session** — once recalled, it is filtered out from all subsequent auto-injections. Every `add_quirk` and every content-changing `update_quirk` is vetted by an immutable trust monitor that rejects destructive patterns (e.g. `rm -rf`, `force push`, `bypass security`). Outdated or fixed quirks should be corrected with `update_quirk` / `delete_quirk` rather than left to contradict newer memory. See [Plugin documentation](doc/plugin.md#9-quirk-memory-experiential-memory) and [CLI Reference: `quirk`](doc/cli.md#quirk).
|
|
170
174
|
|
|
171
175
|
## MCP Server (Optional)
|
|
172
176
|
|
package/dist/api.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { type IndexRunStats } from "./indexer.js";
|
|
6
6
|
import { type WorkspaceFile } from "./content/reader.js";
|
|
7
|
-
import type
|
|
7
|
+
import { type SearchResult } from "./core/interfaces.js";
|
|
8
8
|
/** Options controlling a semantic search query. */
|
|
9
9
|
export interface SearchOptions {
|
|
10
10
|
/** Working directory to resolve relative paths against. */
|
package/dist/api.js
CHANGED
|
@@ -7,6 +7,7 @@ import { retrieve } from "./retriever/retriever.js";
|
|
|
7
7
|
import { optimizeContext, DEFAULT_CONTEXT_OPTIMIZATION } from "./retriever/context-optimizer.js";
|
|
8
8
|
import { runIndexPass } from "./indexer.js";
|
|
9
9
|
import { scanWorkspaceFiles } from "./content/reader.js";
|
|
10
|
+
import { CODE_SEARCH_FILTER } from "./core/interfaces.js";
|
|
10
11
|
import { destroyAllPooledConnections } from "./embedder/http.js";
|
|
11
12
|
/**
|
|
12
13
|
* Format a list of search results into a human-readable markdown block.
|
|
@@ -55,6 +56,7 @@ export async function search(query, options = {}) {
|
|
|
55
56
|
filter: {
|
|
56
57
|
pathPatterns: options.pathHints,
|
|
57
58
|
languages: options.languageHints,
|
|
59
|
+
kinds: CODE_SEARCH_FILTER.kinds,
|
|
58
60
|
},
|
|
59
61
|
});
|
|
60
62
|
const optCfg = ctx.config.retrieval.contextOptimization ?? DEFAULT_CONTEXT_OPTIMIZATION;
|
|
@@ -168,6 +168,7 @@ export function generateSkillFile() {
|
|
|
168
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
169
|
"6. You encounter an error or need a known pitfall → `recall_quirks(query)`",
|
|
170
170
|
"7. You discover a non-obvious fact or workaround → `add_quirk(content)` to persist it",
|
|
171
|
+
"8. A recalled quirk is outdated or wrong → `update_quirk(id, ...)` to fix it, or `delete_quirk(id)` if it no longer applies",
|
|
171
172
|
"",
|
|
172
173
|
"### When to use each tool",
|
|
173
174
|
"",
|
|
@@ -179,6 +180,8 @@ export function generateSkillFile() {
|
|
|
179
180
|
"| `describe_image` | When the user refers to an image or asks \"what's in this screenshot/diagram?\" | `\"assets/login-screen.png\"` |",
|
|
180
181
|
"| `recall_quirks` | You hit an error or need to remember a gotcha, preference, or decision from past sessions | `\"lancedb type casting\"` |",
|
|
181
182
|
"| `add_quirk` | You just discovered a non-obvious fact, workaround, or convention worth remembering | `'\"npm needs --legacy-peer-deps\" --type gotcha --tag installation'` |",
|
|
183
|
+
"| `update_quirk` | A recalled quirk is outdated or wrong — fix its content, type, or tags | `id` from `recall_quirks` output + `content: \"...\"` |",
|
|
184
|
+
"| `delete_quirk` | A quirk is fixed, obsolete, or no longer applies | `id` from `recall_quirks` output |",
|
|
182
185
|
"",
|
|
183
186
|
"### Workflow",
|
|
184
187
|
"",
|
|
@@ -203,6 +206,10 @@ export function generateSkillFile() {
|
|
|
203
206
|
"- `get_file_skeleton`: `filePath` (req)",
|
|
204
207
|
"- `find_usages`: `symbolName` (req), `pathHint?`, `topK?`",
|
|
205
208
|
"- `describe_image`: `filePath` (req)",
|
|
209
|
+
"- `recall_quirks`: `query` (req), `topK?`, `quirkType?`, `tags?`",
|
|
210
|
+
"- `add_quirk`: `content` (req), `quirkType?`, `tags?`, `sourceRef?`",
|
|
211
|
+
"- `update_quirk`: `id` (req) + at least one of `content?`, `quirkType?`, `tags?`, `confidence?`, `sourceRef?`",
|
|
212
|
+
"- `delete_quirk`: `id` (req)",
|
|
206
213
|
"",
|
|
207
214
|
"### Tips",
|
|
208
215
|
"",
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import pc from "picocolors";
|
|
9
|
+
import { CODE_SEARCH_FILTER } from "../../core/interfaces.js";
|
|
9
10
|
import { retrieve } from "../../retriever/retriever.js";
|
|
10
11
|
import { c, resolveCliContext, cleanupContext, logCliError, logCliInfo, formatDuration } from "../format.js";
|
|
11
12
|
import { optimizeContext, DEFAULT_CONTEXT_OPTIMIZATION } from "../../retriever/context-optimizer.js";
|
|
@@ -54,6 +55,7 @@ export function registerQueryCommand(program) {
|
|
|
54
55
|
hybridEnabled: hybridCfg?.enabled,
|
|
55
56
|
queryPrefix: config.embedding.queryPrefix,
|
|
56
57
|
explain: options.explain ?? false,
|
|
58
|
+
filter: CODE_SEARCH_FILTER,
|
|
57
59
|
});
|
|
58
60
|
const optCfg = config.retrieval.contextOptimization ?? DEFAULT_CONTEXT_OPTIMIZATION;
|
|
59
61
|
const results = optimizeContext(rawResults, { topK, config: optCfg });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveCliContext, cleanupContext, logCliInfo, logCliError, c } from "../format.js";
|
|
2
|
-
import { addQuirk, listQuirks, lintQuirks, recallQuirks, removeQuirk } from "../../quirks/quirk-store.js";
|
|
2
|
+
import { addQuirk, listQuirks, lintQuirks, recallQuirks, removeQuirk, updateQuirk } from "../../quirks/quirk-store.js";
|
|
3
3
|
/**
|
|
4
4
|
* Register the `quirk` command on the given Commander program.
|
|
5
5
|
*
|
|
@@ -37,6 +37,40 @@ export function registerQuirkCommand(program) {
|
|
|
37
37
|
process.exit(1);
|
|
38
38
|
}
|
|
39
39
|
});
|
|
40
|
+
quirkCmd
|
|
41
|
+
.command("update")
|
|
42
|
+
.description("Update a quirk by ID (content, type, tags, confidence, source ref)")
|
|
43
|
+
.argument("<id>", "quirk ID")
|
|
44
|
+
.option("--content <text>", "replacement quirk text")
|
|
45
|
+
.option("-t, --type <type>", "quirk type: gotcha, preference, decision, environment-constraint")
|
|
46
|
+
.option("--tag <tags...>", "replacement tags for filtering")
|
|
47
|
+
.option("--confidence <0-1>", "replacement confidence", parseFloat)
|
|
48
|
+
.option("--source-ref <path>", "source file path reference")
|
|
49
|
+
.option("-c, --config <path>", "path to config file")
|
|
50
|
+
.action(async (id, options) => {
|
|
51
|
+
try {
|
|
52
|
+
const ctx = await resolveCliContext(options, resolveLogPath());
|
|
53
|
+
const { config, embedder, store, keywordIndex } = ctx;
|
|
54
|
+
const tags = options.tag;
|
|
55
|
+
const quirk = await updateQuirk({ embedder, store, keywordIndex: keywordIndex, cfg: config, storePath: ctx.storePath }, id, {
|
|
56
|
+
content: options.content,
|
|
57
|
+
quirkType: options.type,
|
|
58
|
+
tags: tags ? (Array.isArray(tags) ? tags : [tags]) : undefined,
|
|
59
|
+
confidence: options.confidence,
|
|
60
|
+
sourceRef: options.sourceRef,
|
|
61
|
+
});
|
|
62
|
+
logCliInfo(ctx.logFilePath, "quirk update", `\n${c.success("Quirk updated:")}`);
|
|
63
|
+
logCliInfo(ctx.logFilePath, "quirk update", ` ${c.label("ID:")} ${quirk.id}`);
|
|
64
|
+
logCliInfo(ctx.logFilePath, "quirk update", ` ${c.label("Type:")} ${quirk.quirkType ?? "general"}`);
|
|
65
|
+
logCliInfo(ctx.logFilePath, "quirk update", ` ${c.label("Confidence:")} ${(quirk.confidence * 100).toFixed(0)}%`);
|
|
66
|
+
logCliInfo(ctx.logFilePath, "quirk update", ` ${c.label("Content:")} ${quirk.content}`);
|
|
67
|
+
await cleanupContext(ctx);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
logCliError(resolveLogPath(), "quirk update", `Failed to update quirk: ${err.message}`, err);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
40
74
|
quirkCmd
|
|
41
75
|
.command("list")
|
|
42
76
|
.description("List all quirks")
|
package/dist/content/reader.js
CHANGED
|
@@ -56,17 +56,17 @@ export async function walkFiles(dir, extensions, excludeDirs, excludeFiles, root
|
|
|
56
56
|
return results;
|
|
57
57
|
}
|
|
58
58
|
async function dispatchExtraction(filePath, buffer, imageVisionProvider, imagePrompt, resizeMaxDimension) {
|
|
59
|
-
const
|
|
60
|
-
if (pdfExtractor.PDF_EXTENSIONS.has(
|
|
59
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
60
|
+
if (pdfExtractor.PDF_EXTENSIONS.has(ext)) {
|
|
61
61
|
return pdfExtractor.extract(filePath, buffer);
|
|
62
62
|
}
|
|
63
|
-
if (docxExtractor.DOCX_EXTENSIONS.has(
|
|
63
|
+
if (docxExtractor.DOCX_EXTENSIONS.has(ext)) {
|
|
64
64
|
return docxExtractor.extract(filePath, buffer);
|
|
65
65
|
}
|
|
66
|
-
if (docExtractor.DOC_EXTENSIONS.has(
|
|
66
|
+
if (docExtractor.DOC_EXTENSIONS.has(ext)) {
|
|
67
67
|
return docExtractor.extract(filePath, buffer);
|
|
68
68
|
}
|
|
69
|
-
if (excelExtractor.EXCEL_EXTENSIONS.has(
|
|
69
|
+
if (excelExtractor.EXCEL_EXTENSIONS.has(ext)) {
|
|
70
70
|
return excelExtractor.extract(filePath, buffer);
|
|
71
71
|
}
|
|
72
72
|
if (imageVisionProvider && imageExtractor.isImageFile(filePath)) {
|
|
@@ -164,10 +164,11 @@ export async function scanWorkspaceFiles(cwd, config, logger, manifest, filterPa
|
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
const isImage = imageVisionProvider !== null && imageExtractor.isImageFile(filePath);
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
167
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
168
|
+
const isBinary = pdfExtractor.PDF_EXTENSIONS.has(ext) ||
|
|
169
|
+
docxExtractor.DOCX_EXTENSIONS.has(ext) ||
|
|
170
|
+
docExtractor.DOC_EXTENSIONS.has(ext) ||
|
|
171
|
+
excelExtractor.EXCEL_EXTENSIONS.has(ext) ||
|
|
171
172
|
isImage;
|
|
172
173
|
logger?.info(`Reading: ${filePath}`);
|
|
173
174
|
const buffer = isBinary ? await fs.readFile(filePath) : Buffer.alloc(0);
|
|
@@ -201,6 +201,14 @@ export interface MetadataFilter {
|
|
|
201
201
|
/** Synthetic kind filters (e.g. ["quirk"]). */
|
|
202
202
|
kinds?: string[];
|
|
203
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Filter for general code/document retrieval that excludes quirk memory
|
|
206
|
+
* chunks. Regular indexed chunks store `kind: ""`, quirk chunks `kind: "quirk"`.
|
|
207
|
+
* All code-search paths (search_semantic, hotkey injection, CLI query, web UI,
|
|
208
|
+
* read-tool context) must pass this so quirks never surface as code results —
|
|
209
|
+
* quirks are only reachable through recall_quirks / the quirk CLI.
|
|
210
|
+
*/
|
|
211
|
+
export declare const CODE_SEARCH_FILTER: MetadataFilter;
|
|
204
212
|
/** Callback interface for reporting indexing progress to the UI or CLI. */
|
|
205
213
|
export interface IndexProgress {
|
|
206
214
|
/** Set the total number of files to be indexed. */
|
package/dist/core/interfaces.js
CHANGED
|
@@ -2,5 +2,12 @@
|
|
|
2
2
|
* @fileoverview Core type definitions for the OpenCodeRAG pipeline: Chunk, SearchResult,
|
|
3
3
|
* Chunker, EmbeddingProvider, VectorStore, KeywordIndex, and related interfaces.
|
|
4
4
|
*/
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Filter for general code/document retrieval that excludes quirk memory
|
|
7
|
+
* chunks. Regular indexed chunks store `kind: ""`, quirk chunks `kind: "quirk"`.
|
|
8
|
+
* All code-search paths (search_semantic, hotkey injection, CLI query, web UI,
|
|
9
|
+
* read-tool context) must pass this so quirks never surface as code results —
|
|
10
|
+
* quirks are only reachable through recall_quirks / the quirk CLI.
|
|
11
|
+
*/
|
|
12
|
+
export const CODE_SEARCH_FILTER = { kinds: [""] };
|
|
6
13
|
//# sourceMappingURL=interfaces.js.map
|
package/dist/mcp/handlers.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Handler implementations for all MCP tools: semantic search, file skeleton, symbol usage lookup, and image description.
|
|
3
3
|
*/
|
|
4
|
-
import type
|
|
4
|
+
import { type EmbeddingProvider, type VectorStore, type KeywordIndex, type SearchResult } from "../core/interfaces.js";
|
|
5
5
|
import type { RagConfig } from "../core/config.js";
|
|
6
6
|
import { type ImageVisionProvider } from "../chunker/image.js";
|
|
7
7
|
import { retrieve } from "../retriever/retriever.js";
|
package/dist/mcp/handlers.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Handler implementations for all MCP tools: semantic search, file skeleton, symbol usage lookup, and image description.
|
|
3
|
+
*/
|
|
4
|
+
import { CODE_SEARCH_FILTER } from "../core/interfaces.js";
|
|
1
5
|
import { SUPPORTED_IMAGE_EXTENSIONS } from "../chunker/image.js";
|
|
2
6
|
import { retrieve } from "../retriever/retriever.js";
|
|
3
7
|
import { optimizeContext, DEFAULT_CONTEXT_OPTIMIZATION } from "../retriever/context-optimizer.js";
|
|
@@ -178,6 +182,7 @@ export async function handleSearchSemantic(params, embedder, store, cfg, keyword
|
|
|
178
182
|
keywordWeight: cfg.retrieval.hybridSearch?.keywordWeight,
|
|
179
183
|
hybridEnabled: cfg.retrieval.hybridSearch?.enabled,
|
|
180
184
|
queryPrefix: cfg.embedding.queryPrefix,
|
|
185
|
+
filter: CODE_SEARCH_FILTER,
|
|
181
186
|
};
|
|
182
187
|
const rawResults = await retrieveFn_(query, embedder, store, retrieveOpts);
|
|
183
188
|
if (rawResults.length === 0) {
|
|
@@ -256,7 +261,7 @@ export async function handleFindUsages(params, embedder, store, cfg, keywordInde
|
|
|
256
261
|
const symbolName = params.symbolName.trim();
|
|
257
262
|
const topK = params.topK ?? 30;
|
|
258
263
|
const kwResults = keywordIndex
|
|
259
|
-
? keywordIndex.search(symbolName, topK)
|
|
264
|
+
? keywordIndex.search(symbolName, topK, CODE_SEARCH_FILTER)
|
|
260
265
|
: [];
|
|
261
266
|
const count = await store.count();
|
|
262
267
|
const vsResults = count > 0
|
|
@@ -265,6 +270,7 @@ export async function handleFindUsages(params, embedder, store, cfg, keywordInde
|
|
|
265
270
|
minScore: 0,
|
|
266
271
|
keywordIndex: undefined,
|
|
267
272
|
queryPrefix: cfg.embedding.queryPrefix,
|
|
273
|
+
filter: CODE_SEARCH_FILTER,
|
|
268
274
|
})
|
|
269
275
|
: [];
|
|
270
276
|
const seen = new Set();
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @fileoverview Creates the RAG-backed read tool that returns full file contents with supplementary semantic context.
|
|
3
3
|
*/
|
|
4
4
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
5
|
-
import type
|
|
5
|
+
import { type EmbeddingProvider, type KeywordIndex, type VectorStore, type SearchResult } from "../core/interfaces.js";
|
|
6
6
|
import type { RagConfig } from "../core/config.js";
|
|
7
7
|
export interface RagReadToolOptions {
|
|
8
8
|
/** Workspace root directory. */
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
6
|
+
import { CODE_SEARCH_FILTER } from "../core/interfaces.js";
|
|
6
7
|
import { retrieve } from "../retriever/retriever.js";
|
|
7
8
|
import { normalizeReadArgs, resolveWorkspacePath } from "./tool-args.js";
|
|
8
9
|
import { buildReadQuery } from "./read-query.js";
|
|
@@ -63,7 +64,7 @@ export function createRagReadTool(options) {
|
|
|
63
64
|
}
|
|
64
65
|
else {
|
|
65
66
|
const retrievalQuery = buildSessionQuery(messageText, resolvedPath, normalized);
|
|
66
|
-
rawResults = await retrieve(retrievalQuery, embedder, store, { topK: retrievalTopK, keywordIndex, hybridEnabled: config.retrieval.hybridSearch?.enabled, queryPrefix: config.embedding.queryPrefix });
|
|
67
|
+
rawResults = await retrieve(retrievalQuery, embedder, store, { topK: retrievalTopK, keywordIndex, hybridEnabled: config.retrieval.hybridSearch?.enabled, queryPrefix: config.embedding.queryPrefix, filter: CODE_SEARCH_FILTER });
|
|
67
68
|
const maxSize = options.maxSessionCacheSize ?? 50;
|
|
68
69
|
if (!sessionRetrievalCache.has(sessionID) && sessionRetrievalCache.size >= maxSize) {
|
|
69
70
|
const oldest = sessionRetrievalCache.keys().next().value;
|
|
@@ -80,7 +81,7 @@ export function createRagReadTool(options) {
|
|
|
80
81
|
startLine: normalized.startLine,
|
|
81
82
|
endLine: normalized.endLine,
|
|
82
83
|
});
|
|
83
|
-
rawResults = await retrieve(retrievalQuery, embedder, store, { topK: retrievalTopK, keywordIndex, hybridEnabled: config.retrieval.hybridSearch?.enabled, queryPrefix: config.embedding.queryPrefix });
|
|
84
|
+
rawResults = await retrieve(retrievalQuery, embedder, store, { topK: retrievalTopK, keywordIndex, hybridEnabled: config.retrieval.hybridSearch?.enabled, queryPrefix: config.embedding.queryPrefix, filter: CODE_SEARCH_FILTER });
|
|
84
85
|
}
|
|
85
86
|
// Collect related files from raw results (before filtering)
|
|
86
87
|
relatedFiles = collectRelatedFiles(rawResults, resolvedPath, readRelatedFilesMax);
|
|
@@ -20,6 +20,8 @@ export const MANDATORY_GUIDANCE_LINES = [
|
|
|
20
20
|
"- `describe_image(filePath)`: describe an image file using a vision model. Call when user refers to a screenshot, diagram, or image.",
|
|
21
21
|
"- `recall_quirks(query)`: query experiential quirk memory (gotchas, preferences, decisions). Call when you hit an error or need to recall known pitfalls.",
|
|
22
22
|
"- `add_quirk(content)`: store a new experiential memory. Call when you discover a non-obvious fact, gotcha, or coding convention.",
|
|
23
|
+
"- `update_quirk(id, ...)`: fix an outdated or wrong quirk (content, type, tags, confidence, source ref). The ID is shown in `recall_quirks` output.",
|
|
24
|
+
"- `delete_quirk(id)`: delete a quirk that is fixed, obsolete, or no longer applies. The ID is shown in `recall_quirks` output.",
|
|
23
25
|
"",
|
|
24
26
|
"Decision tree — ALWAYS follow this order:",
|
|
25
27
|
"1. User mentions code behavior/architecture → `search_semantic(query)`",
|
|
@@ -29,6 +31,7 @@ export const MANDATORY_GUIDANCE_LINES = [
|
|
|
29
31
|
"5. User asks about an image or visual asset → `describe_image(filePath)` to retrieve its generated description, then optionally `search_semantic` for related code",
|
|
30
32
|
"6. You encounter an error or need to recall a known pitfall → `recall_quirks(query)`",
|
|
31
33
|
"7. You discover a non-obvious fact or workaround → `add_quirk(content)` to persist it for future sessions",
|
|
34
|
+
"8. A recalled quirk is outdated or wrong → `update_quirk(id, ...)` to fix it, or `delete_quirk(id)` if it no longer applies",
|
|
32
35
|
"",
|
|
33
36
|
"Proactive triggers — you MUST call these tools when:",
|
|
34
37
|
"- User asks about code behavior, architecture, or implementation details",
|
|
@@ -45,7 +48,7 @@ export const MANDATORY_GUIDANCE_LINES = [
|
|
|
45
48
|
"- Answering code questions without calling `search_semantic` first (you guess at behavior)",
|
|
46
49
|
"- Using `grep`/`glob` when `search_semantic` would find the answer faster",
|
|
47
50
|
"- Treating image files as text — use `describe_image` instead of reading raw bytes",
|
|
48
|
-
"- Using `npx opencode-rag quirk` shell commands instead of the built-in `add_quirk` / `recall_quirks`
|
|
51
|
+
"- Using `npx opencode-rag quirk` shell commands instead of the built-in quirk tools (`add_quirk` / `recall_quirks` / `update_quirk` / `delete_quirk`) (the tools are faster, already loaded in-process, and go through the trust monitor)",
|
|
49
52
|
];
|
|
50
53
|
/**
|
|
51
54
|
* The conditional quirk-capture enforcement lines. Only included when
|
|
@@ -60,6 +63,9 @@ export const QUIRK_ENFORCEMENT_LINES = [
|
|
|
60
63
|
"- You make a design decision that future sessions should remember",
|
|
61
64
|
"- You resolve a gotcha that cost more than one attempt",
|
|
62
65
|
"",
|
|
66
|
+
"MANDATORY quirk hygiene — you MUST call `update_quirk` or `delete_quirk` when:",
|
|
67
|
+
"- A stored quirk is outdated, wrong, or has been fixed — update it or delete it instead of adding a contradicting duplicate",
|
|
68
|
+
"",
|
|
63
69
|
"Anti-pattern — NEVER finish a coding session without adding quirks for resolved errors.",
|
|
64
70
|
];
|
|
65
71
|
/**
|
|
@@ -89,6 +95,7 @@ export function buildAgentsMdDirective(opts) {
|
|
|
89
95
|
"- **Images via describe** — `describe_image(filePath)` — never read raw bytes",
|
|
90
96
|
"- **Recall quirks** — `recall_quirks(query)` when you hit a known pitfall",
|
|
91
97
|
"- **Add quirks** — `add_quirk(content)` when you discover a non-obvious fact",
|
|
98
|
+
"- **Fix quirks** — `update_quirk(id, ...)` / `delete_quirk(id)` when a stored quirk is outdated or wrong",
|
|
92
99
|
"",
|
|
93
100
|
"If no results, run `opencode-rag index`.",
|
|
94
101
|
"",
|
|
@@ -100,6 +107,7 @@ export function buildAgentsMdDirective(opts) {
|
|
|
100
107
|
"5. User asks about an image or visual asset → `describe_image(filePath)` to retrieve its generated description, then optionally `search_semantic` for related code",
|
|
101
108
|
"6. You encounter an error or need to recall a known pitfall → `recall_quirks(query)`",
|
|
102
109
|
"7. You discover a non-obvious fact or workaround → `add_quirk(content)` to persist it for future sessions",
|
|
110
|
+
"8. A recalled quirk is outdated or wrong → `update_quirk(id, ...)` to fix it, or `delete_quirk(id)` if it no longer applies",
|
|
103
111
|
"",
|
|
104
112
|
"### Proactive triggers — you MUST call these tools when",
|
|
105
113
|
"- User asks about code behavior, architecture, or implementation details",
|
|
@@ -116,10 +124,10 @@ export function buildAgentsMdDirective(opts) {
|
|
|
116
124
|
"- Answering code questions without calling `search_semantic` first (you guess at behavior)",
|
|
117
125
|
"- Using `grep`/`glob` when `search_semantic` would find the answer faster",
|
|
118
126
|
"- Treating image files as text — use `describe_image` instead of reading raw bytes",
|
|
119
|
-
"- Using `npx opencode-rag quirk` shell commands instead of the built-in `add_quirk` / `recall_quirks`
|
|
127
|
+
"- Using `npx opencode-rag quirk` shell commands instead of the built-in quirk tools (`add_quirk` / `recall_quirks` / `update_quirk` / `delete_quirk`) (the tools are faster, already loaded in-process, and go through the trust monitor)",
|
|
120
128
|
];
|
|
121
129
|
if (opts.promptEnforcement) {
|
|
122
|
-
lines.push("", "### MANDATORY quirk capture rules — you MUST call `add_quirk` when", "- A build, test, or type-check command fails and you resolve it", "- You discover an undocumented library constraint, peer dep, or workaround", "- You learn an environment-specific requirement (OS, tool version, etc.)", "- You make a design decision that future sessions should remember", "- You resolve a gotcha that cost more than one attempt", "- NEVER finish a coding session without adding quirks for resolved errors.");
|
|
130
|
+
lines.push("", "### MANDATORY quirk capture rules — you MUST call `add_quirk` when", "- A build, test, or type-check command fails and you resolve it", "- You discover an undocumented library constraint, peer dep, or workaround", "- You learn an environment-specific requirement (OS, tool version, etc.)", "- You make a design decision that future sessions should remember", "- You resolve a gotcha that cost more than one attempt", "", "### MANDATORY quirk hygiene — you MUST call `update_quirk` or `delete_quirk` when", "- A stored quirk is outdated, wrong, or has been fixed — update it or delete it instead of adding a contradicting duplicate", "- NEVER finish a coding session without adding quirks for resolved errors.");
|
|
123
131
|
}
|
|
124
132
|
lines.push(END_MARKER);
|
|
125
133
|
return lines.join("\n");
|
package/dist/opencode/tools.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @fileoverview Factory functions for OpenCode autonomous-agent tools (file skeleton, find usages, describe image).
|
|
3
3
|
*/
|
|
4
4
|
import type { ToolDefinition } from "@opencode-ai/plugin";
|
|
5
|
-
import type
|
|
5
|
+
import { type EmbeddingProvider, type VectorStore, type KeywordIndex } from "../core/interfaces.js";
|
|
6
6
|
import type { RagConfig } from "../core/config.js";
|
|
7
7
|
import { type ImageVisionProvider } from "../chunker/image.js";
|
|
8
8
|
import { retrieve } from "../retriever/retriever.js";
|
|
@@ -105,3 +105,41 @@ export interface AddQuirkToolOptions {
|
|
|
105
105
|
* @returns A tool definition suitable for OpenCode plugin registration.
|
|
106
106
|
*/
|
|
107
107
|
export declare function createAddQuirkTool(options: AddQuirkToolOptions): ToolDefinition;
|
|
108
|
+
/** Options for creating the `update_quirk` tool. */
|
|
109
|
+
export interface UpdateQuirkToolOptions {
|
|
110
|
+
store: VectorStore;
|
|
111
|
+
embedder: EmbeddingProvider;
|
|
112
|
+
cfg: RagConfig;
|
|
113
|
+
keywordIndex: KeywordIndex;
|
|
114
|
+
storePath: string;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Create the `update_quirk` tool.
|
|
118
|
+
*
|
|
119
|
+
* Updates an existing quirk by ID — replaces content, type, tags, confidence,
|
|
120
|
+
* or source ref. When content changes, the quirk is re-embedded so recall
|
|
121
|
+
* matches the corrected text. The new content passes the trust monitor.
|
|
122
|
+
*
|
|
123
|
+
* @param options - Store, embedder, config, keyword index, store path.
|
|
124
|
+
* @returns A tool definition suitable for OpenCode plugin registration.
|
|
125
|
+
*/
|
|
126
|
+
export declare function createUpdateQuirkTool(options: UpdateQuirkToolOptions): ToolDefinition;
|
|
127
|
+
/** Options for creating the `delete_quirk` tool. */
|
|
128
|
+
export interface DeleteQuirkToolOptions {
|
|
129
|
+
store: VectorStore;
|
|
130
|
+
embedder: EmbeddingProvider;
|
|
131
|
+
cfg: RagConfig;
|
|
132
|
+
keywordIndex: KeywordIndex;
|
|
133
|
+
storePath: string;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Create the `delete_quirk` tool.
|
|
137
|
+
*
|
|
138
|
+
* Removes a quirk by ID from the vector store, keyword index, and audit log.
|
|
139
|
+
* Use when a quirk is wrong, no longer applies, or has been superseded by an
|
|
140
|
+
* updated version.
|
|
141
|
+
*
|
|
142
|
+
* @param options - Store, embedder, config, keyword index, store path.
|
|
143
|
+
* @returns A tool definition suitable for OpenCode plugin registration.
|
|
144
|
+
*/
|
|
145
|
+
export declare function createDeleteQuirkTool(options: DeleteQuirkToolOptions): ToolDefinition;
|
package/dist/opencode/tools.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* These complement the general-purpose search_semantic tool.
|
|
14
14
|
*/
|
|
15
15
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
16
|
+
import { CODE_SEARCH_FILTER } from "../core/interfaces.js";
|
|
16
17
|
import { SUPPORTED_IMAGE_EXTENSIONS, createImageVisionProvider, getMimeType } from "../chunker/image.js";
|
|
17
18
|
import { resizeImage } from "../content/image.js";
|
|
18
19
|
import { retrieve } from "../retriever/retriever.js";
|
|
@@ -20,7 +21,7 @@ import { Parser } from "web-tree-sitter";
|
|
|
20
21
|
import { initParser, loadLanguage, walkTree } from "../chunker/grammar.js";
|
|
21
22
|
import { readFileSync } from "node:fs";
|
|
22
23
|
import { resolveWorkspacePath } from "./tool-args.js";
|
|
23
|
-
import { addQuirk, recallQuirks } from "../quirks/quirk-store.js";
|
|
24
|
+
import { addQuirk, updateQuirk, removeQuirk, recallQuirks } from "../quirks/quirk-store.js";
|
|
24
25
|
const SKELETON_CONFIGS = {
|
|
25
26
|
".ts": { grammarName: "typescript", nodeTypes: ["function_declaration", "method_definition", "class_declaration", "interface_declaration", "type_alias_declaration", "enum_declaration"] },
|
|
26
27
|
".tsx": { grammarName: "typescript", nodeTypes: ["function_declaration", "method_definition", "class_declaration", "interface_declaration", "type_alias_declaration", "enum_declaration", "arrow_function"] },
|
|
@@ -362,7 +363,7 @@ export function createFindUsagesTool(options) {
|
|
|
362
363
|
async function searchViaKeywordIndex(symbol, topK) {
|
|
363
364
|
if (!keywordIndex)
|
|
364
365
|
return [];
|
|
365
|
-
return keywordIndex.search(symbol, topK);
|
|
366
|
+
return keywordIndex.search(symbol, topK, CODE_SEARCH_FILTER);
|
|
366
367
|
}
|
|
367
368
|
async function searchViaVectorStore(symbol, topK) {
|
|
368
369
|
const count = await store.count();
|
|
@@ -373,6 +374,7 @@ export function createFindUsagesTool(options) {
|
|
|
373
374
|
minScore: 0,
|
|
374
375
|
keywordIndex: undefined,
|
|
375
376
|
queryPrefix: cfg.embedding.queryPrefix,
|
|
377
|
+
filter: CODE_SEARCH_FILTER,
|
|
376
378
|
});
|
|
377
379
|
}
|
|
378
380
|
function escapeRegex(s) {
|
|
@@ -620,4 +622,117 @@ export function createAddQuirkTool(options) {
|
|
|
620
622
|
},
|
|
621
623
|
});
|
|
622
624
|
}
|
|
625
|
+
/**
|
|
626
|
+
* Create the `update_quirk` tool.
|
|
627
|
+
*
|
|
628
|
+
* Updates an existing quirk by ID — replaces content, type, tags, confidence,
|
|
629
|
+
* or source ref. When content changes, the quirk is re-embedded so recall
|
|
630
|
+
* matches the corrected text. The new content passes the trust monitor.
|
|
631
|
+
*
|
|
632
|
+
* @param options - Store, embedder, config, keyword index, store path.
|
|
633
|
+
* @returns A tool definition suitable for OpenCode plugin registration.
|
|
634
|
+
*/
|
|
635
|
+
export function createUpdateQuirkTool(options) {
|
|
636
|
+
const { store, embedder, cfg, keywordIndex, storePath } = options;
|
|
637
|
+
return tool({
|
|
638
|
+
description: "Update an existing experiential memory (quirk) by its ID — fix outdated " +
|
|
639
|
+
"content, change its type, tags, or source ref. Use when a quirk from a " +
|
|
640
|
+
"past session is outdated, wrong, or superseded. Find the ID via " +
|
|
641
|
+
"`recall_quirks` (it is listed in the recall output) or `quirk list` " +
|
|
642
|
+
"in the CLI.",
|
|
643
|
+
args: {
|
|
644
|
+
id: tool.schema.string().min(1, "Quirk ID is required."),
|
|
645
|
+
content: tool.schema.string().optional(),
|
|
646
|
+
quirkType: tool.schema.string().optional(),
|
|
647
|
+
tags: tool.schema.array(tool.schema.string().min(1)).max(10).optional(),
|
|
648
|
+
confidence: tool.schema.number().min(0).max(1).optional(),
|
|
649
|
+
sourceRef: tool.schema.string().optional(),
|
|
650
|
+
},
|
|
651
|
+
async execute(args) {
|
|
652
|
+
try {
|
|
653
|
+
const deps = { embedder, store, keywordIndex, cfg, storePath };
|
|
654
|
+
const fields = [
|
|
655
|
+
args.content,
|
|
656
|
+
args.quirkType,
|
|
657
|
+
args.tags,
|
|
658
|
+
args.confidence,
|
|
659
|
+
args.sourceRef,
|
|
660
|
+
];
|
|
661
|
+
if (fields.every((f) => f === undefined)) {
|
|
662
|
+
return {
|
|
663
|
+
title: "Quirk update",
|
|
664
|
+
output: "Nothing to update — provide at least one of `content`, `quirkType`, `tags`, `confidence`, or `sourceRef`.",
|
|
665
|
+
metadata: { tool: "update_quirk", quirkId: args.id, error: "no fields provided" },
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
const quirk = await updateQuirk(deps, args.id, {
|
|
669
|
+
content: args.content,
|
|
670
|
+
quirkType: args.quirkType,
|
|
671
|
+
tags: args.tags,
|
|
672
|
+
confidence: args.confidence,
|
|
673
|
+
sourceRef: args.sourceRef,
|
|
674
|
+
});
|
|
675
|
+
return {
|
|
676
|
+
title: "Quirk updated",
|
|
677
|
+
output: `**Quirk updated** (id: \`${quirk.id}\`)\n` +
|
|
678
|
+
`\`${quirk.quirkType ?? "general"}\` | confidence=${(quirk.confidence * 100).toFixed(0)}% | tags=${(quirk.tags ?? []).join(", ") || "none"}\n` +
|
|
679
|
+
quirk.content,
|
|
680
|
+
metadata: {
|
|
681
|
+
tool: "update_quirk",
|
|
682
|
+
quirkId: quirk.id,
|
|
683
|
+
quirkType: quirk.quirkType,
|
|
684
|
+
confidence: quirk.confidence,
|
|
685
|
+
},
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
catch (err) {
|
|
689
|
+
return {
|
|
690
|
+
title: "Quirk update",
|
|
691
|
+
output: `Failed to update quirk: ${err instanceof Error ? err.message : String(err)}`,
|
|
692
|
+
metadata: { tool: "update_quirk", error: String(err) },
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
},
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Create the `delete_quirk` tool.
|
|
700
|
+
*
|
|
701
|
+
* Removes a quirk by ID from the vector store, keyword index, and audit log.
|
|
702
|
+
* Use when a quirk is wrong, no longer applies, or has been superseded by an
|
|
703
|
+
* updated version.
|
|
704
|
+
*
|
|
705
|
+
* @param options - Store, embedder, config, keyword index, store path.
|
|
706
|
+
* @returns A tool definition suitable for OpenCode plugin registration.
|
|
707
|
+
*/
|
|
708
|
+
export function createDeleteQuirkTool(options) {
|
|
709
|
+
const { store, embedder, cfg, keywordIndex, storePath } = options;
|
|
710
|
+
return tool({
|
|
711
|
+
description: "Delete an experiential memory (quirk) by its ID. Use when a quirk is " +
|
|
712
|
+
"wrong, outdated, or superseded — for example a gotcha that was fixed " +
|
|
713
|
+
"or a decision that was reversed. Find the ID via `recall_quirks` (it " +
|
|
714
|
+
"is listed in the recall output) or `quirk list` in the CLI.",
|
|
715
|
+
args: {
|
|
716
|
+
id: tool.schema.string().min(1, "Quirk ID is required."),
|
|
717
|
+
},
|
|
718
|
+
async execute(args) {
|
|
719
|
+
try {
|
|
720
|
+
const deps = { embedder, store, keywordIndex, cfg, storePath };
|
|
721
|
+
await removeQuirk(deps, args.id);
|
|
722
|
+
return {
|
|
723
|
+
title: "Quirk deleted",
|
|
724
|
+
output: `**Quirk deleted** (id: \`${args.id}\`). It will no longer be recalled or auto-injected.`,
|
|
725
|
+
metadata: { tool: "delete_quirk", quirkId: args.id },
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
catch (err) {
|
|
729
|
+
return {
|
|
730
|
+
title: "Quirk delete",
|
|
731
|
+
output: `Failed to delete quirk: ${err instanceof Error ? err.message : String(err)}`,
|
|
732
|
+
metadata: { tool: "delete_quirk", error: String(err) },
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
},
|
|
736
|
+
});
|
|
737
|
+
}
|
|
623
738
|
//# sourceMappingURL=tools.js.map
|
package/dist/plugin.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* automatic context injection and documentation mode.
|
|
5
5
|
*/
|
|
6
6
|
import type { Plugin, Hooks } from "@opencode-ai/plugin";
|
|
7
|
-
import type
|
|
7
|
+
import { type EmbeddingProvider, type DescriptionProvider, type KeywordIndex, type VectorStore } from "./core/interfaces.js";
|
|
8
8
|
import { type RagConfig } from "./core/config.js";
|
|
9
9
|
import { createEmbedder } from "./embedder/factory.js";
|
|
10
10
|
import { retrieve } from "./retriever/retriever.js";
|
package/dist/plugin.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* automatic context injection and documentation mode.
|
|
5
5
|
*/
|
|
6
6
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
7
|
+
import { CODE_SEARCH_FILTER } from "./core/interfaces.js";
|
|
7
8
|
import { loadConfig, findConfigFile, DEFAULT_CONFIG, resolveLogConfig, persistProbedDimension } from "./core/config.js";
|
|
8
9
|
import { createEmbedder } from "./embedder/factory.js";
|
|
9
10
|
import { createDescriptionProvider } from "./describer/factory.js";
|
|
@@ -15,7 +16,7 @@ import { appendDebugLog } from "./core/fileLogger.js";
|
|
|
15
16
|
import { loadRuntimeOverrides, applyRuntimeOverrides } from "./core/runtime-overrides.js";
|
|
16
17
|
import { createBackgroundIndexer } from "./watcher.js";
|
|
17
18
|
import { createRagReadTool } from "./opencode/create-read-tool.js";
|
|
18
|
-
import { createFileSkeletonTool, createFindUsagesTool, createDescribeImageTool, createRecallQuirksTool, createAddQuirkTool, } from "./opencode/tools.js";
|
|
19
|
+
import { createFileSkeletonTool, createFindUsagesTool, createDescribeImageTool, createRecallQuirksTool, createAddQuirkTool, createUpdateQuirkTool, createDeleteQuirkTool, } from "./opencode/tools.js";
|
|
19
20
|
import { resolveApiKey } from "./core/resolve-api-key.js";
|
|
20
21
|
import { consumePendingRagInjection } from "./core/rag-injection-flag.js";
|
|
21
22
|
import { loadDocProgress, markSubdirectoryDocumented } from "./core/doc-progress.js";
|
|
@@ -645,6 +646,40 @@ export function createRagHooks(options) {
|
|
|
645
646
|
error: err,
|
|
646
647
|
});
|
|
647
648
|
}
|
|
649
|
+
try {
|
|
650
|
+
const updateQuirkTool = createUpdateQuirkTool({
|
|
651
|
+
store,
|
|
652
|
+
embedder,
|
|
653
|
+
cfg: effectiveCfg,
|
|
654
|
+
keywordIndex: keywordIndex,
|
|
655
|
+
storePath: options.storePath,
|
|
656
|
+
});
|
|
657
|
+
tools["update_quirk"] = updateQuirkTool;
|
|
658
|
+
}
|
|
659
|
+
catch (err) {
|
|
660
|
+
appendDebugLog(options.logFilePath, {
|
|
661
|
+
scope: "plugin",
|
|
662
|
+
message: "Failed to register update_quirk tool",
|
|
663
|
+
error: err,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
try {
|
|
667
|
+
const deleteQuirkTool = createDeleteQuirkTool({
|
|
668
|
+
store,
|
|
669
|
+
embedder,
|
|
670
|
+
cfg: effectiveCfg,
|
|
671
|
+
keywordIndex: keywordIndex,
|
|
672
|
+
storePath: options.storePath,
|
|
673
|
+
});
|
|
674
|
+
tools["delete_quirk"] = deleteQuirkTool;
|
|
675
|
+
}
|
|
676
|
+
catch (err) {
|
|
677
|
+
appendDebugLog(options.logFilePath, {
|
|
678
|
+
scope: "plugin",
|
|
679
|
+
message: "Failed to register delete_quirk tool",
|
|
680
|
+
error: err,
|
|
681
|
+
});
|
|
682
|
+
}
|
|
648
683
|
if (readOverride) {
|
|
649
684
|
const readTool = createRagReadTool({
|
|
650
685
|
worktree: options.worktree,
|
|
@@ -1155,6 +1190,8 @@ export function createRagHooks(options) {
|
|
|
1155
1190
|
keywordIndex,
|
|
1156
1191
|
keywordWeight: hybridCfg?.keywordWeight,
|
|
1157
1192
|
queryPrefix: effectiveCfg.embedding.queryPrefix,
|
|
1193
|
+
// Never surface quirk chunks in hotkey file lists / chunk injections.
|
|
1194
|
+
filter: CODE_SEARCH_FILTER,
|
|
1158
1195
|
});
|
|
1159
1196
|
const retrievalTimeMs = Date.now() - retrievalStart;
|
|
1160
1197
|
if (results.length > 0) {
|
|
@@ -9,9 +9,22 @@ export interface QuirkStoreDeps {
|
|
|
9
9
|
cfg: RagConfig;
|
|
10
10
|
storePath: string;
|
|
11
11
|
}
|
|
12
|
+
/** Look up a single quirk by its ID, or `undefined` when not found. */
|
|
13
|
+
export declare function getQuirk(deps: QuirkStoreDeps, id: string): Promise<Quirk | undefined>;
|
|
14
|
+
/**
|
|
15
|
+
* Update an existing quirk by ID. Fields in `patch` override the stored values.
|
|
16
|
+
*
|
|
17
|
+
* When `content` changes, the new text must pass the trust monitor, the quirk
|
|
18
|
+
* is re-embedded, and the vector-store chunk + keyword index entry are replaced
|
|
19
|
+
* (same ID, new embedding). The audit log entry is rewritten in place.
|
|
20
|
+
*
|
|
21
|
+
* @throws If no quirk with the given ID exists, or the new content is rejected
|
|
22
|
+
* by the trust monitor.
|
|
23
|
+
*/
|
|
24
|
+
export declare function updateQuirk(deps: QuirkStoreDeps, id: string, patch: Partial<QuirkInput>): Promise<Quirk>;
|
|
12
25
|
/** Add a new quirk to the vector store, keyword index, and audit log. */
|
|
13
26
|
export declare function addQuirk(deps: QuirkStoreDeps, input: QuirkInput): Promise<Quirk>;
|
|
14
|
-
/** Remove a quirk by its ID. */
|
|
27
|
+
/** Remove a quirk by its ID. Throws if no quirk with the given ID exists. */
|
|
15
28
|
export declare function removeQuirk(deps: QuirkStoreDeps, id: string): Promise<void>;
|
|
16
29
|
/** List all quirks sorted by lastObserved descending. */
|
|
17
30
|
export declare function listQuirks(deps: QuirkStoreDeps): Promise<Quirk[]>;
|
|
@@ -38,6 +38,106 @@ function rewriteJsonl(filePath, quirks) {
|
|
|
38
38
|
function nowISO() {
|
|
39
39
|
return new Date().toISOString();
|
|
40
40
|
}
|
|
41
|
+
/** Look up a single quirk by its ID, or `undefined` when not found. */
|
|
42
|
+
export async function getQuirk(deps, id) {
|
|
43
|
+
if (!isMemoryStore(deps.storePath)) {
|
|
44
|
+
const jp = jsonlPath(deps.storePath);
|
|
45
|
+
if (existsSync(jp)) {
|
|
46
|
+
const found = readJsonl(jp).find((q) => q.id === id);
|
|
47
|
+
if (found)
|
|
48
|
+
return found;
|
|
49
|
+
}
|
|
50
|
+
const chunks = await deps.store.getChunksByFilePath(QUIRK_FILE_PREFIX + id);
|
|
51
|
+
const c = chunks[0];
|
|
52
|
+
if (c) {
|
|
53
|
+
return {
|
|
54
|
+
id: c.id,
|
|
55
|
+
content: c.content,
|
|
56
|
+
quirkType: c.metadata.quirkType,
|
|
57
|
+
tags: c.metadata.tags ?? [],
|
|
58
|
+
confidence: c.metadata.confidence ?? 1,
|
|
59
|
+
lastObserved: c.metadata.lastObserved ?? "",
|
|
60
|
+
sourceRef: undefined,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
return memQuirks.get(id);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Update an existing quirk by ID. Fields in `patch` override the stored values.
|
|
69
|
+
*
|
|
70
|
+
* When `content` changes, the new text must pass the trust monitor, the quirk
|
|
71
|
+
* is re-embedded, and the vector-store chunk + keyword index entry are replaced
|
|
72
|
+
* (same ID, new embedding). The audit log entry is rewritten in place.
|
|
73
|
+
*
|
|
74
|
+
* @throws If no quirk with the given ID exists, or the new content is rejected
|
|
75
|
+
* by the trust monitor.
|
|
76
|
+
*/
|
|
77
|
+
export async function updateQuirk(deps, id, patch) {
|
|
78
|
+
const existing = await getQuirk(deps, id);
|
|
79
|
+
if (!existing) {
|
|
80
|
+
throw new Error(`Quirk not found: ${id}`);
|
|
81
|
+
}
|
|
82
|
+
const content = patch.content ?? existing.content;
|
|
83
|
+
const quirkType = patch.quirkType ?? existing.quirkType;
|
|
84
|
+
const tags = patch.tags ?? existing.tags;
|
|
85
|
+
const confidence = patch.confidence ?? existing.confidence;
|
|
86
|
+
const sourceRef = patch.sourceRef ?? existing.sourceRef;
|
|
87
|
+
if (content !== existing.content) {
|
|
88
|
+
const allowed = isQuirkAllowed(content);
|
|
89
|
+
if (!allowed.ok) {
|
|
90
|
+
throw new Error(`Quirk rejected by trust monitor: ${allowed.reason}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const updated = {
|
|
94
|
+
id,
|
|
95
|
+
content,
|
|
96
|
+
quirkType,
|
|
97
|
+
tags,
|
|
98
|
+
confidence,
|
|
99
|
+
lastObserved: existing.lastObserved,
|
|
100
|
+
sourceRef,
|
|
101
|
+
};
|
|
102
|
+
const filePath = QUIRK_FILE_PREFIX + id;
|
|
103
|
+
await deps.store.deleteByFilePath(filePath);
|
|
104
|
+
deps.keywordIndex.removeByFilePath(filePath);
|
|
105
|
+
const prefix = deps.cfg.embedding.documentPrefix ?? "";
|
|
106
|
+
const chunkContent = prefix + content;
|
|
107
|
+
const embeddings = await deps.embedder.embed([chunkContent], "document");
|
|
108
|
+
const embedding = embeddings[0];
|
|
109
|
+
if (!embedding || embedding.length === 0) {
|
|
110
|
+
throw new Error("Embedding returned empty vector for quirk content");
|
|
111
|
+
}
|
|
112
|
+
const chunk = {
|
|
113
|
+
id,
|
|
114
|
+
content,
|
|
115
|
+
description: "",
|
|
116
|
+
embedding,
|
|
117
|
+
metadata: {
|
|
118
|
+
filePath,
|
|
119
|
+
startLine: 0,
|
|
120
|
+
endLine: 0,
|
|
121
|
+
language: "quirk",
|
|
122
|
+
kind: "quirk",
|
|
123
|
+
quirkType,
|
|
124
|
+
tags,
|
|
125
|
+
confidence,
|
|
126
|
+
lastObserved: updated.lastObserved,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
await deps.store.addChunks([chunk]);
|
|
130
|
+
deps.keywordIndex.addChunks([chunk]);
|
|
131
|
+
if (!isMemoryStore(deps.storePath)) {
|
|
132
|
+
const jp = jsonlPath(deps.storePath);
|
|
133
|
+
const all = readJsonl(jp).map((q) => (q.id === id ? updated : q));
|
|
134
|
+
rewriteJsonl(jp, all);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
memQuirks.set(id, updated);
|
|
138
|
+
}
|
|
139
|
+
return updated;
|
|
140
|
+
}
|
|
41
141
|
/** Add a new quirk to the vector store, keyword index, and audit log. */
|
|
42
142
|
export async function addQuirk(deps, input) {
|
|
43
143
|
const allowed = isQuirkAllowed(input.content);
|
|
@@ -90,8 +190,12 @@ export async function addQuirk(deps, input) {
|
|
|
90
190
|
}
|
|
91
191
|
return quirk;
|
|
92
192
|
}
|
|
93
|
-
/** Remove a quirk by its ID. */
|
|
193
|
+
/** Remove a quirk by its ID. Throws if no quirk with the given ID exists. */
|
|
94
194
|
export async function removeQuirk(deps, id) {
|
|
195
|
+
const existing = await getQuirk(deps, id);
|
|
196
|
+
if (!existing) {
|
|
197
|
+
throw new Error(`Quirk not found: ${id}`);
|
|
198
|
+
}
|
|
95
199
|
const filePath = QUIRK_FILE_PREFIX + id;
|
|
96
200
|
await deps.store.deleteByFilePath(filePath);
|
|
97
201
|
deps.keywordIndex.removeByFilePath(filePath);
|
package/dist/web/api.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
5
5
|
import { LanceDbStore } from "../vectorstore/lancedb.js";
|
|
6
6
|
import { KeywordIndex } from "../retriever/keyword-index.js";
|
|
7
7
|
import type { RagConfig } from "../core/config.js";
|
|
8
|
-
import type
|
|
8
|
+
import { type EmbeddingProvider } from "../core/interfaces.js";
|
|
9
9
|
/** Internal shape for a JSON API response: an HTTP status code and a serialisable body. */
|
|
10
10
|
interface ApiResponse {
|
|
11
11
|
status: number;
|
package/dist/web/api.js
CHANGED
|
@@ -5,6 +5,7 @@ import { listSessions, getSession, deleteSession, compareSessions, validateSessi
|
|
|
5
5
|
import { analyzeTokenUsage, compareTokenAnalyses, projectTokenSavings } from "../eval/token-analysis.js";
|
|
6
6
|
import { listQuirks, lintQuirks, removeQuirk } from "../quirks/quirk-store.js";
|
|
7
7
|
import { retrieve } from "../retriever/retriever.js";
|
|
8
|
+
import { CODE_SEARCH_FILTER } from "../core/interfaces.js";
|
|
8
9
|
const FILE_MIME_TYPES = {
|
|
9
10
|
".png": "image/png",
|
|
10
11
|
".jpg": "image/jpeg",
|
|
@@ -244,7 +245,16 @@ async function handleQuirkLint(deps) {
|
|
|
244
245
|
}
|
|
245
246
|
/** Delete a single quirk by its ID from the store, index, and audit log. */
|
|
246
247
|
async function handleQuirkDelete(deps, id) {
|
|
247
|
-
|
|
248
|
+
try {
|
|
249
|
+
await removeQuirk(deps, id);
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
253
|
+
if (/Quirk not found/.test(message)) {
|
|
254
|
+
return { status: 404, body: { error: message } };
|
|
255
|
+
}
|
|
256
|
+
throw err;
|
|
257
|
+
}
|
|
248
258
|
return { status: 200, body: { deleted: true, id } };
|
|
249
259
|
}
|
|
250
260
|
/**
|
|
@@ -288,7 +298,7 @@ async function handleSearch(keywordIndex, params) {
|
|
|
288
298
|
if (!query.trim()) {
|
|
289
299
|
return { status: 200, body: { results: [] } };
|
|
290
300
|
}
|
|
291
|
-
const results = keywordIndex.search(query, topK);
|
|
301
|
+
const results = keywordIndex.search(query, topK, CODE_SEARCH_FILTER);
|
|
292
302
|
return {
|
|
293
303
|
status: 200,
|
|
294
304
|
body: {
|
|
@@ -346,6 +356,7 @@ async function handleRetrieve(store, keywordIndex, getEmbedder, cfg, params) {
|
|
|
346
356
|
filter: {
|
|
347
357
|
pathPatterns: pathFilter ? pathFilter.split(",").map((s) => s.trim()).filter(Boolean) : undefined,
|
|
348
358
|
languages: langFilter ? langFilter.split(",").map((s) => s.trim()).filter(Boolean) : undefined,
|
|
359
|
+
kinds: CODE_SEARCH_FILTER.kinds,
|
|
349
360
|
},
|
|
350
361
|
});
|
|
351
362
|
return {
|