opencode-rag-plugin 1.17.0 → 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.
@@ -1,9 +1,85 @@
1
+ /**
2
+ * @fileoverview Version check and self-update functionality. Checks GitHub
3
+ * releases for new versions and installs the newest version via npm, then
4
+ * re-syncs the OpenCode runtime junctions.
5
+ */
6
+ import { setupRuntime } from "./setup-runtime.js";
7
+ /** Information about an available update. */
1
8
  export interface UpdateInfo {
9
+ /** The currently installed version string. */
2
10
  currentVersion: string;
11
+ /** The latest available version on GitHub. */
3
12
  latestVersion: string;
13
+ /** Whether a newer version than the current one exists. */
4
14
  updateAvailable: boolean;
15
+ /** URL to the GitHub release page. */
5
16
  releaseUrl: string;
17
+ /** ISO date string of when the release was published. */
6
18
  publishedAt: string;
7
19
  }
20
+ /** Result of an install-update attempt. */
21
+ export interface InstallUpdateResult {
22
+ /** Whether the install completed successfully. */
23
+ success: boolean;
24
+ /** Human-readable status message. */
25
+ message: string;
26
+ /** Version before the update. */
27
+ fromVersion: string;
28
+ /** Version after the update (undefined if the install failed). */
29
+ toVersion?: string;
30
+ }
31
+ /** Callable shape for the npm runner (a subset of execSync's signature). */
32
+ type NpmRunner = (command: string, options: {
33
+ stdio: "inherit" | "pipe";
34
+ timeout: number;
35
+ }) => unknown;
36
+ /**
37
+ * Read the current version from the package.json sitting next to this module.
38
+ *
39
+ * Resolves the package root via `import.meta.url` so it works both from the
40
+ * source tree and from the compiled `dist/` output. Returns `"0.0.0"` if the
41
+ * file cannot be read or parsed (best-effort — never throws).
42
+ *
43
+ * @returns The current package version string.
44
+ */
45
+ export declare function getCurrentVersion(): string;
46
+ /**
47
+ * Compare two semver-ish strings.
48
+ * @returns 1 if a > b, -1 if a < b, 0 if equal.
49
+ */
8
50
  export declare function compareVersions(a: string, b: string): number;
51
+ /**
52
+ * Check the GitHub releases API for a newer version of OpenCodeRAG.
53
+ *
54
+ * Uses a 5-second timeout; failures (network errors, non-OK responses, missing
55
+ * `tag_name`) are silently caught and reported as "no update available" so the
56
+ * caller never has to handle a rejection.
57
+ *
58
+ * @param currentVersion - The version string to compare against.
59
+ * @returns UpdateInfo indicating whether an update is available.
60
+ */
9
61
  export declare function checkForUpdate(currentVersion: string): Promise<UpdateInfo>;
