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
package/dist/watcher.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import chokidar from "chokidar";
|
|
7
7
|
import path from "node:path";
|
|
8
|
-
import { writeFileSync, unlinkSync, existsSync } from "node:fs";
|
|
8
|
+
import { writeFileSync, unlinkSync, existsSync, readFileSync, openSync, closeSync } from "node:fs";
|
|
9
9
|
import { appendDebugLog } from "./core/fileLogger.js";
|
|
10
10
|
import { isCorruptionError } from "./vectorstore/lancedb.js";
|
|
11
11
|
import { createWatchPassScheduler, createWatchIgnore, runIndexPass, } from "./indexer.js";
|
|
@@ -18,6 +18,81 @@ function writeWatcherStatus(storePath, status) {
|
|
|
18
18
|
// silently ignore write errors
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
// ── Cross-process watcher claim lock ────────────────────────────────────────
|
|
22
|
+
// `index.lock` only serializes individual index passes — it does NOT stop N
|
|
23
|
+
// processes (N OpenCode sessions, or a session + `opencode-rag index --watch`)
|
|
24
|
+
// from each spawning their own chokidar watcher for the same workspace. Every
|
|
25
|
+
// extra watcher fires its own fire-and-forget initial pass (all but one skip
|
|
26
|
+
// the pass lock and then retry every 30s) and burns file-watcher resources.
|
|
27
|
+
// The claim lock below guarantees at most ONE active watcher per workspace:
|
|
28
|
+
// the first process to claim it runs the watcher; the others stay dormant and
|
|
29
|
+
// periodically re-check so they take over if the owning process exits.
|
|
30
|
+
const WATCHER_LOCK_FILE = "watcher.lock";
|
|
31
|
+
/** How often a dormant indexer re-checks whether the watcher lock is free. */
|
|
32
|
+
const WATCHER_RECHECK_MS = 60_000;
|
|
33
|
+
function isPidAlive(pid) {
|
|
34
|
+
try {
|
|
35
|
+
process.kill(pid, 0);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function readWatcherLock(storePath) {
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(readFileSync(path.join(storePath, WATCHER_LOCK_FILE), "utf-8"));
|
|
45
|
+
return typeof parsed.pid === "number" ? parsed : undefined;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Atomically claim the watcher lock for a workspace. Returns true only if
|
|
53
|
+
* this process is now the active watcher. A stale lock (dead PID) or an
|
|
54
|
+
* unreadable/corrupt lock file is cleared and re-claimed.
|
|
55
|
+
*/
|
|
56
|
+
export function tryAcquireWatcherLock(storePath) {
|
|
57
|
+
const lockPath = path.join(storePath, WATCHER_LOCK_FILE);
|
|
58
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
59
|
+
const existing = readWatcherLock(storePath);
|
|
60
|
+
if (!existing || !isPidAlive(existing.pid)) {
|
|
61
|
+
// Stale, corrupt, or missing lock — clear it and claim atomically
|
|
62
|
+
// (O_EXCL create so two racing processes cannot both win).
|
|
63
|
+
try {
|
|
64
|
+
unlinkSync(lockPath);
|
|
65
|
+
}
|
|
66
|
+
catch { /* may not exist */ }
|
|
67
|
+
try {
|
|
68
|
+
const fd = openSync(lockPath, "wx");
|
|
69
|
+
try {
|
|
70
|
+
writeFileSync(fd, JSON.stringify({ pid: process.pid, startedAt: Date.now() }), "utf-8");
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
closeSync(fd);
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Someone else claimed it between our unlink and create — retry once.
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
/** Release the watcher lock — only if this process owns it. */
|
|
87
|
+
export function releaseWatcherLock(storePath) {
|
|
88
|
+
const lock = readWatcherLock(storePath);
|
|
89
|
+
if (lock?.pid === process.pid) {
|
|
90
|
+
try {
|
|
91
|
+
unlinkSync(path.join(storePath, WATCHER_LOCK_FILE));
|
|
92
|
+
}
|
|
93
|
+
catch { /* ignore */ }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
21
96
|
/**
|
|
22
97
|
* Create a background file watcher that automatically re-indexes the
|
|
23
98
|
* workspace when files change. Uses chokidar for file system events and
|
|
@@ -29,98 +104,134 @@ function writeWatcherStatus(storePath, status) {
|
|
|
29
104
|
*/
|
|
30
105
|
export function createBackgroundIndexer(options) {
|
|
31
106
|
const { cwd, storePath, config, store, embedder, logFilePath, logLevel, keywordIndex, descriptionProvider, dimension } = options;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
107
|
+
const autoIndexCfg = config.openCode.autoIndex ?? { enabled: false, debounceMs: 5000, intervalMs: 300000 };
|
|
108
|
+
let active = false;
|
|
109
|
+
let recheckTimer;
|
|
110
|
+
let stopActive;
|
|
111
|
+
/** Start the chokidar watcher, debounced scheduler, and initial pass. */
|
|
112
|
+
const startActive = () => {
|
|
113
|
+
if (active)
|
|
114
|
+
return;
|
|
115
|
+
active = true;
|
|
116
|
+
writeWatcherStatus(storePath, { running: false, lastRunAt: undefined });
|
|
117
|
+
const ac = new AbortController();
|
|
118
|
+
const updateStatus = (partial) => {
|
|
119
|
+
writeWatcherStatus(storePath, { running: false, lastRunAt: undefined, ...partial });
|
|
120
|
+
};
|
|
121
|
+
const runPass = async (filterPaths) => {
|
|
122
|
+
updateStatus({ running: true, lastRunAt: Date.now() });
|
|
123
|
+
try {
|
|
124
|
+
const stats = await runIndexPass({
|
|
125
|
+
cwd,
|
|
126
|
+
storePath,
|
|
127
|
+
config,
|
|
128
|
+
store,
|
|
129
|
+
embedder,
|
|
130
|
+
keywordIndex,
|
|
131
|
+
descriptionProvider,
|
|
132
|
+
dimension,
|
|
133
|
+
filterPaths,
|
|
134
|
+
abortSignal: ac.signal,
|
|
135
|
+
logger: {
|
|
136
|
+
info: (message) => appendDebugLog(logFilePath, { scope: "autoIndex", message }, logLevel),
|
|
137
|
+
warn: (message) => appendDebugLog(logFilePath, { scope: "autoIndex", message }, logLevel),
|
|
138
|
+
debug: (message) => appendDebugLog(logFilePath, { scope: "autoIndex", message: `DEBUG: ${message}`, severity: "debug" }, logLevel),
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
// A lock-skipped pass did NO work — retry shortly so the workspace
|
|
142
|
+
// does not stay unindexed until the next file event.
|
|
143
|
+
if (stats.skipped) {
|
|
144
|
+
appendDebugLog(logFilePath, {
|
|
145
|
+
scope: "autoIndex",
|
|
146
|
+
message: "Index pass skipped (another pass holds the lock) — retrying in 30s",
|
|
147
|
+
}, logLevel);
|
|
148
|
+
if (!ac.signal.aborted) {
|
|
149
|
+
setTimeout(() => {
|
|
150
|
+
if (!ac.signal.aborted)
|
|
151
|
+
scheduler.notifyChange(filterPaths);
|
|
152
|
+
}, 30_000).unref();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
updateStatus({ running: false, lastRunAt: Date.now() });
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
66
158
|
appendDebugLog(logFilePath, {
|
|
67
159
|
scope: "autoIndex",
|
|
68
|
-
message: "
|
|
160
|
+
message: "Watch reindex pass failed",
|
|
161
|
+
error: err,
|
|
69
162
|
}, logLevel);
|
|
163
|
+
if (isCorruptionError(err)) {
|
|
164
|
+
appendDebugLog(logFilePath, {
|
|
165
|
+
scope: "autoIndex",
|
|
166
|
+
message: "Corruption detected — run 'opencode-rag index --force' to rebuild manually",
|
|
167
|
+
}, logLevel);
|
|
168
|
+
}
|
|
169
|
+
updateStatus({ running: false, lastRunAt: Date.now() });
|
|
70
170
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
async
|
|
171
|
+
};
|
|
172
|
+
// Fire-and-forget initial index pass
|
|
173
|
+
runPass().catch((err) => {
|
|
174
|
+
appendDebugLog(logFilePath, {
|
|
175
|
+
scope: "autoIndex",
|
|
176
|
+
message: "Initial index pass failed",
|
|
177
|
+
error: err,
|
|
178
|
+
}, logLevel);
|
|
179
|
+
});
|
|
180
|
+
const scheduler = createWatchPassScheduler(runPass, (error) => {
|
|
181
|
+
const message = error.message || String(error);
|
|
182
|
+
appendDebugLog(logFilePath, {
|
|
183
|
+
scope: "autoIndex",
|
|
184
|
+
message: `Watch reindex failed: ${message}`,
|
|
185
|
+
error,
|
|
186
|
+
}, logLevel);
|
|
187
|
+
}, autoIndexCfg.debounceMs);
|
|
188
|
+
const watcher = chokidar.watch(cwd, {
|
|
189
|
+
ignored: createWatchIgnore(cwd, config, storePath),
|
|
190
|
+
ignoreInitial: true,
|
|
191
|
+
persistent: true,
|
|
192
|
+
});
|
|
193
|
+
const handleChange = (filePath) => scheduler.notifyChange(filePath ? [filePath] : undefined);
|
|
194
|
+
watcher.on("add", handleChange);
|
|
195
|
+
watcher.on("change", handleChange);
|
|
196
|
+
watcher.on("unlink", handleChange);
|
|
197
|
+
watcher.on("unlinkDir", handleChange);
|
|
198
|
+
watcher.on("addDir", handleChange);
|
|
199
|
+
watcher.on("error", (error) => {
|
|
200
|
+
appendDebugLog(logFilePath, {
|
|
201
|
+
scope: "autoIndex",
|
|
202
|
+
message: `Watcher error: ${error.message}`,
|
|
203
|
+
error,
|
|
204
|
+
}, logLevel);
|
|
205
|
+
});
|
|
206
|
+
// Periodic timer: only needed for git backend (chokidar gets real FS events).
|
|
207
|
+
// Note: with git mode BOTH backends run — the scheduler coalesces redundant
|
|
208
|
+
// passes into one, so this only adds a safety net for missed events.
|
|
209
|
+
const watcherBackend = autoIndexCfg.watcher ?? "chokidar";
|
|
210
|
+
const periodicTimer = watcherBackend === "git"
|
|
211
|
+
? setInterval(() => {
|
|
212
|
+
scheduler.notifyChange();
|
|
213
|
+
}, autoIndexCfg.intervalMs)
|
|
214
|
+
: undefined;
|
|
215
|
+
// Never keep the process alive just for the periodic scan
|
|
216
|
+
periodicTimer?.unref();
|
|
217
|
+
stopActive = async () => {
|
|
218
|
+
if (!active)
|
|
219
|
+
return;
|
|
220
|
+
active = false;
|
|
118
221
|
if (periodicTimer)
|
|
119
222
|
clearInterval(periodicTimer);
|
|
120
223
|
ac.abort();
|
|
121
224
|
scheduler.close();
|
|
122
|
-
|
|
123
|
-
|
|
225
|
+
// chokidar close() can hang on some Windows setups — guard it like
|
|
226
|
+
// the CLI does.
|
|
227
|
+
await Promise.race([
|
|
228
|
+
scheduler.waitForIdle(),
|
|
229
|
+
new Promise((resolve) => setTimeout(resolve, 5000).unref()),
|
|
230
|
+
]);
|
|
231
|
+
await Promise.race([
|
|
232
|
+
watcher.close(),
|
|
233
|
+
new Promise((resolve) => setTimeout(resolve, 5000).unref()),
|
|
234
|
+
]);
|
|
124
235
|
const statusPath = path.join(storePath, "watcher-status.json");
|
|
125
236
|
if (existsSync(statusPath)) {
|
|
126
237
|
try {
|
|
@@ -128,10 +239,51 @@ export function createBackgroundIndexer(options) {
|
|
|
128
239
|
}
|
|
129
240
|
catch { /* ignore */ }
|
|
130
241
|
}
|
|
242
|
+
releaseWatcherLock(storePath);
|
|
131
243
|
appendDebugLog(logFilePath, {
|
|
132
244
|
scope: "autoIndex",
|
|
133
245
|
message: "Background indexer shut down",
|
|
134
246
|
});
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
if (tryAcquireWatcherLock(storePath)) {
|
|
250
|
+
startActive();
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
// Another process already runs the watcher for this workspace (a second
|
|
254
|
+
// OpenCode session, `opencode-rag index --watch`, …). Stay dormant and
|
|
255
|
+
// re-check periodically so this process takes over once the owner exits.
|
|
256
|
+
const owner = readWatcherLock(storePath);
|
|
257
|
+
appendDebugLog(logFilePath, {
|
|
258
|
+
scope: "autoIndex",
|
|
259
|
+
message: owner?.pid
|
|
260
|
+
? `Watcher already running for this workspace (PID ${owner.pid}) — skipping duplicate watcher`
|
|
261
|
+
: "Watcher already running for this workspace — skipping duplicate watcher",
|
|
262
|
+
}, logLevel);
|
|
263
|
+
recheckTimer = setInterval(() => {
|
|
264
|
+
if (!active && tryAcquireWatcherLock(storePath)) {
|
|
265
|
+
appendDebugLog(logFilePath, {
|
|
266
|
+
scope: "autoIndex",
|
|
267
|
+
message: "Previous watcher released this workspace — taking over",
|
|
268
|
+
}, logLevel);
|
|
269
|
+
if (recheckTimer) {
|
|
270
|
+
clearInterval(recheckTimer);
|
|
271
|
+
recheckTimer = undefined;
|
|
272
|
+
}
|
|
273
|
+
startActive();
|
|
274
|
+
}
|
|
275
|
+
}, WATCHER_RECHECK_MS);
|
|
276
|
+
recheckTimer.unref();
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
async close() {
|
|
280
|
+
if (recheckTimer) {
|
|
281
|
+
clearInterval(recheckTimer);
|
|
282
|
+
recheckTimer = undefined;
|
|
283
|
+
}
|
|
284
|
+
if (stopActive) {
|
|
285
|
+
await stopActive();
|
|
286
|
+
}
|
|
135
287
|
},
|
|
136
288
|
};
|
|
137
289
|
}
|
package/dist/web/api.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ interface ApiResponse {
|
|
|
28
28
|
* @param cfg - Active RAG configuration (used by quirk endpoints).
|
|
29
29
|
* @returns An async handler that returns `true` when a route matched or `false` otherwise.
|
|
30
30
|
*/
|
|
31
|
-
export declare function createApiHandler(store: LanceDbStore, keywordIndex: KeywordIndex, storePath: string, cwd?: string, cfg?: RagConfig, getEmbedder?: () => Promise<EmbeddingProvider
|
|
31
|
+
export declare function createApiHandler(store: LanceDbStore, keywordIndex: KeywordIndex, storePath: string, cwd?: string, cfg?: RagConfig, getEmbedder?: () => Promise<EmbeddingProvider>, token?: string): (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
|
|
32
32
|
/**
|
|
33
33
|
* Perform token-usage analysis for a single evaluation session.
|
|
34
34
|
*
|
|
@@ -52,4 +52,8 @@ export declare function handleEvalTokenCompare(storePath: string, params: URLSea
|
|
|
52
52
|
* `avgReadsPerQueryWithRAG`, `queryCount`.
|
|
53
53
|
*/
|
|
54
54
|
export declare function handleEvalProjectSavings(body: unknown): ApiResponse;
|
|
55
|
+
/** Thrown when the request body exceeds {@link MAX_BODY_BYTES}; mapped to a 413 response. */
|
|
56
|
+
export declare class BodyTooLargeError extends Error {
|
|
57
|
+
constructor();
|
|
58
|
+
}
|
|
55
59
|
export {};
|