opencode-rag-plugin 1.17.2 → 1.17.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ReadMe.md +4 -2
- package/dist/core/auto-update-state.d.ts +33 -0
- package/dist/core/auto-update-state.js +69 -0
- package/dist/core/config.d.ts +8 -2
- package/dist/core/config.js +4 -1
- package/dist/plugin.js +85 -6
- package/dist/vectorstore/lancedb.d.ts +1 -0
- package/dist/vectorstore/lancedb.js +8 -2
- package/package.json +1 -1
package/ReadMe.md
CHANGED
|
@@ -125,14 +125,16 @@ The agent maintains all wiki pages during normal coding sessions (ingest on lear
|
|
|
125
125
|
|
|
126
126
|
See [Plugin documentation](doc/plugin.md#6-wiki-mode--slash-command-wiki) for the full protocol.
|
|
127
127
|
|
|
128
|
-
## MCP Server
|
|
128
|
+
## MCP Server (Optional)
|
|
129
129
|
|
|
130
|
-
OpenCodeRAG ships a CLI-based [MCP (Model Context Protocol)](https://spec.modelcontextprotocol.io/) server that exposes semantic code tools to any MCP-compatible client (Claude Desktop,
|
|
130
|
+
OpenCodeRAG ships a CLI-based [MCP (Model Context Protocol)](https://spec.modelcontextprotocol.io/) server that exposes semantic code tools to any MCP-compatible client (Claude Desktop, Cursor, etc.).
|
|
131
131
|
|
|
132
132
|
```bash
|
|
133
133
|
opencode-rag mcp
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
+
> **Note:** The MCP server is **optional**. When running as an OpenCode plugin, the four tools are registered **in-process** and work without the MCP server. The `chat.message` hook for hotkey injection also runs in-process. The MCP server is only needed when an **external** MCP client connects to OpenCodeRAG. The plugin auto-starts the server only if `mcp.enabled` is `true` in your config (default: `false`).
|
|
137
|
+
|
|
136
138
|
### MCP Tools
|
|
137
139
|
|
|
138
140
|
| Tool | Description |
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Persisted state for the background auto-update mechanism.
|
|
3
|
+
* Tracks when an install was last attempted, for which version,
|
|
4
|
+
* and consecutive failure counts to implement cooldown and backoff.
|
|
5
|
+
*/
|
|
6
|
+
/** Persisted state of a background auto-update attempt. */
|
|
7
|
+
export interface AutoUpdateState {
|
|
8
|
+
/** Epoch ms of the most recent attempt. */
|
|
9
|
+
lastAttemptAt: number;
|
|
10
|
+
/** The latestVersion we tried to install (or checked). */
|
|
11
|
+
lastAttemptedVersion: string;
|
|
12
|
+
/** Outcome of the last attempt. */
|
|
13
|
+
lastResult: "success" | "failure";
|
|
14
|
+
/** Consecutive failures so far (reset on success). */
|
|
15
|
+
consecutiveFailures: number;
|
|
16
|
+
}
|
|
17
|
+
/** Full path to the state file for a given store path. */
|
|
18
|
+
export declare function statePath(storePath: string): string;
|
|
19
|
+
/** Load persisted auto-update state, or return null if missing / corrupt. */
|
|
20
|
+
export declare function loadAutoUpdateState(storePath: string): AutoUpdateState | null;
|
|
21
|
+
/** Persist auto-update state to disk (best-effort, never throws). */
|
|
22
|
+
export declare function saveAutoUpdateState(storePath: string, state: AutoUpdateState): void;
|
|
23
|
+
/**
|
|
24
|
+
* Determine whether an auto-install attempt should proceed, based on
|
|
25
|
+
* the persisted state, the version we just discovered, and configured
|
|
26
|
+
* limits.
|
|
27
|
+
*
|
|
28
|
+
* @returns An object with `attempt: boolean` and `reason: string`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function shouldAttemptInstall(state: AutoUpdateState | null, latestVersion: string, cooldownMs: number, maxConsecutiveFailures: number): {
|
|
31
|
+
attempt: boolean;
|
|
32
|
+
reason: string;
|
|
33
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Persisted state for the background auto-update mechanism.
|
|
3
|
+
* Tracks when an install was last attempted, for which version,
|
|
4
|
+
* and consecutive failure counts to implement cooldown and backoff.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
/** Name of the state file written to the store path. */
|
|
9
|
+
const STATE_FILE_NAME = ".auto-update-state.json";
|
|
10
|
+
/** Full path to the state file for a given store path. */
|
|
11
|
+
export function statePath(storePath) {
|
|
12
|
+
return path.join(storePath, STATE_FILE_NAME);
|
|
13
|
+
}
|
|
14
|
+
/** Load persisted auto-update state, or return null if missing / corrupt. */
|
|
15
|
+
export function loadAutoUpdateState(storePath) {
|
|
16
|
+
try {
|
|
17
|
+
const p = statePath(storePath);
|
|
18
|
+
if (!existsSync(p))
|
|
19
|
+
return null;
|
|
20
|
+
const raw = readFileSync(p, "utf-8");
|
|
21
|
+
const parsed = JSON.parse(raw);
|
|
22
|
+
if (typeof parsed.lastAttemptAt !== "number" ||
|
|
23
|
+
typeof parsed.lastAttemptedVersion !== "string" ||
|
|
24
|
+
(parsed.lastResult !== "success" && parsed.lastResult !== "failure") ||
|
|
25
|
+
typeof parsed.consecutiveFailures !== "number") {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return parsed;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Persist auto-update state to disk (best-effort, never throws). */
|
|
35
|
+
export function saveAutoUpdateState(storePath, state) {
|
|
36
|
+
try {
|
|
37
|
+
writeFileSync(statePath(storePath), JSON.stringify(state, null, 2), "utf-8");
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
/* best-effort */
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Determine whether an auto-install attempt should proceed, based on
|
|
45
|
+
* the persisted state, the version we just discovered, and configured
|
|
46
|
+
* limits.
|
|
47
|
+
*
|
|
48
|
+
* @returns An object with `attempt: boolean` and `reason: string`.
|
|
49
|
+
*/
|
|
50
|
+
export function shouldAttemptInstall(state, latestVersion, cooldownMs, maxConsecutiveFailures) {
|
|
51
|
+
if (!state) {
|
|
52
|
+
return { attempt: true, reason: "first run" };
|
|
53
|
+
}
|
|
54
|
+
const elapsed = Date.now() - state.lastAttemptAt;
|
|
55
|
+
if (state.lastAttemptedVersion === latestVersion && elapsed < cooldownMs) {
|
|
56
|
+
return {
|
|
57
|
+
attempt: false,
|
|
58
|
+
reason: `version ${latestVersion} already attempted ${Math.round(elapsed / 1000)}s ago (cooldown ${Math.round(cooldownMs / 1000)}s)`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (state.lastResult === "failure" && state.consecutiveFailures >= maxConsecutiveFailures && elapsed < cooldownMs) {
|
|
62
|
+
return {
|
|
63
|
+
attempt: false,
|
|
64
|
+
reason: `${state.consecutiveFailures} consecutive failures within cooldown window`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return { attempt: true, reason: "proceeding" };
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=auto-update-state.js.map
|
package/dist/core/config.d.ts
CHANGED
|
@@ -128,15 +128,21 @@ export interface TuiConfig {
|
|
|
128
128
|
/** Keybinding to toggle the chunk viewer panel. */
|
|
129
129
|
chunksKeybinding: string;
|
|
130
130
|
}
|
|
131
|
-
/** Configuration for MCP (Model Context Protocol) server
|
|
131
|
+
/** Configuration for the standalone MCP (Model Context Protocol) server. */
|
|
132
132
|
export interface McpConfig {
|
|
133
|
-
/** Whether the MCP server
|
|
133
|
+
/** Whether to auto-start the MCP server on plugin load. Default false — agent tools run in-process regardless. Set true only when an external MCP client needs to connect via `opencode-rag mcp`. */
|
|
134
134
|
enabled: boolean;
|
|
135
135
|
}
|
|
136
136
|
/** Configuration for automatic self-updates. */
|
|
137
137
|
export interface AutoUpdateConfig {
|
|
138
138
|
/** Whether update checking is enabled (defaults to true). */
|
|
139
139
|
enabled: boolean;
|
|
140
|
+
/** When true, automatically install a newer release on startup (default false). */
|
|
141
|
+
autoInstall?: boolean;
|
|
142
|
+
/** Cooldown window (ms) before re-attempting the same version (default 3 600 000 = 1h). */
|
|
143
|
+
cooldownMs?: number;
|
|
144
|
+
/** Back off after this many consecutive failures (default 3). */
|
|
145
|
+
maxConsecutiveFailures?: number;
|
|
140
146
|
}
|
|
141
147
|
/** Configuration for post-retrieval context window optimization (adjacent merge, similarity dedup, file diversity cap). */
|
|
142
148
|
export interface ContextOptimizationConfig {
|
package/dist/core/config.js
CHANGED
|
@@ -245,10 +245,13 @@ export const DEFAULT_CONFIG = {
|
|
|
245
245
|
"- The wiki is git-trackable in `.opencode/` — every edit is a diff",
|
|
246
246
|
},
|
|
247
247
|
mcp: {
|
|
248
|
-
enabled:
|
|
248
|
+
enabled: false,
|
|
249
249
|
},
|
|
250
250
|
autoUpdate: {
|
|
251
251
|
enabled: true,
|
|
252
|
+
autoInstall: false,
|
|
253
|
+
cooldownMs: 3_600_000,
|
|
254
|
+
maxConsecutiveFailures: 3,
|
|
252
255
|
},
|
|
253
256
|
ui: {
|
|
254
257
|
port: 3210,
|
package/dist/plugin.js
CHANGED
|
@@ -22,7 +22,8 @@ import { loadDocProgress, markSubdirectoryDocumented } from "./core/doc-progress
|
|
|
22
22
|
import { loadManifest } from "./core/manifest.js";
|
|
23
23
|
import { createSessionLogger } from "./eval/session-logger.js";
|
|
24
24
|
import { countTokens } from "./eval/token-counter.js";
|
|
25
|
-
import { checkForUpdate, getCurrentVersion } from "./core/version-check.js";
|
|
25
|
+
import { checkForUpdate, getCurrentVersion, installLatestUpdate } from "./core/version-check.js";
|
|
26
|
+
import { loadAutoUpdateState, saveAutoUpdateState, shouldAttemptInstall } from "./core/auto-update-state.js";
|
|
26
27
|
import { destroyAllPooledConnections } from "./embedder/http.js";
|
|
27
28
|
import { existsSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
|
|
28
29
|
import path from "node:path";
|
|
@@ -41,6 +42,8 @@ const mcpServers = new Map();
|
|
|
41
42
|
const pendingUpdateInfo = new Map();
|
|
42
43
|
/** Workspace directories that have already been prompted about an update this session. */
|
|
43
44
|
const notifiedUpdateDirs = new Set();
|
|
45
|
+
/** Pending "restart needed" notice after a background auto-install. */
|
|
46
|
+
const pendingRestartNotice = new Map();
|
|
44
47
|
/** Guard flag to prevent re-entrant shutdown. */
|
|
45
48
|
let shutdownRegistered = false;
|
|
46
49
|
/** Close all active background indexers and MCP servers, then destroy idle sockets. */
|
|
@@ -61,6 +64,7 @@ async function shutdownPluginResources() {
|
|
|
61
64
|
}
|
|
62
65
|
configCache.clear();
|
|
63
66
|
pendingUpdateInfo.clear();
|
|
67
|
+
pendingRestartNotice.clear();
|
|
64
68
|
notifiedUpdateDirs.clear();
|
|
65
69
|
destroyAllPooledConnections();
|
|
66
70
|
}
|
|
@@ -699,6 +703,16 @@ export function createRagHooks(options) {
|
|
|
699
703
|
`they would like you to install it. If they agree, run \`opencode-rag update\` via the bash tool, then tell ` +
|
|
700
704
|
`them to restart OpenCode to load the new version. Do not mention this notice again.`);
|
|
701
705
|
}
|
|
706
|
+
// Inject a one-time "restart needed" notice after a background auto-install.
|
|
707
|
+
// Fires only once per workspace per session, mirroring the notify-only pattern above.
|
|
708
|
+
const restartInfo = pendingRestartNotice.get(options.worktree);
|
|
709
|
+
if (restartInfo && !notifiedUpdateDirs.has(options.worktree + ":restart")) {
|
|
710
|
+
notifiedUpdateDirs.add(options.worktree + ":restart");
|
|
711
|
+
pendingRestartNotice.delete(options.worktree);
|
|
712
|
+
output.system.unshift(`OpenCodeRAG was automatically updated from v${restartInfo.fromVersion} to v${restartInfo.toVersion} ` +
|
|
713
|
+
`in the background. At the very start of your next response, tell the user the update was installed ` +
|
|
714
|
+
`and ask them to restart OpenCode to load the new version. Do not mention this notice again.`);
|
|
715
|
+
}
|
|
702
716
|
// Inject documentation mode system prompt if enabled
|
|
703
717
|
const docMode = getEffectiveCfg().documentationMode;
|
|
704
718
|
if (docMode?.enabled && docMode.systemPrompt) {
|
|
@@ -1161,6 +1175,7 @@ export const ragPlugin = async (input, _options) => {
|
|
|
1161
1175
|
// Clean up stale config cache and pending update info for this directory
|
|
1162
1176
|
configCache.delete(input.directory);
|
|
1163
1177
|
pendingUpdateInfo.delete(input.directory);
|
|
1178
|
+
pendingRestartNotice.delete(input.directory);
|
|
1164
1179
|
notifiedUpdateDirs.delete(input.directory);
|
|
1165
1180
|
// Clean up idle HTTP sockets from previous provider connections
|
|
1166
1181
|
destroyAllPooledConnections();
|
|
@@ -1253,8 +1268,11 @@ export const ragPlugin = async (input, _options) => {
|
|
|
1253
1268
|
catch { /* ignore */ }
|
|
1254
1269
|
}
|
|
1255
1270
|
}
|
|
1256
|
-
// Auto-start MCP server if enabled (
|
|
1257
|
-
|
|
1271
|
+
// Auto-start standalone MCP server if enabled (default: off). Agent tools
|
|
1272
|
+
// (search_semantic, get_file_skeleton, find_usages, describe_image) and the
|
|
1273
|
+
// chat.message hook run in-process regardless. Enable only when an external
|
|
1274
|
+
// MCP client connects to `opencode-rag mcp`.
|
|
1275
|
+
const mcpCfg = effectiveCfg.mcp ?? { enabled: false };
|
|
1258
1276
|
const isTempDir = path.resolve(input.directory).startsWith(tmpdir());
|
|
1259
1277
|
if (mcpCfg.enabled && !isTempDir) {
|
|
1260
1278
|
const mcpInstance = startMcpServerProcess(input.directory, logFilePath, logLevel);
|
|
@@ -1262,14 +1280,75 @@ export const ragPlugin = async (input, _options) => {
|
|
|
1262
1280
|
}
|
|
1263
1281
|
// Auto-update check (non-blocking, best-effort). On by default; can be
|
|
1264
1282
|
// disabled via `autoUpdate.enabled: false` in opencode-rag.json. When a newer
|
|
1265
|
-
// release is found it is
|
|
1266
|
-
// the system transform hook (
|
|
1283
|
+
// release is found it is either surfaced as a one-time install prompt via
|
|
1284
|
+
// the system transform hook (default) or automatically installed in the
|
|
1285
|
+
// background when `autoUpdate.autoInstall: true` is configured.
|
|
1267
1286
|
const autoUpdateCfg = effectiveCfg.autoUpdate;
|
|
1268
1287
|
if (autoUpdateCfg?.enabled) {
|
|
1269
1288
|
const currentVersion = getCurrentVersion();
|
|
1270
1289
|
checkForUpdate(currentVersion)
|
|
1271
1290
|
.then((info) => {
|
|
1272
|
-
if (info.updateAvailable)
|
|
1291
|
+
if (!info.updateAvailable)
|
|
1292
|
+
return;
|
|
1293
|
+
if (autoUpdateCfg?.autoInstall) {
|
|
1294
|
+
// Background auto-install branch (opt-in). Persist a cooldown file
|
|
1295
|
+
// so we don't re-install on every plugin reload or after failures.
|
|
1296
|
+
const cooldownMs = autoUpdateCfg.cooldownMs ?? 3_600_000;
|
|
1297
|
+
const maxFailures = autoUpdateCfg.maxConsecutiveFailures ?? 3;
|
|
1298
|
+
const state = loadAutoUpdateState(storePath);
|
|
1299
|
+
const { attempt, reason } = shouldAttemptInstall(state, info.latestVersion, cooldownMs, maxFailures);
|
|
1300
|
+
if (!attempt) {
|
|
1301
|
+
appendDebugLog(logFilePath, {
|
|
1302
|
+
scope: "updater",
|
|
1303
|
+
message: `Auto-install skipped: ${reason}`,
|
|
1304
|
+
}, logLevel);
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
saveAutoUpdateState(storePath, {
|
|
1308
|
+
lastAttemptAt: Date.now(),
|
|
1309
|
+
lastAttemptedVersion: info.latestVersion,
|
|
1310
|
+
lastResult: "failure",
|
|
1311
|
+
consecutiveFailures: 1,
|
|
1312
|
+
});
|
|
1313
|
+
installLatestUpdate({ verbose: false })
|
|
1314
|
+
.then((result) => {
|
|
1315
|
+
if (result.success && result.toVersion && result.toVersion !== currentVersion) {
|
|
1316
|
+
pendingRestartNotice.set(input.directory, {
|
|
1317
|
+
fromVersion: currentVersion,
|
|
1318
|
+
toVersion: result.toVersion,
|
|
1319
|
+
});
|
|
1320
|
+
saveAutoUpdateState(storePath, {
|
|
1321
|
+
lastAttemptAt: Date.now(),
|
|
1322
|
+
lastAttemptedVersion: info.latestVersion,
|
|
1323
|
+
lastResult: "success",
|
|
1324
|
+
consecutiveFailures: 0,
|
|
1325
|
+
});
|
|
1326
|
+
appendDebugLog(logFilePath, {
|
|
1327
|
+
scope: "updater",
|
|
1328
|
+
message: `Auto-installed v${currentVersion} → v${result.toVersion}`,
|
|
1329
|
+
}, logLevel);
|
|
1330
|
+
}
|
|
1331
|
+
else {
|
|
1332
|
+
saveAutoUpdateState(storePath, {
|
|
1333
|
+
lastAttemptAt: Date.now(),
|
|
1334
|
+
lastAttemptedVersion: info.latestVersion,
|
|
1335
|
+
lastResult: "success",
|
|
1336
|
+
consecutiveFailures: 0,
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
})
|
|
1340
|
+
.catch(() => {
|
|
1341
|
+
const existing = loadAutoUpdateState(storePath);
|
|
1342
|
+
saveAutoUpdateState(storePath, {
|
|
1343
|
+
lastAttemptAt: Date.now(),
|
|
1344
|
+
lastAttemptedVersion: info.latestVersion,
|
|
1345
|
+
lastResult: "failure",
|
|
1346
|
+
consecutiveFailures: (existing?.consecutiveFailures ?? 0) + 1,
|
|
1347
|
+
});
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
else {
|
|
1351
|
+
// Notify-only: surface as a one-time agent prompt
|
|
1273
1352
|
pendingUpdateInfo.set(input.directory, info);
|
|
1274
1353
|
appendDebugLog(logFilePath, {
|
|
1275
1354
|
scope: "updater",
|
|
@@ -30,6 +30,7 @@ export declare class LanceDbStore implements VectorStore {
|
|
|
30
30
|
private db;
|
|
31
31
|
private table;
|
|
32
32
|
private tableInit;
|
|
33
|
+
private writeLock;
|
|
33
34
|
/**
|
|
34
35
|
* @param dbPath - Filesystem path to the LanceDB database directory.
|
|
35
36
|
* @param vectorDimension - Dimension of the embedding vectors. Default: 384.
|
|
@@ -75,6 +75,7 @@ export class LanceDbStore {
|
|
|
75
75
|
db = null;
|
|
76
76
|
table = null;
|
|
77
77
|
tableInit = null;
|
|
78
|
+
writeLock = Promise.resolve(void 0);
|
|
78
79
|
/**
|
|
79
80
|
* @param dbPath - Filesystem path to the LanceDB database directory.
|
|
80
81
|
* @param vectorDimension - Dimension of the embedding vectors. Default: 384.
|
|
@@ -184,12 +185,17 @@ export class LanceDbStore {
|
|
|
184
185
|
async addChunks(chunks) {
|
|
185
186
|
if (chunks.length === 0)
|
|
186
187
|
return;
|
|
188
|
+
const done = this.writeLock.then(() => this.addChunksInternal(chunks));
|
|
189
|
+
this.writeLock = done.catch(() => { });
|
|
187
190
|
try {
|
|
188
|
-
await
|
|
191
|
+
await done;
|
|
189
192
|
}
|
|
190
193
|
catch (err) {
|
|
194
|
+
this.writeLock = Promise.resolve();
|
|
191
195
|
if (isCorruptionError(err) && await this.tryRepair()) {
|
|
192
|
-
|
|
196
|
+
const retry = this.addChunksInternal(chunks);
|
|
197
|
+
this.writeLock = retry.catch(() => { });
|
|
198
|
+
await retry;
|
|
193
199
|
return;
|
|
194
200
|
}
|
|
195
201
|
throw err;
|