62
+ /**
63
+ * Install the newest published version of OpenCodeRAG.
64
+ *
65
+ * Runs `npm install -g <package>@latest` to refresh the global install, then
66
+ * calls {@link setupRuntime} with `force: true` to re-create the
67
+ * `~/.opencode/node_modules/` junctions so OpenCode picks up the new build on
68
+ * the next restart.
69
+ *
70
+ * @param options - Optional verbosity flag. When `verbose` is true, npm output
71
+ * is streamed to the console; otherwise it is captured silently. The
72
+ * `_execSync` and `_setupRuntime` seams are for testing only.
73
+ * @returns An {@link InstallUpdateResult} with success status, message, and the
74
+ * from/to versions.
75
+ */
76
+ export declare function installLatestUpdate(options?: {
77
+ verbose?: boolean;
78
+ /** Test seam: override the npm runner. */
79
+ _execSync?: NpmRunner;
80
+ /** Test seam: override the runtime sync. */
81
+ _setupRuntime?: typeof setupRuntime;
82
+ /** Test seam: override the version reader for the "to" version. */
83
+ _getCurrentVersion?: typeof getCurrentVersion;
84
+ }): Promise<InstallUpdateResult>;
85
+ export {};
@@ -1,3 +1,45 @@
1
+ /**
2
+ * @fileoverview Version check and self-update functionality. Checks GitHub
3
+ * releases for new versions and installs the newest version via npm, then
4
+ * re-syncs the OpenCode runtime junctions.
5
+ */
6
+ import { readFileSync } from "node:fs";
7
+ import path from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ import { execSync } from "node:child_process";
10
+ import { setupRuntime } from "./setup-runtime.js";
11
+ /** npm package name (must match the `name` field in package.json). */
12
+ const PACKAGE_NAME = "opencode-rag-plugin";
13
+ /** GitHub repo used for release checks (owner/repo). */
14
+ const GITHUB_REPO = "MrDoe/OpenCodeRAG";
15
+ const GITHUB_API_URL = `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`;
16
+ /**
17
+ * Read the current version from the package.json sitting next to this module.
18
+ *
19
+ * Resolves the package root via `import.meta.url` so it works both from the
20
+ * source tree and from the compiled `dist/` output. Returns `"0.0.0"` if the
21
+ * file cannot be read or parsed (best-effort — never throws).
22
+ *
23
+ * @returns The current package version string.
24
+ */
25
+ export function getCurrentVersion() {
26
+ try {
27
+ const pkgPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
28
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
29
+ return pkg.version ?? "0.0.0";
30
+ }
31
+ catch {
32
+ return "0.0.0";
33
+ }
34
+ }
35
+ /** Strip a leading 'v' or 'V' prefix from a version tag. */
36
+ function normalizeVersion(tag) {
37
+ return tag.replace(/^v/i, "");
38
+ }
39
+ /**
40
+ * Compare two semver-ish strings.
41
+ * @returns 1 if a > b, -1 if a < b, 0 if equal.
42
+ */
1
43
  export function compareVersions(a, b) {
2
44
  const pa = a.split(".").map((s) => parseInt(s, 10));
3
45
  const pb = b.split(".").map((s) => parseInt(s, 10));
@@ -11,11 +53,21 @@ export function compareVersions(a, b) {
11
53
  }
12
54
  return 0;
13
55
  }
