opencode-rag-plugin 1.19.3 → 1.19.5
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/dist/api.d.ts +1 -1
- package/dist/api.js +2 -0
- package/dist/chunker/base.js +19 -5
- package/dist/chunker/factory.js +27 -9
- package/dist/chunker/grammar.d.ts +18 -1
- package/dist/chunker/grammar.js +48 -10
- package/dist/chunker/pdf.js +30 -14
- package/dist/cli/commands/init-helpers.js +15 -2
- package/dist/cli/commands/init.js +31 -21
- package/dist/cli/commands/query.js +2 -0
- package/dist/cli/commands/quirk.js +9 -3
- package/dist/cli/commands/setup.js +5 -2
- package/dist/cli/commands/status.js +14 -5
- package/dist/cli/commands/ui.js +23 -9
- package/dist/cli/commands/update.js +4 -5
- package/dist/cli/format.d.ts +5 -2
- package/dist/cli/format.js +14 -5
- package/dist/content/image.js +33 -11
- package/dist/content/reader.js +79 -17
- package/dist/core/bootstrap.js +10 -3
- package/dist/core/config.js +31 -0
- package/dist/core/desc-cache.d.ts +8 -2
- package/dist/core/desc-cache.js +10 -3
- package/dist/core/doc-progress.js +5 -2
- package/dist/core/interfaces.d.ts +8 -0
- package/dist/core/interfaces.js +8 -1
- package/dist/core/provider-defaults.d.ts +2 -0
- package/dist/core/provider-defaults.js +19 -4
- package/dist/core/runtime-overrides.d.ts +0 -6
- package/dist/core/version-check.d.ts +5 -0
- package/dist/core/version-check.js +8 -2
- package/dist/describer/anthropic.d.ts +2 -2
- package/dist/describer/anthropic.js +19 -5
- package/dist/describer/describer.js +15 -2
- package/dist/describer/gemini.js +25 -10
- package/dist/embedder/factory.d.ts +5 -3
- package/dist/embedder/factory.js +41 -8
- package/dist/embedder/health.js +19 -19
- package/dist/embedder/http.d.ts +14 -1
- package/dist/embedder/http.js +60 -6
- package/dist/eval/session-logger.js +7 -0
- package/dist/eval/storage.js +8 -0
- package/dist/indexer/git-diff.d.ts +1 -1
- package/dist/indexer/git-diff.js +5 -1
- package/dist/indexer/pipeline.js +421 -344
- package/dist/indexer/stats.d.ts +2 -0
- package/dist/indexer/stats.js +1 -0
- package/dist/indexer/watch.js +8 -1
- package/dist/indexer/worker.js +21 -0
- package/dist/mcp/cli.js +4 -0
- package/dist/mcp/handlers.d.ts +1 -1
- package/dist/mcp/handlers.js +23 -6
- package/dist/mcp/server.js +3 -0
- package/dist/opencode/create-read-tool.d.ts +1 -1
- package/dist/opencode/create-read-tool.js +17 -5
- package/dist/opencode/tool-args.js +23 -1
- package/dist/opencode/tools.d.ts +1 -1
- package/dist/opencode/tools.js +3 -1
- package/dist/plugin.d.ts +1 -1
- package/dist/plugin.js +69 -152
- package/dist/quirks/auto-capture.js +5 -0
- package/dist/quirks/quirk-store.d.ts +1 -1
- package/dist/quirks/quirk-store.js +56 -17
- package/dist/retriever/context-optimizer.js +18 -4
- package/dist/retriever/keyword-index.d.ts +2 -0
- package/dist/retriever/keyword-index.js +38 -4
- package/dist/retriever/retriever.js +6 -1
- package/dist/tui.js +41 -4
- package/dist/vectorstore/lancedb.d.ts +25 -1
- package/dist/vectorstore/lancedb.js +157 -11
- package/dist/vectorstore/memory.js +5 -1
- package/dist/watcher.js +30 -4
- package/dist/web/api.d.ts +6 -2
- package/dist/web/api.js +198 -70
- package/dist/web/server.d.ts +2 -0
- package/dist/web/server.js +66 -28
- package/dist/web/static.d.ts +5 -2
- package/dist/web/static.js +9 -5
- package/dist/web/ui/assets/index-BDPYdtA1.js +3 -0
- package/dist/web/ui/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/ui/assets/index-CJBvt6e0.js +0 -3
package/dist/cli/format.d.ts
CHANGED
|
@@ -60,7 +60,7 @@ export declare const c: {
|
|
|
60
60
|
* @param message - Human-readable error message.
|
|
61
61
|
* @param error - Optional error object for structured logging.
|
|
62
62
|
*/
|
|
63
|
-
export declare function logCliError(
|
|
63
|
+
export declare function logCliError(logFilePath: string, scope: string, message: string, error?: unknown): void;
|
|
64
64
|
/**
|
|
65
65
|
* Log an informational message to stdout and optionally append to the debug log.
|
|
66
66
|
*
|
|
@@ -68,7 +68,7 @@ export declare function logCliError(_logFilePath: string, _scope: string, messag
|
|
|
68
68
|
* @param scope - Logical scope (e.g. "index", "query") for log filtering.
|
|
69
69
|
* @param message - Human-readable info message.
|
|
70
70
|
*/
|
|
71
|
-
export declare function logCliInfo(
|
|
71
|
+
export declare function logCliInfo(logFilePath: string, scope: string, message: string): void;
|
|
72
72
|
/**
|
|
73
73
|
* Resolve a full `RagContext` from CLI options and log the config details.
|
|
74
74
|
*
|
|
@@ -82,6 +82,9 @@ export declare function resolveCliContext(opt: CliOptions, logFilePath: string,
|
|
|
82
82
|
/**
|
|
83
83
|
* Gracefully close a `RagContext` — closes the vector store and destroys pooled HTTP connections.
|
|
84
84
|
*
|
|
85
|
+
* The store close is raced against a timeout because LanceDB's native `close()`
|
|
86
|
+
* can hang indefinitely on Windows; callers must never be blocked forever.
|
|
87
|
+
*
|
|
85
88
|
* @param ctx - The `RagContext` to clean up.
|
|
86
89
|
*/
|
|
87
90
|
export declare function cleanupContext(ctx: RagContext): Promise<void>;
|
package/dist/cli/format.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import pc from "picocolors";
|
|
9
9
|
import { resolveRagContext } from "../core/bootstrap.js";
|
|
10
10
|
import { destroyAllPooledConnections } from "../embedder/http.js";
|
|
11
|
+
import { appendDebugLog } from "../core/fileLogger.js";
|
|
11
12
|
// ── Color palette ───────────────────────────────────────────────
|
|
12
13
|
/**
|
|
13
14
|
* Semantic color helpers for consistent CLI output styling.
|
|
@@ -61,9 +62,9 @@ export const c = {
|
|
|
61
62
|
* @param message - Human-readable error message.
|
|
62
63
|
* @param error - Optional error object for structured logging.
|
|
63
64
|
*/
|
|
64
|
-
export function logCliError(
|
|
65
|
+
export function logCliError(logFilePath, scope, message, error) {
|
|
65
66
|
console.error(c.error(message));
|
|
66
|
-
|
|
67
|
+
appendDebugLog(logFilePath, { scope, message, error });
|
|
67
68
|
}
|
|
68
69
|
/**
|
|
69
70
|
* Log an informational message to stdout and optionally append to the debug log.
|
|
@@ -72,9 +73,9 @@ export function logCliError(_logFilePath, _scope, message, _error) {
|
|
|
72
73
|
* @param scope - Logical scope (e.g. "index", "query") for log filtering.
|
|
73
74
|
* @param message - Human-readable info message.
|
|
74
75
|
*/
|
|
75
|
-
export function logCliInfo(
|
|
76
|
+
export function logCliInfo(logFilePath, scope, message) {
|
|
76
77
|
console.log(message);
|
|
77
|
-
|
|
78
|
+
appendDebugLog(logFilePath, { scope, message });
|
|
78
79
|
}
|
|
79
80
|
// ── Context resolution ──────────────────────────────────────────
|
|
80
81
|
/**
|
|
@@ -109,10 +110,18 @@ function logConfigDetails(logFilePath, config) {
|
|
|
109
110
|
/**
|
|
110
111
|
* Gracefully close a `RagContext` — closes the vector store and destroys pooled HTTP connections.
|
|
111
112
|
*
|
|
113
|
+
* The store close is raced against a timeout because LanceDB's native `close()`
|
|
114
|
+
* can hang indefinitely on Windows; callers must never be blocked forever.
|
|
115
|
+
*
|
|
112
116
|
* @param ctx - The `RagContext` to clean up.
|
|
113
117
|
*/
|
|
114
118
|
export async function cleanupContext(ctx) {
|
|
115
|
-
await
|
|
119
|
+
await Promise.race([
|
|
120
|
+
ctx.store.close(),
|
|
121
|
+
new Promise((resolve) => {
|
|
122
|
+
setTimeout(resolve, 5000).unref();
|
|
123
|
+
}),
|
|
124
|
+
]);
|
|
116
125
|
destroyAllPooledConnections();
|
|
117
126
|
}
|
|
118
127
|
// ── Formatting helpers ──────────────────────────────────────────
|
package/dist/content/image.js
CHANGED
|
@@ -95,25 +95,47 @@ export async function resizeImage(buffer, filePath, maxDimension) {
|
|
|
95
95
|
const { pixels, width, height, channels } = decodeBmp(buffer);
|
|
96
96
|
const ch = channels;
|
|
97
97
|
if (width <= maxDimension && height <= maxDimension) {
|
|
98
|
-
|
|
99
|
-
.jpeg({ quality: 80 })
|
|
100
|
-
|
|
98
|
+
const pipeline = sharp(pixels, { raw: { width, height, channels: ch } })
|
|
99
|
+
.jpeg({ quality: 80 });
|
|
100
|
+
try {
|
|
101
|
+
return await pipeline.toBuffer();
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
pipeline.destroy();
|
|
105
|
+
}
|
|
101
106
|
}
|
|
102
|
-
|
|
107
|
+
const pipeline = sharp(pixels, { raw: { width, height, channels: ch } })
|
|
103
108
|
.resize({ width: maxDimension, fit: "inside", withoutEnlargement: true })
|
|
104
|
-
.jpeg({ quality: 80 })
|
|
105
|
-
|
|
109
|
+
.jpeg({ quality: 80 });
|
|
110
|
+
try {
|
|
111
|
+
return await pipeline.toBuffer();
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
pipeline.destroy();
|
|
115
|
+
}
|
|
106
116
|
}
|
|
107
|
-
const
|
|
117
|
+
const metaPipeline = sharp(buffer);
|
|
118
|
+
const meta = await metaPipeline.metadata().finally(() => metaPipeline.destroy());
|
|
108
119
|
const w = meta.width ?? 0;
|
|
109
120
|
const h = meta.height ?? 0;
|
|
110
121
|
if (w <= maxDimension && h <= maxDimension) {
|
|
111
|
-
|
|
122
|
+
const pipeline = sharp(buffer).jpeg({ quality: 80 });
|
|
123
|
+
try {
|
|
124
|
+
return await pipeline.toBuffer();
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
pipeline.destroy();
|
|
128
|
+
}
|
|
112
129
|
}
|
|
113
|
-
|
|
130
|
+
const pipeline = sharp(buffer)
|
|
114
131
|
.resize({ width: maxDimension, fit: "inside", withoutEnlargement: true })
|
|
115
|
-
.jpeg({ quality: 80 })
|
|
116
|
-
|
|
132
|
+
.jpeg({ quality: 80 });
|
|
133
|
+
try {
|
|
134
|
+
return await pipeline.toBuffer();
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
pipeline.destroy();
|
|
138
|
+
}
|
|
117
139
|
}
|
|
118
140
|
catch (err) {
|
|
119
141
|
throw new Error(`Image resize failed: ${err instanceof Error ? err.message : String(err)}`);
|
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)) {
|
|
@@ -145,13 +145,57 @@ export async function scanWorkspaceFiles(cwd, config, logger, manifest, filterPa
|
|
|
145
145
|
const stat = await fs.stat(filePath);
|
|
146
146
|
const entry = manifest.files[normalizedPath];
|
|
147
147
|
if (entry.mtime === stat.mtimeMs && entry.size === stat.size) {
|
|
148
|
+
// Fast path — BUT only when the description config is unchanged:
|
|
149
|
+
// when descHash differs, the worker needs the full content to
|
|
150
|
+
// re-chunk and re-describe the file. Returning empty content here
|
|
151
|
+
// would make chunkFile() yield zero chunks and the pipeline would
|
|
152
|
+
// DELETE the file from the index instead of re-describing it.
|
|
153
|
+
// Mirrors pipeline.ts: `descriptionProvider ? computeDescriptionConfigHash(config) : undefined`.
|
|
154
|
+
const currentDescHash = config.description?.enabled
|
|
155
|
+
? (computeDescriptionConfigHash(config) ?? "")
|
|
156
|
+
: "";
|
|
157
|
+
if (!currentDescHash || entry.descHash === currentDescHash) {
|
|
158
|
+
completed++;
|
|
159
|
+
return {
|
|
160
|
+
filePath,
|
|
161
|
+
normalizedPath,
|
|
162
|
+
content: "",
|
|
163
|
+
hash: entry.hash,
|
|
164
|
+
isEmpty: false,
|
|
165
|
+
isTooSmall: false,
|
|
166
|
+
extractionStatus: "ok",
|
|
167
|
+
mtime: stat.mtimeMs,
|
|
168
|
+
size: stat.size,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
/* stat failed, fall through to full read */
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const isImage = imageVisionProvider !== null && imageExtractor.isImageFile(filePath);
|
|
178
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
179
|
+
const isBinary = pdfExtractor.PDF_EXTENSIONS.has(ext) ||
|
|
180
|
+
docxExtractor.DOCX_EXTENSIONS.has(ext) ||
|
|
181
|
+
docExtractor.DOC_EXTENSIONS.has(ext) ||
|
|
182
|
+
excelExtractor.EXCEL_EXTENSIONS.has(ext) ||
|
|
183
|
+
isImage;
|
|
184
|
+
// Reject oversized binaries BEFORE buffering them — a 2 GB PDF was
|
|
185
|
+
// previously read fully into memory only to be rejected by the
|
|
186
|
+
// 100 MB check inside the PDF extractor.
|
|
187
|
+
if (isBinary && pdfExtractor.PDF_EXTENSIONS.has(ext)) {
|
|
188
|
+
try {
|
|
189
|
+
const stat = await fs.stat(filePath);
|
|
190
|
+
if (stat.size > 100 * 1024 * 1024) {
|
|
191
|
+
logger?.warn(` ${filePath} (PDF exceeds 100 MB — skipping)`);
|
|
148
192
|
completed++;
|
|
149
193
|
return {
|
|
150
194
|
filePath,
|
|
151
195
|
normalizedPath,
|
|
152
196
|
content: "",
|
|
153
|
-
hash:
|
|
154
|
-
isEmpty:
|
|
197
|
+
hash: computeFileHash(""),
|
|
198
|
+
isEmpty: true,
|
|
155
199
|
isTooSmall: false,
|
|
156
200
|
extractionStatus: "ok",
|
|
157
201
|
mtime: stat.mtimeMs,
|
|
@@ -160,16 +204,10 @@ export async function scanWorkspaceFiles(cwd, config, logger, manifest, filterPa
|
|
|
160
204
|
}
|
|
161
205
|
}
|
|
162
206
|
catch {
|
|
163
|
-
/* stat failed, fall through to
|
|
207
|
+
/* stat failed, fall through to read */
|
|
164
208
|
}
|
|
165
209
|
}
|
|
166
|
-
|
|
167
|
-
const isBinary = pdfExtractor.PDF_EXTENSIONS.has(filePath.toLowerCase()) ||
|
|
168
|
-
docxExtractor.DOCX_EXTENSIONS.has(filePath.toLowerCase()) ||
|
|
169
|
-
docExtractor.DOC_EXTENSIONS.has(filePath.toLowerCase()) ||
|
|
170
|
-
excelExtractor.EXCEL_EXTENSIONS.has(filePath.toLowerCase()) ||
|
|
171
|
-
isImage;
|
|
172
|
-
logger?.info(`Reading: ${filePath}`);
|
|
210
|
+
logger?.debug(`Reading: ${filePath}`);
|
|
173
211
|
const buffer = isBinary ? await fs.readFile(filePath) : Buffer.alloc(0);
|
|
174
212
|
// For images, check the persistent description cache before calling the vision provider
|
|
175
213
|
if (isImage && descCache && imageDescConfigHash) {
|
|
@@ -205,15 +243,39 @@ export async function scanWorkspaceFiles(cwd, config, logger, manifest, filterPa
|
|
|
205
243
|
logger?.info(` Describing image: ${filePath}`);
|
|
206
244
|
}
|
|
207
245
|
const result = await dispatchExtraction(filePath, buffer, imageVisionProvider, imagePrompt, imageResizeMaxDimension);
|
|
208
|
-
// Cache the image description for future runs
|
|
246
|
+
// Cache the image description for future runs. Saves are throttled —
|
|
247
|
+
// a full cache rewrite per image was O(n²) I/O for image-heavy workspaces.
|
|
248
|
+
// The pipeline saves the cache again at the end of a pass.
|
|
209
249
|
if (isImage && result.ok && descCache && imageDescConfigHash) {
|
|
210
250
|
const imageBytesHash = computeFileHash(buffer.toString("base64"));
|
|
211
251
|
const cacheKey = DescriptionCache.imageKey(imageBytesHash, imageDescConfigHash);
|
|
212
252
|
descCache.set(cacheKey, result.content);
|
|
213
|
-
|
|
253
|
+
if (completed % 25 === 0) {
|
|
254
|
+
await descCache.save();
|
|
255
|
+
}
|
|
214
256
|
}
|
|
215
257
|
if (!result.ok) {
|
|
216
258
|
logger?.warn(` ${filePath} (extraction failed: ${result.error})`);
|
|
259
|
+
// A transient extraction failure (file lock, antivirus, timeout) must
|
|
260
|
+
// NOT delete a previously-good index entry. Report the file as
|
|
261
|
+
// unchanged with the OLD hash so the worker keeps the old chunks;
|
|
262
|
+
// the entry stays and the file is re-attempted on a later pass.
|
|
263
|
+
const previous = manifest?.files[normalizedPath];
|
|
264
|
+
if (previous) {
|
|
265
|
+
completed++;
|
|
266
|
+
return {
|
|
267
|
+
filePath,
|
|
268
|
+
normalizedPath,
|
|
269
|
+
content: "",
|
|
270
|
+
hash: previous.hash,
|
|
271
|
+
isEmpty: false,
|
|
272
|
+
isTooSmall: false,
|
|
273
|
+
extractionStatus: "failed",
|
|
274
|
+
extractionError: result.error,
|
|
275
|
+
mtime: previous.mtime,
|
|
276
|
+
size: previous.size,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
217
279
|
}
|
|
218
280
|
const content = result.content;
|
|
219
281
|
const byteLength = Buffer.byteLength(content, "utf-8");
|
package/dist/core/bootstrap.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* creates embedder, vector store, keyword index, and description provider.
|
|
4
4
|
*/
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import { loadConfig, findConfigFile, DEFAULT_CONFIG } from "./config.js";
|
|
6
|
+
import { loadConfig, findConfigFile, resolveLogConfig, DEFAULT_CONFIG } from "./config.js";
|
|
7
7
|
import { resolveApiKey } from "./resolve-api-key.js";
|
|
8
8
|
import { loadChunkersFromConfig } from "../chunker/loader.js";
|
|
9
9
|
import { createEmbedder } from "../embedder/factory.js";
|
|
@@ -21,6 +21,10 @@ async function probeDimension(embedder) {
|
|
|
21
21
|
catch {
|
|
22
22
|
// fallback to 384
|
|
23
23
|
}
|
|
24
|
+
// A wrong dimension is only discovered later as cryptic LanceDB errors —
|
|
25
|
+
// surface the fallback loudly so misconfigured providers are easy to spot.
|
|
26
|
+
console.warn("[bootstrap] Could not probe embedding dimension — falling back to 384. " +
|
|
27
|
+
"If indexing later fails with dimension errors, set embedding.vectorDimension explicitly.");
|
|
24
28
|
return 384;
|
|
25
29
|
}
|
|
26
30
|
/** Load the keyword index from disk, or create a new empty one if loading fails. */
|
|
@@ -50,9 +54,12 @@ export async function resolveRagContext(opts = {}) {
|
|
|
50
54
|
await loadChunkersFromConfig(cfg, path.dirname(configPath));
|
|
51
55
|
}
|
|
52
56
|
else {
|
|
53
|
-
|
|
57
|
+
// Deep-clone so resolveApiKey (below) cannot mutate the shared
|
|
58
|
+
// DEFAULT_CONFIG singleton.
|
|
59
|
+
cfg = structuredClone(DEFAULT_CONFIG);
|
|
60
|
+
resolveApiKey(cfg, workDir);
|
|
54
61
|
}
|
|
55
|
-
const logFilePath = path.resolve(workDir, cfg.
|
|
62
|
+
const logFilePath = path.resolve(workDir, resolveLogConfig(cfg).logFilePath);
|
|
56
63
|
const embedder = createEmbedder(cfg);
|
|
57
64
|
const dimension = opts.skipProbe ? 384 : await probeDimension(embedder);
|
|
58
65
|
const storePath = path.resolve(workDir, cfg.vectorStore.path);
|
package/dist/core/config.js
CHANGED
|
@@ -378,6 +378,23 @@ export function validateConfig(config) {
|
|
|
378
378
|
if (config.memory?.autoInjectTopK != null && config.memory.autoInjectTopK < 1) {
|
|
379
379
|
warnings.push("memory.autoInjectTopK must be >= 1");
|
|
380
380
|
}
|
|
381
|
+
if (config.memory?.minConfidence != null) {
|
|
382
|
+
const r = config.memory.minConfidence;
|
|
383
|
+
if (r < 0 || r > 1)
|
|
384
|
+
warnings.push("memory.minConfidence must be between 0 and 1");
|
|
385
|
+
}
|
|
386
|
+
if (config.memory?.autoCaptureDedupThreshold != null) {
|
|
387
|
+
const t = config.memory.autoCaptureDedupThreshold;
|
|
388
|
+
// > 1 would make dedup reject every candidate; < 0 disables it silently
|
|
389
|
+
if (t < 0 || t > 1)
|
|
390
|
+
warnings.push("memory.autoCaptureDedupThreshold must be between 0 and 1");
|
|
391
|
+
}
|
|
392
|
+
if (config.memory?.decay && config.memory.decay.halfLifeDays <= 0) {
|
|
393
|
+
warnings.push("memory.decay.halfLifeDays must be > 0");
|
|
394
|
+
}
|
|
395
|
+
if (config.openCode.maxReadOutputChars != null && config.openCode.maxReadOutputChars <= 0) {
|
|
396
|
+
warnings.push("openCode.maxReadOutputChars must be > 0");
|
|
397
|
+
}
|
|
381
398
|
if (config.openCode.maxContextChunks <= 0) {
|
|
382
399
|
warnings.push("openCode.maxContextChunks must be > 0");
|
|
383
400
|
}
|
|
@@ -480,6 +497,14 @@ export function loadConfig(filePath, validate = true) {
|
|
|
480
497
|
...DEFAULT_CONFIG.retrieval.hybridSearch,
|
|
481
498
|
...(safeObj(parsed.retrieval?.hybridSearch) ?? {}),
|
|
482
499
|
},
|
|
500
|
+
// contextOptimization MUST be nested-merged too — a shallow spread
|
|
501
|
+
// would replace the whole default object with a partial user object,
|
|
502
|
+
// silently leaving maxPerFile/mergeAdjacent/similarityThreshold etc.
|
|
503
|
+
// undefined (which disables the features via NaN comparisons).
|
|
504
|
+
contextOptimization: {
|
|
505
|
+
...DEFAULT_CONFIG.retrieval.contextOptimization,
|
|
506
|
+
...(safeObj(parsed.retrieval?.contextOptimization) ?? {}),
|
|
507
|
+
},
|
|
483
508
|
},
|
|
484
509
|
openCode: (() => {
|
|
485
510
|
const base = DEFAULT_CONFIG.openCode;
|
|
@@ -528,6 +553,12 @@ export function loadConfig(filePath, validate = true) {
|
|
|
528
553
|
memory: {
|
|
529
554
|
...DEFAULT_CONFIG.memory,
|
|
530
555
|
...(safeObj(parsed.memory) ?? {}),
|
|
556
|
+
// decay is a nested object — a user config with only
|
|
557
|
+
// `memory.decay.enabled` must not lose the halfLifeDays default.
|
|
558
|
+
decay: {
|
|
559
|
+
...DEFAULT_CONFIG.memory.decay,
|
|
560
|
+
...(safeObj((safeObj(parsed.memory) ?? {})?.decay) ?? {}),
|
|
561
|
+
},
|
|
531
562
|
},
|
|
532
563
|
ui: {
|
|
533
564
|
...DEFAULT_CONFIG.ui,
|
|
@@ -22,8 +22,14 @@ export declare class DescriptionCache {
|
|
|
22
22
|
has(key: string): boolean;
|
|
23
23
|
/** Persist the cache to disk if dirty. Safe to call multiple times. */
|
|
24
24
|
save(): Promise<void>;
|
|
25
|
-
/**
|
|
26
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Build a cache key for a code chunk.
|
|
27
|
+
*
|
|
28
|
+
* The optional `context` (relative path + line range) is included because
|
|
29
|
+
* the description prompt contains the file path — identical chunk content
|
|
30
|
+
* in two different files must not reuse a contextually wrong description.
|
|
31
|
+
*/
|
|
32
|
+
static codeKey(content: string, descConfigHash: string, context?: string): string;
|
|
27
33
|
/** Build a cache key for an image file. */
|
|
28
34
|
static imageKey(imageBytesHash: string, imageDescConfigHash: string): string;
|
|
29
35
|
/** Remove old entries to stay under the limit. */
|
package/dist/core/desc-cache.js
CHANGED
|
@@ -89,10 +89,17 @@ export class DescriptionCache {
|
|
|
89
89
|
});
|
|
90
90
|
return this.savePromise;
|
|
91
91
|
}
|
|
92
|
-
/**
|
|
93
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Build a cache key for a code chunk.
|
|
94
|
+
*
|
|
95
|
+
* The optional `context` (relative path + line range) is included because
|
|
96
|
+
* the description prompt contains the file path — identical chunk content
|
|
97
|
+
* in two different files must not reuse a contextually wrong description.
|
|
98
|
+
*/
|
|
99
|
+
static codeKey(content, descConfigHash, context) {
|
|
94
100
|
const contentHash = createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
95
|
-
|
|
101
|
+
const contextHash = context ? createHash("sha256").update(context).digest("hex").slice(0, 8) : "";
|
|
102
|
+
return contentHash + "_" + descConfigHash.slice(0, 16) + (contextHash ? "_" + contextHash : "");
|
|
96
103
|
}
|
|
97
104
|
/** Build a cache key for an image file. */
|
|
98
105
|
static imageKey(imageBytesHash, imageDescConfigHash) {
|
|
@@ -47,17 +47,20 @@ export function markFileDocumented(storePath, filePath) {
|
|
|
47
47
|
export function markSubdirectoryDocumented(storePath, subdir, allFilePaths) {
|
|
48
48
|
const normalized = subdir.replace(/\\/g, "/").replace(/\/$/, "");
|
|
49
49
|
const progress = loadDocProgress(storePath);
|
|
50
|
+
// Set lookups avoid the O(n·m) `includes` scan for large workspaces
|
|
51
|
+
const documentedSet = new Set(progress.documented);
|
|
50
52
|
let changed = false;
|
|
51
53
|
for (const filePath of allFilePaths) {
|
|
52
54
|
const normalizedFile = filePath.replace(/\\/g, "/");
|
|
53
55
|
if (normalizedFile.startsWith(normalized + "/") || normalizedFile === normalized) {
|
|
54
|
-
if (!
|
|
55
|
-
|
|
56
|
+
if (!documentedSet.has(filePath)) {
|
|
57
|
+
documentedSet.add(filePath);
|
|
56
58
|
changed = true;
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
62
|
if (changed) {
|
|
63
|
+
progress.documented = [...documentedSet];
|
|
61
64
|
progress.lastUpdated = Date.now();
|
|
62
65
|
saveDocProgress(storePath, progress);
|
|
63
66
|
}
|
|
@@ -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
|
|
@@ -19,3 +19,5 @@ export declare const PROVIDER_DEFAULTS: Record<string, ProviderDefaults>;
|
|
|
19
19
|
export declare function getProviderDefault(provider: string): ProviderDefaults | undefined;
|
|
20
20
|
/** Check whether a given provider uses an OpenAI-compatible API format. */
|
|
21
21
|
export declare function isOpenAiCompatible(provider: string): boolean;
|
|
22
|
+
/** Whether the given provider can produce embeddings. */
|
|
23
|
+
export declare function supportsEmbedding(provider: string): boolean;
|
|
@@ -81,11 +81,26 @@ export const PROVIDER_DEFAULTS = {
|
|
|
81
81
|
export function getProviderDefault(provider) {
|
|
82
82
|
return PROVIDER_DEFAULTS[provider];
|
|
83
83
|
}
|
|
84
|
+
/** OpenAI-compatible providers (embedding + chat via /v1-style APIs). */
|
|
85
|
+
const OPENAI_COMPATIBLE_PROVIDERS = new Set([
|
|
86
|
+
"openai",
|
|
87
|
+
"nvidia",
|
|
88
|
+
"azure",
|
|
89
|
+
"mistral",
|
|
90
|
+
"together",
|
|
91
|
+
"groq",
|
|
92
|
+
"deepseek",
|
|
93
|
+
"fireworks",
|
|
94
|
+
]);
|
|
84
95
|
/** Check whether a given provider uses an OpenAI-compatible API format. */
|
|
85
96
|
export function isOpenAiCompatible(provider) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return
|
|
97
|
+
// Only KNOWN OpenAI-compatible providers qualify — an unknown/typo'd
|
|
98
|
+
// provider name must fail fast instead of silently attempting
|
|
99
|
+
// OpenAI-style calls against a nonsense base URL.
|
|
100
|
+
return OPENAI_COMPATIBLE_PROVIDERS.has(provider);
|
|
101
|
+
}
|
|
102
|
+
/** Whether the given provider can produce embeddings. */
|
|
103
|
+
export function supportsEmbedding(provider) {
|
|
104
|
+
return PROVIDER_DEFAULTS[provider]?.supportsEmbedding ?? false;
|
|
90
105
|
}
|
|
91
106
|
//# sourceMappingURL=provider-defaults.js.map
|
|
@@ -45,6 +45,11 @@ type NpmRunner = (command: string, options: {
|
|
|
45
45
|
export declare function getCurrentVersion(): string;
|
|
46
46
|
/**
|
|
47
47
|
* Compare two semver-ish strings.
|
|
48
|
+
*
|
|
49
|
+
* Pre-release suffixes are stripped before comparison — "1.2.3-beta.1"
|
|
50
|
+
* must compare equal to "1.2.3" (parseInt would turn "3-beta.1" into 3,
|
|
51
|
+
* and non-numeric segments into NaN which compares equal).
|
|
52
|
+
*
|
|
48
53
|
* @returns 1 if a > b, -1 if a < b, 0 if equal.
|
|
49
54
|
*/
|
|
50
55
|
export declare function compareVersions(a: string, b: string): number;
|
|
@@ -38,11 +38,17 @@ function normalizeVersion(tag) {
|
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
40
|
* Compare two semver-ish strings.
|
|
41
|
+
*
|
|
42
|
+
* Pre-release suffixes are stripped before comparison — "1.2.3-beta.1"
|
|
43
|
+
* must compare equal to "1.2.3" (parseInt would turn "3-beta.1" into 3,
|
|
44
|
+
* and non-numeric segments into NaN which compares equal).
|
|
45
|
+
*
|
|
41
46
|
* @returns 1 if a > b, -1 if a < b, 0 if equal.
|
|
42
47
|
*/
|
|
43
48
|
export function compareVersions(a, b) {
|
|
44
|
-
const
|
|
45
|
-
const
|
|
49
|
+
const clean = (s) => s.split("-", 1)[0] ?? s;
|
|
50
|
+
const pa = clean(a).split(".").map((s) => parseInt(s, 10));
|
|
51
|
+
const pb = clean(b).split(".").map((s) => parseInt(s, 10));
|
|
46
52
|
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
47
53
|
const na = pa[i] ?? 0;
|
|
48
54
|
const nb = pb[i] ?? 0;
|
|
@@ -25,8 +25,8 @@ export declare class AnthropicDescriptionProvider implements DescriptionProvider
|
|
|
25
25
|
generateBatchDescriptions(chunks: Chunk[], logger?: DescriptionLogger, opts?: BatchDescriptionOptions): Promise<Map<string, string>>;
|
|
26
26
|
/**
|
|
27
27
|
* Sends a request to the Anthropic Messages API with retry and exponential backoff.
|
|
28
|
-
* The system prompt is
|
|
29
|
-
* the
|
|
28
|
+
* The system prompt is sent via the native `system` field (never concatenated into
|
|
29
|
+
* the user message, so chunk content cannot compete with it).
|
|
30
30
|
*
|
|
31
31
|
* @param messages - The user messages to send.
|
|
32
32
|
* @param timeoutMs - Request timeout in milliseconds.
|
|
@@ -59,8 +59,8 @@ export class AnthropicDescriptionProvider {
|
|
|
59
59
|
}
|
|
60
60
|
/**
|
|
61
61
|
* Sends a request to the Anthropic Messages API with retry and exponential backoff.
|
|
62
|
-
* The system prompt is
|
|
63
|
-
* the
|
|
62
|
+
* The system prompt is sent via the native `system` field (never concatenated into
|
|
63
|
+
* the user message, so chunk content cannot compete with it).
|
|
64
64
|
*
|
|
65
65
|
* @param messages - The user messages to send.
|
|
66
66
|
* @param timeoutMs - Request timeout in milliseconds.
|
|
@@ -74,7 +74,8 @@ export class AnthropicDescriptionProvider {
|
|
|
74
74
|
const body = {
|
|
75
75
|
model: this.config.model,
|
|
76
76
|
max_tokens: 4096,
|
|
77
|
-
|
|
77
|
+
system: systemPrompt,
|
|
78
|
+
messages: messages.map((m) => ({ role: m.role, content: m.content })),
|
|
78
79
|
};
|
|
79
80
|
const headers = {
|
|
80
81
|
"x-api-key": apiKey,
|
|
@@ -85,7 +86,20 @@ export class AnthropicDescriptionProvider {
|
|
|
85
86
|
const retryBaseDelayMs = this.config.retryBaseDelayMs ?? 1000;
|
|
86
87
|
let lastError;
|
|
87
88
|
for (let attempt = 0; attempt <= retryMax; attempt++) {
|
|
88
|
-
|
|
89
|
+
let response;
|
|
90
|
+
try {
|
|
91
|
+
response = await postJson(`${baseUrl}/messages`, body, headers, timeoutMs, this.config.proxy);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
// Network-level failures (ECONNREFUSED, socket timeouts) are transient —
|
|
95
|
+
// treat them like retryable HTTP statuses.
|
|
96
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
97
|
+
if (attempt === retryMax)
|
|
98
|
+
throw lastError;
|
|
99
|
+
const delayMs = retryBaseDelayMs * Math.pow(2, attempt) * (0.8 + Math.random() * 0.4);
|
|
100
|
+
await sleep(delayMs);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
89
103
|
if (response.ok) {
|
|
90
104
|
const json = (await response.json());
|
|
91
105
|
const text = json.content?.[0]?.text;
|
|
@@ -100,7 +114,7 @@ export class AnthropicDescriptionProvider {
|
|
|
100
114
|
throw error;
|
|
101
115
|
}
|
|
102
116
|
lastError = error;
|
|
103
|
-
const delayMs = retryBaseDelayMs * Math.pow(2, attempt);
|
|
117
|
+
const delayMs = retryBaseDelayMs * Math.pow(2, attempt) * (0.8 + Math.random() * 0.4);
|
|
104
118
|
await sleep(delayMs);
|
|
105
119
|
}
|
|
106
120
|
throw lastError ?? new Error("Anthropic LLM request failed: unknown error");
|
|
@@ -85,7 +85,20 @@ export class LlmDescriptionProvider {
|
|
|
85
85
|
const retryBaseDelayMs = this.config.retryBaseDelayMs ?? 1000;
|
|
86
86
|
let lastError;
|
|
87
87
|
for (let attempt = 0; attempt <= retryMax; attempt++) {
|
|
88
|
-
|
|
88
|
+
let response;
|
|
89
|
+
try {
|
|
90
|
+
response = await postJson(url, body, headers, timeoutMs, this.config.proxy);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
// Network-level failures (ECONNREFUSED, socket timeouts) are transient —
|
|
94
|
+
// treat them like retryable HTTP statuses.
|
|
95
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
96
|
+
if (attempt === retryMax)
|
|
97
|
+
throw lastError;
|
|
98
|
+
const delayMs = retryBaseDelayMs * Math.pow(2, attempt) * (0.8 + Math.random() * 0.4);
|
|
99
|
+
await sleep(delayMs);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
89
102
|
if (response.ok) {
|
|
90
103
|
const json = (await response.json());
|
|
91
104
|
return extractResponseText(json, isOllama);
|
|
@@ -96,7 +109,7 @@ export class LlmDescriptionProvider {
|
|
|
96
109
|
throw error;
|
|
97
110
|
}
|
|
98
111
|
lastError = error;
|
|
99
|
-
const delayMs = retryBaseDelayMs * Math.pow(2, attempt);
|
|
112
|
+
const delayMs = retryBaseDelayMs * Math.pow(2, attempt) * (0.8 + Math.random() * 0.4);
|
|
100
113
|
await sleep(delayMs);
|
|
101
114
|
}
|
|
102
115
|
throw lastError ?? new Error("Description LLM request failed: unknown error");
|