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 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, OpenCode, Cursor, etc.).
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
@@ -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 integration. */
131
+ /** Configuration for the standalone MCP (Model Context Protocol) server. */
132
132
  export interface McpConfig {
133
- /** Whether the MCP server is enabled. */
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 {
@@ -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: true,
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 (skip in temp dirs / test environments)
1257
- const mcpCfg = effectiveCfg.mcp ?? { enabled: true };
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 stored and surfaced as a one-time install prompt via
1266
- // the system transform hook (see createRagHooks).
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 this.addChunksInternal(chunks);
191
+ await done;
189
192
  }
190
193
  catch (err) {
194
+ this.writeLock = Promise.resolve();
191
195
  if (isCorruptionError(err) && await this.tryRepair()) {
192
- await this.addChunksInternal(chunks);
196
+ const retry = this.addChunksInternal(chunks);
197
+ this.writeLock = retry.catch(() => { });
198
+ await retry;
193
199
  return;
194
200
  }
195
201
  throw err;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-rag-plugin",
3
- "version": "1.17.2",
3
+ "version": "1.17.3",
4
4
  "description": "OpenCode plugin for local-first RAG-based semantic code search",
5
5
  "type": "module",
6
6
  "main": "./dist/plugin-entry.js",