56
+ /**
57
+ * Check the GitHub releases API for a newer version of OpenCodeRAG.
58
+ *
59
+ * Uses a 5-second timeout; failures (network errors, non-OK responses, missing
60
+ * `tag_name`) are silently caught and reported as "no update available" so the
61
+ * caller never has to handle a rejection.
62
+ *
63
+ * @param currentVersion - The version string to compare against.
64
+ * @returns UpdateInfo indicating whether an update is available.
65
+ */
14
66
  export async function checkForUpdate(currentVersion) {
15
67
  const controller = new AbortController();
16
68
  const timeout = setTimeout(() => controller.abort(), 5_000);
17
69
  try {
18
- const response = await fetch("https://api.github.com/repos/MrDoe/OpenCodeRAG/releases/latest", {
70
+ const response = await fetch(GITHUB_API_URL, {
19
71
  headers: {
20
72
  Accept: "application/vnd.github+json",
21
73
  "User-Agent": "opencode-rag-updater",
@@ -30,7 +82,7 @@ export async function checkForUpdate(currentVersion) {
30
82
  if (!tagName) {
31
83
  return { currentVersion, latestVersion: currentVersion, updateAvailable: false, releaseUrl: "", publishedAt: "" };
32
84
  }
33
- const latestVersion = tagName.replace(/^v/i, "");
85
+ const latestVersion = normalizeVersion(tagName);
34
86
  return {
35
87
  currentVersion,
36
88
  latestVersion,
@@ -46,4 +98,63 @@ export async function checkForUpdate(currentVersion) {
46
98
  clearTimeout(timeout);
47
99
  }
48
100
  }
101
+ /**
102
+ * Install the newest published version of OpenCodeRAG.
103
+ *
104
+ * Runs `npm install -g <package>@latest` to refresh the global install, then
105
+ * calls {@link setupRuntime} with `force: true` to re-create the
106
+ * `~/.opencode/node_modules/` junctions so OpenCode picks up the new build on
107
+ * the next restart.
108
+ *
109
+ * @param options - Optional verbosity flag. When `verbose` is true, npm output
110
+ * is streamed to the console; otherwise it is captured silently. The
111
+ * `_execSync` and `_setupRuntime` seams are for testing only.
112
+ * @returns An {@link InstallUpdateResult} with success status, message, and the
113
+ * from/to versions.
114
+ */
115
+ export async function installLatestUpdate(options) {
116
+ const verbose = options?.verbose ?? false;
117
+ const stdio = verbose ? "inherit" : "pipe";
118
+ const run = options?._execSync ?? execSync;
119
+ const syncRuntime = options?._setupRuntime ?? setupRuntime;
120
+ const readVersion = options?._getCurrentVersion ?? getCurrentVersion;
121
+ const fromVersion = readVersion();
122
+ try {
123
+ run(`npm install -g ${PACKAGE_NAME}@latest --no-fund --no-audit`, {
124
+ stdio,
125
+ timeout: 120_000,
126
+ });
127
+ }
128
+ catch (err) {
129
+ return {
130
+ success: false,
131
+ message: `npm install failed: ${err.message}`,
132
+ fromVersion,
133
+ };
134
+ }
135
+ const toVersion = readVersion();
136
+ const result = await syncRuntime({ force: true, silent: !verbose, version: toVersion });
137
+ if (!result.success) {
138
+ return {
139
+ success: false,
140
+ message: `installed v${toVersion} but runtime sync failed: ${result.errors.join("; ")}. Run \`opencode-rag setup\` to retry.`,
141
+ fromVersion,
142
+ toVersion,
143
+ };
144
+ }
145
+ if (compareVersions(toVersion, fromVersion) <= 0) {
146
+ return {
147
+ success: true,
148
+ message: `Already up-to-date (v${toVersion}). Runtime re-synced.`,
149
+ fromVersion,
150
+ toVersion,
151
+ };
152
+ }
153
+ return {
154
+ success: true,
155
+ message: `Updated v${fromVersion} → v${toVersion}. Restart OpenCode to load the new version.`,
156
+ fromVersion,
157
+ toVersion,
158
+ };
159
+ }
49
160
  //# sourceMappingURL=version-check.js.map
@@ -9,7 +9,7 @@ import type { ProxyConfig } from "../core/config.js";
9
9
  * @param baseUrl - Cohere API base URL
10
10
  * @param model - Model name to use for embedding
11
11
  * @param apiKey - API key for authentication
12
- * @param timeoutMs - Request timeout in milliseconds (default 30000)
12
+ * @param timeoutMs - Request timeout in milliseconds (default 120000)
13
13
  * @param proxy - Optional proxy configuration
14
14
  */
15
15
  export declare class CohereProvider implements EmbeddingProvider {
@@ -5,7 +5,7 @@ import { postJson } from "./http.js";
5
5
  * @param baseUrl - Cohere API base URL
6
6
  * @param model - Model name to use for embedding
7
7
  * @param apiKey - API key for authentication
8
- * @param timeoutMs - Request timeout in milliseconds (default 30000)
8
+ * @param timeoutMs - Request timeout in milliseconds (default 120000)
9
9
  * @param proxy - Optional proxy configuration
10
10
  */
11
11
  export class CohereProvider {
@@ -15,7 +15,7 @@ export class CohereProvider {
15
15
  apiKey;
16
16
  timeoutMs;
17
17
  proxy;
18
- constructor(baseUrl, model, apiKey, timeoutMs = 30000, proxy) {
18
+ constructor(baseUrl, model, apiKey, timeoutMs = 120000, proxy) {
19
19
  this.baseUrl = baseUrl.replace(/\/+$/, "");
20
20
  this.model = model;
21
21
  this.apiKey = apiKey;
@@ -16,7 +16,7 @@ import pLimit from "p-limit";
16
16
  */
17
17
  export function createEmbedder(config) {
18
18
  const { provider, baseUrl, model, apiKey, proxy, timeoutMs } = config.embedding;
19
- const effectiveTimeoutMs = timeoutMs ?? 30000;
19
+ const effectiveTimeoutMs = timeoutMs ?? 120000;
20
20
  if (provider === "ollama") {
21
21
  return new OllamaProvider(baseUrl, model, apiKey, effectiveTimeoutMs, proxy, config.logging.level);
22
22
  }
@@ -4,7 +4,7 @@ import { postJson } from "./http.js";
4
4
  * Returns one result per configured model (embedding + description + image_description if enabled).
5
5
  */
6
6
  export async function checkProviderHealth(config) {
7
- const timeoutMs = config.embedding.timeoutMs ?? 30000;
7
+ const timeoutMs = config.embedding.timeoutMs ?? 120000;
8
8
  const checks = [
9
9
  checkEmbeddingModel(config, timeoutMs),
10
10
  ];
@@ -9,7 +9,7 @@ import type { ProxyConfig } from "../core/config.js";
9
9
  * @param baseUrl - Ollama server base URL (e.g. http://localhost:11434)
10
10
  * @param model - Model name to use for embedding
11
11
  * @param apiKey - Optional API key for authenticated endpoints
12
- * @param timeoutMs - Request timeout in milliseconds (default 30000)
12
+ * @param timeoutMs - Request timeout in milliseconds (default 120000)
13
13
  * @param proxy - Optional proxy configuration
14
14
  * @param logLevel - Optional logging level for debug output
15
15
  */
@@ -7,7 +7,7 @@ import { appendDebugLog } from "../core/fileLogger.js";
7
7
  * @param baseUrl - Ollama server base URL (e.g. http://localhost:11434)
8
8
  * @param model - Model name to use for embedding
9
9
  * @param apiKey - Optional API key for authenticated endpoints
10
- * @param timeoutMs - Request timeout in milliseconds (default 30000)
10
+ * @param timeoutMs - Request timeout in milliseconds (default 120000)
11
11
  * @param proxy - Optional proxy configuration
12
12
  * @param logLevel - Optional logging level for debug output
13
13
  */
@@ -19,7 +19,7 @@ export class OllamaProvider {
19
19
  timeoutMs;
20
20
  proxy;
21
21
  logLevel;
22
- constructor(baseUrl, model, apiKey, timeoutMs = 30000, proxy, logLevel) {
22
+ constructor(baseUrl, model, apiKey, timeoutMs = 120000, proxy, logLevel) {
23
23
  this.baseUrl = baseUrl.replace(/\/+$/, "");
24
24
  this.model = model;
25
25
  this.apiKey = apiKey;
@@ -9,7 +9,7 @@ import type { ProxyConfig } from "../core/config.js";
9
9
  * @param baseUrl - API base URL (e.g. https://api.openai.com/v1)
10
10
  * @param model - Model name to use for embedding
11
11
  * @param apiKey - API key for authentication
12
- * @param timeoutMs - Request timeout in milliseconds (default 30000)
12
+ * @param timeoutMs - Request timeout in milliseconds (default 120000)
13
13
  * @param proxy - Optional proxy configuration
14
14
  */
15
15
  export declare class OpenAIProvider implements EmbeddingProvider {
@@ -16,7 +16,7 @@ function inferProviderName(baseUrl) {
16
16
  * @param baseUrl - API base URL (e.g. https://api.openai.com/v1)
17
17
  * @param model - Model name to use for embedding
18
18
  * @param apiKey - API key for authentication
19
- * @param timeoutMs - Request timeout in milliseconds (default 30000)
19
+ * @param timeoutMs - Request timeout in milliseconds (default 120000)
20
20
  * @param proxy - Optional proxy configuration
21
21
  */
22
22
  export class OpenAIProvider {
@@ -27,7 +27,7 @@ export class OpenAIProvider {
27
27
  timeoutMs;
28
28
  proxy;
29
29
  provider;
30
- constructor(baseUrl, model, apiKey, timeoutMs = 30000, proxy) {
30
+ constructor(baseUrl, model, apiKey, timeoutMs = 120000, proxy) {
31
31
  this.baseUrl = baseUrl.replace(/\/+$/, "");
32
32
  this.model = model;
33
33
  this.apiKey = apiKey;
package/dist/index.d.ts CHANGED
@@ -25,6 +25,9 @@ export type { ContextOptimizationConfig, ContextOptimizationOptions } from "./re
25
25
  */
26
26
  export { search, indexWorkspace, getContext, validateConfig, scanWorkspace, getIndexStatusSummary } from "./api.js";
27
27
  export type { SearchOptions, IndexOptions, ContextResult, ConfigValidationResult, WorkspaceFile, IndexRunStats } from "./api.js";
28
+ /** Version check and self-update API. */
29
+ export { checkForUpdate, getCurrentVersion, installLatestUpdate, compareVersions } from "./core/version-check.js";
30
+ export type { UpdateInfo, InstallUpdateResult } from "./core/version-check.js";
28
31
  /** The plugin server configuration object, used to register OpenCodeRAG as an OpenCode plugin. */
29
32
  export declare const server: import("@opencode-ai/plugin").Plugin;
30
33
  /** Unique identifier for the OpenCodeRAG plugin. */
package/dist/index.js CHANGED
@@ -21,6 +21,8 @@ export { DescriptionCache } from "./core/desc-cache.js";
21
21
  * @module
22
22
  */
23
23
  export { search, indexWorkspace, getContext, validateConfig, scanWorkspace, getIndexStatusSummary } from "./api.js";
24
+ /** Version check and self-update API. */
25
+ export { checkForUpdate, getCurrentVersion, installLatestUpdate, compareVersions } from "./core/version-check.js";
24
26
  /** Plugin entry — only importable inside OpenCode's runtime. */
25
27
  import { ragPlugin } from "./plugin.js";
26
28
  /** The plugin server configuration object, used to register OpenCodeRAG as an OpenCode plugin. */
@@ -210,10 +210,17 @@ async function runIndexPassInner(options, logger) {
210
210
  effectiveStore = createVectorStore(options.config, tempStorePath, options.dimension);
211
211
  logger.debug(`Rebuilding index in temporary store at ${tempStorePath}`);
212
212
  }
213
+ else if (existingCount > 0) {
214
+ // NEVER destroy existing data when we can't do an atomic rebuild.
215
+ // Abort and ask the user to run 'opencode-rag index --force' manually.
216
+ logger.warn("Cannot rebuild safely without embedding dimension — aborting to protect existing data. " +
217
+ "Run 'opencode-rag index --force' manually to rebuild.");
218
+ // Restore manifest entries we just deleted so the next pass can retry incrementally
219
+ return createIndexStats(workspaceFiles.length, manifestStatus);
220
+ }
213
221
  else {
214
- // Fallback — in-memory store or no dimension: clear in place.
215
- logger.warn("No embedding dimension available; falling back to in-place clear.");
216
- await options.store.clear();
222
+ // No existing data safe to proceed with in-place indexing (no clear needed)
223
+ logger.debug("No existing data; indexing from scratch.");
217
224
  }
218
225
  }
219
226
  const stats = createIndexStats(workspaceFiles.length, manifestStatus);
@@ -518,7 +525,7 @@ async function runIndexPassInner(options, logger) {
518
525
  }
519
526
  // ── Phase 2: Embed all texts in a single batched call ──────────────────
520
527
  const batchSize = isOllama
521
- ? Math.min(options.config.indexing.ollamaMaxBatchSize ?? 4000, defaultBatchSize)
528
+ ? Math.min(options.config.indexing.ollamaMaxBatchSize ?? 500, defaultBatchSize)
522
529
  : defaultBatchSize;
523
530
  let embeddedDone = 0;
524
531
  const allTexts = embedQueue.map(item => item.text);
@@ -556,6 +563,8 @@ async function runIndexPassInner(options, logger) {
556
563
  }
557
564
  }
558
565
  // ── Phase 3: Store + manifest update per file (parallel) ──────────────
566
+ const filesToStore = prepared.filter((p) => !earlyWorkerResults.has(prepared.indexOf(p)) && p.chunks && (p.textToEmbed?.length ?? 0) > 0).length;
567
+ let storedFiles = 0;
559
568
  const storeLimit = pLimit(options.config.indexing.concurrency);
560
569
  const storeResults = await Promise.all(prepared.map((prep, fi) => storeLimit(async () => {
561
570
  if (aborted()) {
@@ -618,6 +627,8 @@ async function runIndexPassInner(options, logger) {
618
627
  enqueueManifestSave();
619
628
  }
620
629
  options.progress?.finishFile(prep.fileLabel);
630
+ storedFiles++;
631
+ logChunkProgress("Storing", prep.fileLabel, storedFiles, filesToStore, storedFiles, filesToStore);
621
632
  return result;
622
633
  })));
623
634
  const workerResults = storeResults;
package/dist/plugin.js CHANGED
@@ -22,9 +22,10 @@ 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 } 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
- import { existsSync, readFileSync, unlinkSync } from "node:fs";
28
+ import { existsSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
28
29
  import path from "node:path";
29
30
  import { fileURLToPath } from "node:url";
30
31
  import { spawn, execSync } from "node:child_process";
@@ -39,6 +40,10 @@ const backgroundIndexers = new Map();
39
40
  const mcpServers = new Map();
40
41
  /** Pending update notifications keyed by workspace directory. */
41
42
  const pendingUpdateInfo = new Map();
43
+ /** Workspace directories that have already been prompted about an update this session. */
44
+ const notifiedUpdateDirs = new Set();
45
+ /** Pending "restart needed" notice after a background auto-install. */
46
+ const pendingRestartNotice = new Map();
42
47
  /** Guard flag to prevent re-entrant shutdown. */
43
48
  let shutdownRegistered = false;
44
49
  /** Close all active background indexers and MCP servers, then destroy idle sockets. */
@@ -59,6 +64,8 @@ async function shutdownPluginResources() {
59
64
  }
60
65
  configCache.clear();
61
66
  pendingUpdateInfo.clear();
67
+ pendingRestartNotice.clear();
68
+ notifiedUpdateDirs.clear();
62
69
  destroyAllPooledConnections();
63
70
  }
64
71
  /** Register a process.beforeExit handler so that resources are cleaned up
@@ -685,17 +692,37 @@ export function createRagHooks(options) {
685
692
  ];
686
693
  output.system.unshift(guidance.join("\n"));
687
694
  }
688
- // Inject update notification if available
695
+ // Inject a one-time update prompt so the agent asks the user to install.
696
+ // Fires only once per workspace per session to avoid nagging on every turn.
689
697
  const updateInfo = pendingUpdateInfo.get(options.worktree);
690
- if (updateInfo) {
691
- output.system.unshift(`OpenCodeRAG update available: ${updateInfo.currentVersion} → ${updateInfo.latestVersion}. ` +
692
- `Run \`npm update -g opencode-rag-plugin && opencode-rag setup\` to install.`);
698
+ if (updateInfo && !notifiedUpdateDirs.has(options.worktree)) {
699
+ notifiedUpdateDirs.add(options.worktree);
700
+ pendingUpdateInfo.delete(options.worktree);
701
+ output.system.unshift(`A newer OpenCodeRAG version is available: ${updateInfo.currentVersion} → ${updateInfo.latestVersion}. ` +
702
+ `At the very start of your next response, briefly tell the user a new version is available and ask whether ` +
703
+ `they would like you to install it. If they agree, run \`opencode-rag update\` via the bash tool, then tell ` +
704
+ `them to restart OpenCode to load the new version. Do not mention this notice again.`);
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.`);
693
715
  }
694
716
  // Inject documentation mode system prompt if enabled
695
717
  const docMode = getEffectiveCfg().documentationMode;
696
718
  if (docMode?.enabled && docMode.systemPrompt) {
697
719
  output.system.unshift(docMode.systemPrompt);
698
720
  }
721
+ // Inject wiki mode system prompt if enabled
722
+ const wikiMode = getEffectiveCfg().wikiMode;
723
+ if (wikiMode?.enabled && wikiMode.systemPrompt) {
724
+ output.system.unshift(wikiMode.systemPrompt);
725
+ }
699
726
  },
700
727
  async "chat.message"(input, output) {
701
728
  try {
@@ -797,6 +824,64 @@ export function createRagHooks(options) {
797
824
  }
798
825
  return;
799
826
  }
827
+ // Handle /wiki slash command
828
+ if (text.startsWith("/wiki")) {
829
+ const wikiMode = getEffectiveCfg().wikiMode;
830
+ if (!wikiMode?.enabled) {
831
+ const parts = output?.parts ?? output?.message?.parts;
832
+ if (Array.isArray(parts) && parts.length > 0) {
833
+ const first = parts[0];
834
+ if (typeof first.text === "string") {
835
+ parts[0] = { ...first, text: "Wiki mode is not enabled. Set `wikiMode.enabled` to `true` in opencode-rag.json." };
836
+ }
837
+ }
838
+ return;
839
+ }
840
+ const arg = text.slice(5).trim().toLowerCase();
841
+ const wikiDir = path.join(options.worktree, ".opencode", "wiki");
842
+ const lines = [];
843
+ if (arg === "lint") {
844
+ lines.push("## Wiki Lint", "", "Run a health check on the wiki. Check for:", "- Orphan pages with no inbound [[links]]", "- Stale pages whose sourceRefs files changed since lastReviewed", "- Contradictions between pages", "- Important concepts mentioned but lacking their own page", "", "Append findings to .opencode/wiki/log.md and fix any issues found.");
845
+ }
846
+ else if (arg === "seed") {
847
+ lines.push("## Wiki Seed", "", "Generate the initial wiki from existing sources:", "1. Read README and top-level docs for project overview → create overview page", "2. Scan file structure with get_file_skeleton for major modules → create entity pages", "3. Identify common conventions from code patterns → create concept pages", "4. Create .opencode/wiki/index.md listing all pages", "5. Create .opencode/wiki/log.md with initial entry", "6. Keep pages focused — one concept per page, short prose, cross-referenced with [[links]]");
848
+ }
849
+ else {
850
+ if (existsSync(wikiDir)) {
851
+ const entries = readdirSync(wikiDir);
852
+ const pages = entries.filter((e) => e.endsWith(".md") && e !== "log.md").length;
853
+ const hasIndex = entries.includes("index.md");
854
+ const hasLog = entries.includes("log.md");
855
+ const dirs = entries.filter((e) => !e.includes(".")).length;
856
+ let lastLogEntry = "";
857
+ const logPath = path.join(wikiDir, "log.md");
858
+ if (hasLog && existsSync(logPath)) {
859
+ const logContent = readFileSync(logPath, "utf-8");
860
+ const logLines = logContent.trim().split("\n").filter((l) => l.startsWith("## ["));
861
+ if (logLines.length > 0) {
862
+ lastLogEntry = logLines[logLines.length - 1] ?? "";
863
+ }
864
+ }
865
+ lines.push("## Wiki Status", "", `Wiki: \`.opencode/wiki/\``, `Pages: ~${pages} (.md files)`, `Subdirectories: \`${dirs}\``, `Index: ${hasIndex ? "yes" : "no"} | Log: ${hasLog ? "yes" : "no"}`);
866
+ if (lastLogEntry) {
867
+ lines.push(`Latest log: \`${lastLogEntry}\``);
868
+ }
869
+ lines.push("", "Commands:", "- `/wiki` — show this status", "- `/wiki lint` — health-check the wiki", "- `/wiki seed` — generate initial wiki pages from the codebase");
870
+ }
871
+ else {
872
+ lines.push("## Wiki Status", "", "No wiki yet at `.opencode/wiki/`.", "Use `/wiki seed` to generate the initial wiki from existing sources.");
873
+ }
874
+ }
875
+ const wikiMsg = lines.join("\n");
876
+ const parts = output?.parts ?? output?.message?.parts;
877
+ if (Array.isArray(parts) && parts.length > 0) {
878
+ const first = parts[0];
879
+ if (typeof first.text === "string") {
880
+ parts[0] = { ...first, text: wikiMsg };
881
+ }
882
+ }
883
+ return;
884
+ }
800
885
  // Handle hotkey-triggered RAG injection (Ctrl+Enter / Ctrl+Alt+Enter)
801
886
  const pendingInjection = consumePendingRagInjection(options.storePath);
802
887
  if (pendingInjection) {
@@ -1090,6 +1175,8 @@ export const ragPlugin = async (input, _options) => {
1090
1175
  // Clean up stale config cache and pending update info for this directory
1091
1176
  configCache.delete(input.directory);
1092
1177
  pendingUpdateInfo.delete(input.directory);
1178
+ pendingRestartNotice.delete(input.directory);
1179
+ notifiedUpdateDirs.delete(input.directory);
1093
1180
  // Clean up idle HTTP sockets from previous provider connections
1094
1181
  destroyAllPooledConnections();
1095
1182
  appendDebugLog(logFilePath, {
@@ -1165,6 +1252,7 @@ export const ragPlugin = async (input, _options) => {
1165
1252
  logLevel,
1166
1253
  keywordIndex,
1167
1254
  descriptionProvider,
1255
+ dimension: vectorDimension,
1168
1256
  });
1169
1257
  backgroundIndexers.set(input.directory, indexer);
1170
1258
  }
@@ -1180,28 +1268,87 @@ export const ragPlugin = async (input, _options) => {
1180
1268
  catch { /* ignore */ }
1181
1269
  }
1182
1270
  }
1183
- // Auto-start MCP server if enabled (skip in temp dirs / test environments)
1184
- 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 };
1185
1276
  const isTempDir = path.resolve(input.directory).startsWith(tmpdir());
1186
1277
  if (mcpCfg.enabled && !isTempDir) {
1187
1278
  const mcpInstance = startMcpServerProcess(input.directory, logFilePath, logLevel);
1188
1279
  mcpServers.set(input.directory, mcpInstance);
1189
1280
  }
1190
- // Auto-update check (non-blocking, best-effort)
1281
+ // Auto-update check (non-blocking, best-effort). On by default; can be
1282
+ // disabled via `autoUpdate.enabled: false` in opencode-rag.json. When a newer
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.
1191
1286
  const autoUpdateCfg = effectiveCfg.autoUpdate;
1192
1287
  if (autoUpdateCfg?.enabled) {
1193
- const currentVersion = (() => {
1194
- try {
1195
- const pkgPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
1196
- return JSON.parse(readFileSync(pkgPath, "utf-8")).version;
1197
- }
1198
- catch {
1199
- return "0.0.0";
1200
- }
1201
- })();
1288
+ const currentVersion = getCurrentVersion();
1202
1289
  checkForUpdate(currentVersion)
1203
1290
  .then((info) => {
1204
- 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
1205
1352
  pendingUpdateInfo.set(input.directory, info);
1206
1353
  appendDebugLog(logFilePath, {
1207
1354
  scope: "updater",