opencode-rag-plugin 1.19.5 → 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/image.js +5 -0
- 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 +6 -2
- package/dist/cli/commands/init.js +30 -3
- package/dist/cli/commands/setup.js +7 -1
- package/dist/core/config.d.ts +27 -1
- package/dist/core/config.js +10 -2
- package/dist/core/interfaces.d.ts +32 -4
- package/dist/core/manifest.js +1 -1
- package/dist/describer/describer.d.ts +20 -0
- package/dist/describer/describer.js +117 -14
- package/dist/describer/shared.d.ts +28 -0
- package/dist/describer/shared.js +60 -0
- package/dist/embedder/factory.js +1 -1
- package/dist/embedder/ollama.d.ts +3 -1
- package/dist/embedder/ollama.js +12 -2
- package/dist/indexer/pipeline.js +187 -99
- package/dist/vectorstore/lancedb.d.ts +79 -7
- package/dist/vectorstore/lancedb.js +200 -72
- package/dist/vectorstore/memory.d.ts +8 -3
- package/dist/vectorstore/memory.js +22 -3
- package/dist/watcher.d.ts +8 -0
- package/dist/watcher.js +222 -96
- package/package.json +1 -1
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,110 +104,120 @@ 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
|
-
|
|
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) {
|
|
60
158
|
appendDebugLog(logFilePath, {
|
|
61
159
|
scope: "autoIndex",
|
|
62
|
-
message: "
|
|
160
|
+
message: "Watch reindex pass failed",
|
|
161
|
+
error: err,
|
|
63
162
|
}, logLevel);
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
},
|
|
163
|
+
if (isCorruptionError(err)) {
|
|
164
|
+
appendDebugLog(logFilePath, {
|
|
165
|
+
scope: "autoIndex",
|
|
166
|
+
message: "Corruption detected — run 'opencode-rag index --force' to rebuild manually",
|
|
167
|
+
}, logLevel);
|
|
69
168
|
}
|
|
169
|
+
updateStatus({ running: false, lastRunAt: Date.now() });
|
|
70
170
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
catch
|
|
171
|
+
};
|
|
172
|
+
// Fire-and-forget initial index pass
|
|
173
|
+
runPass().catch((err) => {
|
|
74
174
|
appendDebugLog(logFilePath, {
|
|
75
175
|
scope: "autoIndex",
|
|
76
|
-
message: "
|
|
176
|
+
message: "Initial index pass failed",
|
|
77
177
|
error: err,
|
|
78
178
|
}, logLevel);
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}, logLevel);
|
|
122
|
-
});
|
|
123
|
-
// Periodic timer: only needed for git backend (chokidar gets real FS events).
|
|
124
|
-
// Note: with git mode BOTH backends run — the scheduler coalesces redundant
|
|
125
|
-
// passes into one, so this only adds a safety net for missed events.
|
|
126
|
-
const watcherBackend = autoIndexCfg.watcher ?? "chokidar";
|
|
127
|
-
const periodicTimer = watcherBackend === "git"
|
|
128
|
-
? setInterval(() => {
|
|
129
|
-
scheduler.notifyChange();
|
|
130
|
-
}, autoIndexCfg.intervalMs)
|
|
131
|
-
: undefined;
|
|
132
|
-
// Never keep the process alive just for the periodic scan
|
|
133
|
-
periodicTimer?.unref();
|
|
134
|
-
return {
|
|
135
|
-
async close() {
|
|
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;
|
|
136
221
|
if (periodicTimer)
|
|
137
222
|
clearInterval(periodicTimer);
|
|
138
223
|
ac.abort();
|
|
@@ -154,10 +239,51 @@ export function createBackgroundIndexer(options) {
|
|
|
154
239
|
}
|
|
155
240
|
catch { /* ignore */ }
|
|
156
241
|
}
|
|
242
|
+
releaseWatcherLock(storePath);
|
|
157
243
|
appendDebugLog(logFilePath, {
|
|
158
244
|
scope: "autoIndex",
|
|
159
245
|
message: "Background indexer shut down",
|
|
160
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
|
+
}
|
|
161
287
|
},
|
|
162
288
|
};
|
|
163
289
|
}
|