opencode-rag-plugin 1.19.4 → 1.19.8
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/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/image.js +5 -0
- package/dist/chunker/pdf.js +30 -14
- package/dist/cli/commands/backend-detect.d.ts +61 -0
- package/dist/cli/commands/backend-detect.js +119 -0
- package/dist/cli/commands/index-command.js +11 -0
- package/dist/cli/commands/init-helpers.d.ts +4 -1
- package/dist/cli/commands/init-helpers.js +21 -4
- package/dist/cli/commands/init.js +61 -24
- package/dist/cli/commands/quirk.js +9 -3
- package/dist/cli/commands/setup.js +12 -3
- 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 +74 -13
- package/dist/core/bootstrap.js +10 -3
- package/dist/core/config.d.ts +27 -1
- package/dist/core/config.js +41 -2
- 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 +32 -4
- package/dist/core/manifest.js +1 -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.d.ts +20 -0
- package/dist/describer/describer.js +132 -16
- package/dist/describer/gemini.js +25 -10
- package/dist/describer/shared.d.ts +28 -0
- package/dist/describer/shared.js +60 -0
- package/dist/embedder/factory.d.ts +5 -3
- package/dist/embedder/factory.js +42 -9
- package/dist/embedder/health.js +19 -19
- package/dist/embedder/http.d.ts +14 -1
- package/dist/embedder/http.js +60 -6
- package/dist/embedder/ollama.d.ts +3 -1
- package/dist/embedder/ollama.js +12 -2
- 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 +511 -346
- 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.js +16 -5
- package/dist/mcp/server.js +3 -0
- package/dist/opencode/create-read-tool.js +14 -3
- package/dist/opencode/tool-args.js +23 -1
- package/dist/plugin.js +66 -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 +104 -8
- package/dist/vectorstore/lancedb.js +345 -71
- package/dist/vectorstore/memory.d.ts +8 -3
- package/dist/vectorstore/memory.js +27 -4
- package/dist/watcher.d.ts +8 -0
- package/dist/watcher.js +237 -85
- package/dist/web/api.d.ts +5 -1
- package/dist/web/api.js +195 -69
- 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
|
@@ -11,12 +11,13 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from "node:fs";
|
|
14
|
-
import { loadConfig } from "../../core/config.js";
|
|
14
|
+
import { loadConfig, findConfigFile } from "../../core/config.js";
|
|
15
15
|
import { checkProviderHealth, pullOllamaModels } from "../../embedder/health.js";
|
|
16
16
|
import { destroyAllPooledConnections } from "../../embedder/http.js";
|
|
17
17
|
import { c } from "../format.js";
|
|
18
18
|
import { getPackageMetadata, readJsonObject, writeJsonFile } from "../helpers.js";
|
|
19
19
|
import { buildOpencodeConfig, buildWorkspacePackageJson, generateDefaultConfigJson, generateSkillFile, generateWorkspacePluginFile, generateWorkspaceTuiPluginFile, installPluginFromGlobal, mergeAgentsMdContent, mergeGitignoreContent, } from "./init-helpers.js";
|
|
20
|
+
import { detectOllamaBackend } from "./backend-detect.js";
|
|
20
21
|
/**
|
|
21
22
|
* Register the `init` command on the given Commander program.
|
|
22
23
|
*
|
|
@@ -29,7 +30,14 @@ import { buildOpencodeConfig, buildWorkspacePackageJson, generateDefaultConfigJs
|
|
|
29
30
|
export function registerInitCommand(program) {
|
|
30
31
|
program
|
|
31
32
|
.command("init")
|
|
32
|
-
.description("Configure
|
|
33
|
+
.description("Configure this workspace (files + auto-tuned opencode-rag.json)")
|
|
34
|
+
.addHelpText("after", "\nUse cases:\n" +
|
|
35
|
+
" - First-time workspace setup: creates .opencode/, the RAG skill file, AGENTS.md\n" +
|
|
36
|
+
" guidance, and opencode-rag.json with embedding batches auto-detected for the\n" +
|
|
37
|
+
" Ollama backend (GPU vs CPU).\n" +
|
|
38
|
+
" - Re-running in an existing workspace: re-syncs plugin/skill files and keeps the\n" +
|
|
39
|
+
" existing opencode-rag.json (overwriting requires interactive confirmation).\n" +
|
|
40
|
+
"\nWorkspace-level step — run AFTER 'opencode-rag setup' on this machine, then 'opencode-rag index'.\n")
|
|
33
41
|
.option("-f, --force", "overwrite existing files")
|
|
34
42
|
.option("--skip-install", "skip installing workspace-local plugin dependencies")
|
|
35
43
|
.option("--skip-health-check", "skip provider connectivity and model availability check")
|
|
@@ -37,7 +45,10 @@ export function registerInitCommand(program) {
|
|
|
37
45
|
try {
|
|
38
46
|
const cwd = process.cwd();
|
|
39
47
|
const packageMetadata = getPackageMetadata();
|
|
40
|
-
|
|
48
|
+
// Use findConfigFile so an existing config in .opencode/rag.json (or
|
|
49
|
+
// .opencode/opencode-rag.json) is respected instead of being shadowed
|
|
50
|
+
// by a new root-level opencode-rag.json.
|
|
51
|
+
const configPath = findConfigFile(cwd) ?? path.join(cwd, "opencode-rag.json");
|
|
41
52
|
const opencodeDir = path.join(cwd, ".opencode");
|
|
42
53
|
const gitignorePath = path.join(opencodeDir, ".gitignore");
|
|
43
54
|
const opencodeConfigPath = path.join(opencodeDir, "opencode.json");
|
|
@@ -186,8 +197,27 @@ export function registerInitCommand(program) {
|
|
|
186
197
|
console.log(` ${c.exists("Exists:")} .opencode/package.json`);
|
|
187
198
|
}
|
|
188
199
|
const configExists = existsSync(configPath);
|
|
200
|
+
// Detect the Ollama backend (CPU vs GPU) lazily — only when we are
|
|
201
|
+
// actually about to write a config — and tune embedding batches for it.
|
|
202
|
+
let detectedTuning;
|
|
203
|
+
const configContent = async () => {
|
|
204
|
+
if (!detectedTuning) {
|
|
205
|
+
try {
|
|
206
|
+
const info = await detectOllamaBackend();
|
|
207
|
+
detectedTuning = info.tuning;
|
|
208
|
+
const icon = info.backend === "gpu" ? c.success("GPU:") :
|
|
209
|
+
info.backend === "cpu" ? c.warn("CPU:") :
|
|
210
|
+
c.dim("Backend:");
|
|
211
|
+
console.log(` ${icon} ${info.message}`);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
detectedTuning = undefined;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return generateDefaultConfigJson(detectedTuning);
|
|
218
|
+
};
|
|
189
219
|
if (!configExists) {
|
|
190
|
-
writeFileSync(configPath,
|
|
220
|
+
writeFileSync(configPath, await configContent(), "utf-8");
|
|
191
221
|
console.log(` ${c.created("Created:")} opencode-rag.json`);
|
|
192
222
|
}
|
|
193
223
|
else {
|
|
@@ -205,7 +235,7 @@ export function registerInitCommand(program) {
|
|
|
205
235
|
if (overwrite) {
|
|
206
236
|
copyFileSync(configPath, `${configPath}.bak`);
|
|
207
237
|
console.log(` ${c.dim("Backup:")} opencode-rag.json.bak`);
|
|
208
|
-
writeFileSync(configPath,
|
|
238
|
+
writeFileSync(configPath, await configContent(), "utf-8");
|
|
209
239
|
console.log(` ${c.updated("Updated:")} opencode-rag.json`);
|
|
210
240
|
}
|
|
211
241
|
else {
|
|
@@ -253,27 +283,34 @@ export function registerInitCommand(program) {
|
|
|
253
283
|
return { model: r.model, baseUrl: ragConfig.embedding.baseUrl, proxy: ragConfig.embedding.proxy };
|
|
254
284
|
});
|
|
255
285
|
console.log(`\n ${c.warn("Models not found:")} ${pullEntries.map((e) => e.model).join(", ")}`);
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
});
|
|
261
|
-
rl.close();
|
|
262
|
-
if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
|
|
263
|
-
console.log();
|
|
264
|
-
try {
|
|
265
|
-
await pullOllamaModels(pullEntries, (model, line) => {
|
|
266
|
-
console.log(` ${c.value(model)}: ${line}`);
|
|
267
|
-
});
|
|
268
|
-
console.log(`\n ${c.success("Models pulled successfully.")}`);
|
|
269
|
-
}
|
|
270
|
-
catch (err) {
|
|
271
|
-
console.error(`\n ${c.error("Pull failed:")} ${err.message}`);
|
|
272
|
-
console.log(` ${c.dim("Pull manually with: ollama pull <model>")}`);
|
|
273
|
-
}
|
|
286
|
+
// Non-TTY guard: rl.question would block forever with a still-open
|
|
287
|
+
// stdin pipe (e.g. `echo y | opencode-rag init`, CI runners).
|
|
288
|
+
if (!process.stdin.isTTY) {
|
|
289
|
+
console.log(` ${c.dim("Non-interactive shell — skipping. Pull manually with: ollama pull <model>")}`);
|
|
274
290
|
}
|
|
275
291
|
else {
|
|
276
|
-
|
|
292
|
+
const readline = await import("node:readline");
|
|
293
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
294
|
+
const answer = await new Promise((resolve) => {
|
|
295
|
+
rl.question(` Pull ${pullEntries.length === 1 ? "this model" : "these models"} now? (y/n) `, resolve);
|
|
296
|
+
});
|
|
297
|
+
rl.close();
|
|
298
|
+
if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
|
|
299
|
+
console.log();
|
|
300
|
+
try {
|
|
301
|
+
await pullOllamaModels(pullEntries, (model, line) => {
|
|
302
|
+
console.log(` ${c.value(model)}: ${line}`);
|
|
303
|
+
});
|
|
304
|
+
console.log(`\n ${c.success("Models pulled successfully.")}`);
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
console.error(`\n ${c.error("Pull failed:")} ${err.message}`);
|
|
308
|
+
console.log(` ${c.dim("Pull manually with: ollama pull <model>")}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
console.log(` ${c.dim("Skipped. Pull manually with: ollama pull <model>")}`);
|
|
313
|
+
}
|
|
277
314
|
}
|
|
278
315
|
}
|
|
279
316
|
const hasErrors = results.some((r) => r.status === "error");
|
|
@@ -16,6 +16,7 @@ export function registerQuirkCommand(program) {
|
|
|
16
16
|
.option("-t, --type <type>", "quirk type: gotcha, preference, decision, environment-constraint")
|
|
17
17
|
.option("--tag <tags...>", "tags for filtering")
|
|
18
18
|
.option("--source-ref <path>", "source file path reference")
|
|
19
|
+
.option("-c, --config <path>", "path to config file")
|
|
19
20
|
.action(async (content, options) => {
|
|
20
21
|
try {
|
|
21
22
|
const ctx = await resolveCliContext(options, resolveLogPath());
|
|
@@ -49,6 +50,11 @@ export function registerQuirkCommand(program) {
|
|
|
49
50
|
.option("-c, --config <path>", "path to config file")
|
|
50
51
|
.action(async (id, options) => {
|
|
51
52
|
try {
|
|
53
|
+
const confidence = options.confidence;
|
|
54
|
+
if (confidence !== undefined && (Number.isNaN(confidence) || confidence < 0 || confidence > 1)) {
|
|
55
|
+
logCliError(resolveLogPath(), "quirk update", "Failed to update quirk: --confidence must be a number between 0 and 1");
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
52
58
|
const ctx = await resolveCliContext(options, resolveLogPath());
|
|
53
59
|
const { config, embedder, store, keywordIndex } = ctx;
|
|
54
60
|
const tags = options.tag;
|
|
@@ -56,7 +62,7 @@ export function registerQuirkCommand(program) {
|
|
|
56
62
|
content: options.content,
|
|
57
63
|
quirkType: options.type,
|
|
58
64
|
tags: tags ? (Array.isArray(tags) ? tags : [tags]) : undefined,
|
|
59
|
-
confidence
|
|
65
|
+
confidence,
|
|
60
66
|
sourceRef: options.sourceRef,
|
|
61
67
|
});
|
|
62
68
|
logCliInfo(ctx.logFilePath, "quirk update", `\n${c.success("Quirk updated:")}`);
|
|
@@ -148,7 +154,7 @@ export function registerQuirkCommand(program) {
|
|
|
148
154
|
const { config, embedder, store, keywordIndex } = ctx;
|
|
149
155
|
const results = await recallQuirks({ embedder, store, keywordIndex: keywordIndex, cfg: config, storePath: ctx.storePath }, content, { topK: 5 });
|
|
150
156
|
if (results.length > 0) {
|
|
151
|
-
logCliInfo(ctx.logFilePath, "quirk test", c.
|
|
157
|
+
logCliInfo(ctx.logFilePath, "quirk test", c.warn("\n✗ Similar quirk(s) already exist — quirk has NOT been appended:\n"));
|
|
152
158
|
for (const r of results) {
|
|
153
159
|
const badge = r.chunk.metadata.quirkType ? `[${r.chunk.metadata.quirkType}] ` : "";
|
|
154
160
|
const tags = r.chunk.metadata.tags?.length ? ` (${r.chunk.metadata.tags.join(", ")})` : "";
|
|
@@ -159,7 +165,7 @@ export function registerQuirkCommand(program) {
|
|
|
159
165
|
}
|
|
160
166
|
}
|
|
161
167
|
else {
|
|
162
|
-
logCliInfo(ctx.logFilePath, "quirk test", c.
|
|
168
|
+
logCliInfo(ctx.logFilePath, "quirk test", c.success("\n✓ No matching quirk found — safe to append\n"));
|
|
163
169
|
}
|
|
164
170
|
await cleanupContext(ctx);
|
|
165
171
|
}
|
|
@@ -36,7 +36,13 @@ function checkOpenCodeRunning() {
|
|
|
36
36
|
export function registerSetupCommand(program) {
|
|
37
37
|
program
|
|
38
38
|
.command("setup")
|
|
39
|
-
.description("
|
|
39
|
+
.description("Install/update the OpenCodeRAG runtime once per machine")
|
|
40
|
+
.addHelpText("after", "\nUse cases:\n" +
|
|
41
|
+
" - First-time install: run once per machine to install the plugin runtime\n" +
|
|
42
|
+
" into ~/.opencode/ so OpenCode can discover the RAG plugin.\n" +
|
|
43
|
+
" - Updating: re-sync the runtime to the published plugin version.\n" +
|
|
44
|
+
" - Troubleshooting: use --check to inspect the runtime, or --force to reinstall.\n" +
|
|
45
|
+
"\nMachine-level step — run BEFORE 'opencode-rag init' (init configures each workspace).\n")
|
|
40
46
|
.option("--uninstall", "remove the runtime and cleanup")
|
|
41
47
|
.option("-f, --force", "force re-setup even if up-to-date")
|
|
42
48
|
.option("--check", "check whether the runtime is correctly installed")
|
|
@@ -81,9 +87,12 @@ export function registerSetupCommand(program) {
|
|
|
81
87
|
if (options.uninstall) {
|
|
82
88
|
console.log(`\n${c.heading("Removing OpenCodeRAG runtime...")}\n`);
|
|
83
89
|
removeIfExists(runtimePluginDir);
|
|
84
|
-
|
|
90
|
+
// Only remove the @opencode-ai/plugin SDK package — the scope dir may
|
|
91
|
+
// be shared with other OpenCode plugins/tools.
|
|
92
|
+
removeIfExists(runtimeSdkPluginDir);
|
|
85
93
|
removeIfExists(versionFile);
|
|
86
|
-
console.log(` ${c.updated("Removed:")} ${c.file(
|
|
94
|
+
console.log(` ${c.updated("Removed:")} ${c.file(runtimePluginDir)}`);
|
|
95
|
+
console.log(` ${c.updated("Removed:")} ${c.file(runtimeSdkPluginDir)}`);
|
|
87
96
|
console.log(`\n ${c.success("Done.")} Run ${c.file("npm uninstall -g opencode-rag-plugin")} to remove the global package.\n`);
|
|
88
97
|
return;
|
|
89
98
|
}
|
|
@@ -147,14 +147,23 @@ export function registerStatusCommand(program) {
|
|
|
147
147
|
logCliInfo(logFilePath, "status", `${c.label("Runtime:")} ${c.warn("version unknown — run `opencode-rag setup`")}`);
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
|
-
//
|
|
151
|
-
//
|
|
150
|
+
// GitHub update check. Runs unless autoUpdate is explicitly disabled.
|
|
151
|
+
// Awaited (raced against 3s) so the result can actually print —
|
|
152
|
+
// process.exit(0) below would previously kill the promise before it
|
|
153
|
+
// resolved, making the Update: line dead code.
|
|
152
154
|
if (config.autoUpdate?.enabled) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
+
try {
|
|
156
|
+
const info = await Promise.race([
|
|
157
|
+
checkForUpdate(pkg.version),
|
|
158
|
+
new Promise((resolve) => setTimeout(() => resolve(null), 3000)),
|
|
159
|
+
]);
|
|
160
|
+
if (info && info.updateAvailable) {
|
|
155
161
|
process.stdout.write(` ${c.label("Update:")} ${c.warn(`v${info.latestVersion} available — run \`opencode-rag update\` to install`)}\n`);
|
|
156
162
|
}
|
|
157
|
-
}
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
/* ignore network errors */
|
|
166
|
+
}
|
|
158
167
|
}
|
|
159
168
|
// Force exit — avoid LanceDB close() hanging on Windows native bindings.
|
|
160
169
|
// Status is read-only so there's no state to lose.
|
package/dist/cli/commands/ui.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* `ui` command — starts a local web UI for browsing the vector database.
|
|
6
6
|
*/
|
|
7
7
|
import path from "node:path";
|
|
8
|
-
import { c, resolveCliContext, logCliError, logCliInfo } from "../format.js";
|
|
8
|
+
import { c, resolveCliContext, cleanupContext, logCliError, logCliInfo } from "../format.js";
|
|
9
9
|
/**
|
|
10
10
|
* Register the `ui` command on the given Commander program.
|
|
11
11
|
*
|
|
@@ -33,9 +33,10 @@ export function registerUiCommand(program) {
|
|
|
33
33
|
const openBrowser = options.open !== false && (config.ui?.openBrowser ?? true);
|
|
34
34
|
const { startWebUi } = await import("../../web/server.js");
|
|
35
35
|
const server = await startWebUi(storePath, port, cwd, config.embedding.vectorDimension ?? 384, config);
|
|
36
|
-
const url = `http://127.0.0.1:${server.port}`;
|
|
36
|
+
const url = `http://127.0.0.1:${server.port}/?token=${server.token}`;
|
|
37
37
|
logCliInfo(logFilePath, "ui", `\n${c.heading("OpenCodeRAG Web UI")}`);
|
|
38
38
|
logCliInfo(logFilePath, "ui", ` ${c.label("URL:")} ${c.value(url)}`);
|
|
39
|
+
logCliInfo(logFilePath, "ui", ` ${c.dim("The token in the URL authenticates this session — keep it private.")}`);
|
|
39
40
|
logCliInfo(logFilePath, "ui", ` ${c.dim("Press Ctrl+C to stop")}\n`);
|
|
40
41
|
if (openBrowser) {
|
|
41
42
|
const { spawn } = await import("node:child_process");
|
|
@@ -52,14 +53,27 @@ export function registerUiCommand(program) {
|
|
|
52
53
|
console.error(c.dim(`Could not open browser automatically. Open ${url} manually.`));
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
let shuttingDown = false;
|
|
57
|
+
const shutdown = async () => {
|
|
58
|
+
if (shuttingDown)
|
|
59
|
+
return;
|
|
60
|
+
shuttingDown = true;
|
|
61
|
+
try {
|
|
62
|
+
await server.close();
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// best-effort — the process is exiting anyway
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
await cleanupContext(ctx);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// best-effort cleanup
|
|
72
|
+
}
|
|
61
73
|
process.exit(0);
|
|
62
|
-
}
|
|
74
|
+
};
|
|
75
|
+
process.on("SIGINT", shutdown);
|
|
76
|
+
process.on("SIGTERM", shutdown);
|
|
63
77
|
}
|
|
64
78
|
catch (err) {
|
|
65
79
|
const message = err.message || String(err);
|
|
@@ -24,11 +24,10 @@ export function registerUpdateCommand(program) {
|
|
|
24
24
|
console.log(`\n${c.heading("OpenCodeRAG Update")}\n`);
|
|
25
25
|
console.log(` ${c.label("Current version:")} ${c.value(currentVersion)}`);
|
|
26
26
|
console.log(` ${c.label("Checking...")} `);
|
|
27
|
-
|
|
28
|
-
try
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
catch {
|
|
27
|
+
// checkForUpdate never throws (all failures collapse to "no update"),
|
|
28
|
+
// so the try/catch below is defensive only.
|
|
29
|
+
const info = await checkForUpdate(currentVersion).catch(() => null);
|
|
30
|
+
if (!info) {
|
|
32
31
|
console.log(`\n ${c.warn("Could not reach the update server. Check your network and try again.")}\n`);
|
|
33
32
|
process.exit(1);
|
|
34
33
|
return;
|
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
|
@@ -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,17 +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 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) ||
|
|
172
|
-
isImage;
|
|
173
|
-
logger?.info(`Reading: ${filePath}`);
|
|
210
|
+
logger?.debug(`Reading: ${filePath}`);
|
|
174
211
|
const buffer = isBinary ? await fs.readFile(filePath) : Buffer.alloc(0);
|
|
175
212
|
// For images, check the persistent description cache before calling the vision provider
|
|
176
213
|
if (isImage && descCache && imageDescConfigHash) {
|
|
@@ -206,15 +243,39 @@ export async function scanWorkspaceFiles(cwd, config, logger, manifest, filterPa
|
|
|
206
243
|
logger?.info(` Describing image: ${filePath}`);
|
|
207
244
|
}
|
|
208
245
|
const result = await dispatchExtraction(filePath, buffer, imageVisionProvider, imagePrompt, imageResizeMaxDimension);
|
|
209
|
-
// 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.
|
|
210
249
|
if (isImage && result.ok && descCache && imageDescConfigHash) {
|
|
211
250
|
const imageBytesHash = computeFileHash(buffer.toString("base64"));
|
|
212
251
|
const cacheKey = DescriptionCache.imageKey(imageBytesHash, imageDescConfigHash);
|
|
213
252
|
descCache.set(cacheKey, result.content);
|
|
214
|
-
|
|
253
|
+
if (completed % 25 === 0) {
|
|
254
|
+
await descCache.save();
|
|
255
|
+
}
|
|
215
256
|
}
|
|
216
257
|
if (!result.ok) {
|
|
217
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
|
+
}
|
|
218
279
|
}
|
|
219
280
|
const content = result.content;
|
|
220
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);
|