opencode-rag-plugin 1.17.0 → 1.17.2

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
@@ -41,10 +41,14 @@ opencode-rag query "authentication middleware"
41
41
  | **OpenCode plugin** | Auto-inject context, read-tool override, TUI settings, Ctrl+Enter to add RAG context, MCP registration on `init` |
42
42
  | **Incremental indexing** | File-hash manifest, background watcher, auto-rebuild on corruption |
43
43
  | **Privacy-first** | All processing stays local (when using Ollama) |
44
- | **CLI Tools** | `index`, `query`, `status`, `list`, `show`, `dump`, `clear`, `init`, `ui`, `mcp` |
44
+ | **CLI Tools** | `init`, `index`, `query`, `status`, `list`, `show`, `dump`, `clear`, `describe-image`, `ui`, `mcp`, `setup`, `eval:sessions`, `eval:analyze`, `eval:compare` |
45
45
  | **Proxy-aware** | Corporate proxy support with raw-socket localhost bypass |
46
46
  | **OpenAI / Anthropic / Cohere** | Use alternate embedding providers with API key auto-resolution |
47
47
  | **Evaluation** | Session-level token tracking, RAG-on vs RAG-off comparison, tiktoken BPE counting |
48
+ | **Documentation mode** | `/doc` slash command: agent adds JSDoc/TSDoc to undocumented files, progress tracked per subdirectory |
49
+ | **Wiki mode** | `/wiki` slash command: agent maintains a persistent knowledge wiki at `.opencode/wiki/` (ingest, query, lint, seed) |
50
+ | **Context optimization** | Post-retrieval dedup, per-file chunk limits, adjacent-merge to fit the context window |
51
+ | **AGENTS.md directive** | `opencode-rag init` merges a tool-usage directive into `AGENTS.md` via sentinel markers |
48
52
 
49
53
  ## Web UI
50
54
 
@@ -80,6 +84,47 @@ OpenCodeRAG can index image files (PNG, JPEG, WebP, etc.) by sending them to a v
80
84
 
81
85
  **Disabled by default** — enable in `opencode-rag.json` to opt in (recommended for dedicated GPUs).
82
86
 
87
+ ## Documentation Mode
88
+
89
+ When `documentationMode.enabled` is `true`, the OpenCode plugin exposes a `/doc` slash command that drives the agent to add JSDoc/TSDoc comments to undocumented source files. No agent tools are registered — documentation is driven entirely through the slash command and the documentation system prompt.
90
+
91
+ ```json
92
+ {
93
+ "documentationMode": {
94
+ "enabled": true,
95
+ "batchSize": 5
96
+ }
97
+ }
98
+ ```
99
+
100
+ **Workflow:** Type `/doc` to list all undocumented files grouped by subdirectory. The agent picks a subdirectory, documents every public symbol (preserving existing comments and implementation code), then types `/doc src/auth/` to mark that subdirectory complete. Progress is persisted in `.opencode/rag_db/doc-mode-progress.json` so subsequent sessions resume where you left off.
101
+
102
+ See [Plugin documentation](doc/plugin.md#5-documentation-mode--slash-command-doc) for details.
103
+
104
+ ## Wiki Mode
105
+
106
+ When `wikiMode.enabled` is `true`, the OpenCode plugin injects a wiki-maintainer system prompt that instructs the agent to build and maintain a persistent knowledge wiki at `.opencode/wiki/` — a structured, interlinked collection of markdown pages synthesizing knowledge from the codebase, docs, and conversations.
107
+
108
+ ```json
109
+ {
110
+ "wikiMode": {
111
+ "enabled": true
112
+ }
113
+ }
114
+ ```
115
+
116
+ **Slash commands:**
117
+
118
+ | Command | Action |
119
+ |---|---|
120
+ | `/wiki` | Show wiki status (page count, subdirectories, index/log presence, latest log entry) |
121
+ | `/wiki seed` | Generate the initial wiki from README, file structure, and code patterns |
122
+ | `/wiki lint` | Health-check for orphan pages, stale source refs, contradictions, missing cross-links |
123
+
124
+ The agent maintains all wiki pages during normal coding sessions (ingest on learning). The user never writes wiki pages directly. The wiki is git-trackable in `.opencode/` so every edit is a diff.
125
+
126
+ See [Plugin documentation](doc/plugin.md#6-wiki-mode--slash-command-wiki) for the full protocol.
127
+
83
128
  ## MCP Server
84
129
 
85
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.).
@@ -124,10 +169,10 @@ OpenCodeRAG registers tools that agents can invoke directly. Agents discover the
124
169
 
125
170
  When using OpenCode, the plugin enhances your agent with three discovery mechanisms:
126
171
 
127
- ### 1. Skill-Based Discovery (Recommended)
172
+ ### 1. Skill-Based Discovery
128
173
  `opencode-rag init` creates `.opencode/skills/opencode-rag/SKILL.md` - an OpenCode skill that teaches agents the tool workflow. Agents load it on demand via the `skill` tool, keeping token overhead minimal.
129
174
 
130
- ### 2. System Prompt Guidance (Conditional)
175
+ ### 2. System Prompt Guidance
131
176
  When chunks are indexed, a brief tool list is prepended to the system prompt so agents know the tools exist. This is skipped when no chunks are indexed to save tokens.
132
177
 
133
178
  ### 3. On-Demand RAG Context (Ctrl+Enter / Ctrl+Alt+Enter)
@@ -12,5 +12,6 @@ export { registerInitCommand } from "./init.js";
12
12
  export { registerUiCommand } from "./ui.js";
13
13
  export { registerMcpCommand } from "./mcp.js";
14
14
  export { registerSetupCommand } from "./setup.js";
15
+ export { registerUpdateCommand } from "./update.js";
15
16
  export { registerEvalSessionsCommand, registerEvalAnalyzeCommand, registerEvalCompareCommand } from "./eval.js";
16
17
  export { registerDescribeImageCommand } from "./describe-image.js";
@@ -12,6 +12,7 @@ export { registerInitCommand } from "./init.js";
12
12
  export { registerUiCommand } from "./ui.js";
13
13
  export { registerMcpCommand } from "./mcp.js";
14
14
  export { registerSetupCommand } from "./setup.js";
15
+ export { registerUpdateCommand } from "./update.js";
15
16
  export { registerEvalSessionsCommand, registerEvalAnalyzeCommand, registerEvalCompareCommand } from "./eval.js";
16
17
  export { registerDescribeImageCommand } from "./describe-image.js";
17
18
  //# sourceMappingURL=index.js.map
@@ -460,6 +460,9 @@ export function generateDefaultConfigJson() {
460
460
  timeoutMs: DEFAULT_CONFIG.description.timeoutMs,
461
461
  maxContentChars: DEFAULT_CONFIG.description.maxContentChars,
462
462
  },
463
+ wikiMode: {
464
+ enabled: DEFAULT_CONFIG.wikiMode.enabled,
465
+ },
463
466
  mcp: {
464
467
  enabled: DEFAULT_CONFIG.mcp.enabled,
465
468
  },
@@ -147,12 +147,12 @@ export function registerStatusCommand(program) {
147
147
  logCliInfo(logFilePath, "status", `${c.label("Runtime:")} ${c.warn("version unknown — run `opencode-rag setup`")}`);
148
148
  }
149
149
  }
150
- // Async GitHub update check (fire-and-forget, 5s timeout)
151
- // Only phone home if autoUpdate is explicitly enabled (opt-in).
150
+ // Async GitHub update check (fire-and-forget, 5s timeout).
151
+ // Runs unless autoUpdate is explicitly disabled.
152
152
  if (config.autoUpdate?.enabled) {
153
153
  checkForUpdate(pkg.version).then((info) => {
154
154
  if (info.updateAvailable) {
155
- process.stdout.write(` ${c.label("Update:")} ${c.warn(`v${info.latestVersion} available — npm update -g opencode-rag-plugin`)}\n`);
155
+ process.stdout.write(` ${c.label("Update:")} ${c.warn(`v${info.latestVersion} available — run \`opencode-rag update\` to install`)}\n`);
156
156
  }
157
157
  }).catch(() => { });
158
158
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @fileoverview `update` command — checks GitHub for a newer OpenCodeRAG release and installs it.
3
+ */
4
+ /**
5
+ * `update` command — checks for a newer published version of OpenCodeRAG and,
6
+ * by default, installs it (via `npm install -g ...@latest`) then re-syncs the
7
+ * OpenCode runtime junctions so the new build is picked up on next restart.
8
+ */
9
+ import type { Command } from "commander";
10
+ /**
11
+ * Register the `update` command on the given Commander program.
12
+ *
13
+ * @param program - The Commander `Command` instance to register on.
14
+ */
15
+ export declare function registerUpdateCommand(program: Command): void;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @fileoverview `update` command — checks GitHub for a newer OpenCodeRAG release and installs it.
3
+ */
4
+ /**
5
+ * `update` command — checks for a newer published version of OpenCodeRAG and,
6
+ * by default, installs it (via `npm install -g ...@latest`) then re-syncs the
7
+ * OpenCode runtime junctions so the new build is picked up on next restart.
8
+ */
9
+ import { c } from "../format.js";
10
+ import { checkForUpdate, getCurrentVersion, installLatestUpdate } from "../../core/version-check.js";
11
+ /**
12
+ * Register the `update` command on the given Commander program.
13
+ *
14
+ * @param program - The Commander `Command` instance to register on.
15
+ */
16
+ export function registerUpdateCommand(program) {
17
+ program
18
+ .command("update")
19
+ .description("Check for and install the newest published version of OpenCodeRAG")
20
+ .option("--check", "only check whether an update is available; do not install")
21
+ .option("-v, --verbose", "stream npm/setup output to the console")
22
+ .action(async (options) => {
23
+ const currentVersion = getCurrentVersion();
24
+ console.log(`\n${c.heading("OpenCodeRAG Update")}\n`);
25
+ console.log(` ${c.label("Current version:")} ${c.value(currentVersion)}`);
26
+ console.log(` ${c.label("Checking...")} `);
27
+ let info;
28
+ try {
29
+ info = await checkForUpdate(currentVersion);
30
+ }
31
+ catch {
32
+ console.log(`\n ${c.warn("Could not reach the update server. Check your network and try again.")}\n`);
33
+ process.exit(1);
34
+ return;
35
+ }
36
+ if (!info.updateAvailable) {
37
+ console.log(`\n ${c.success("Already up-to-date.")} (${c.value(info.latestVersion || currentVersion)})\n`);
38
+ return;
39
+ }
40
+ console.log(` ${c.label("Latest version:")} ${c.value(info.latestVersion)}`);
41
+ if (info.publishedAt) {
42
+ console.log(` ${c.label("Published:")} ${c.dim(info.publishedAt)}`);
43
+ }
44
+ if (info.releaseUrl) {
45
+ console.log(` ${c.label("Release notes:")} ${c.file(info.releaseUrl)}`);
46
+ }
47
+ if (options.check) {
48
+ console.log(`\n ${c.warn("Update available.")} Run ${c.file("opencode-rag update")} to install.\n`);
49
+ return;
50
+ }
51
+ console.log(`\n ${c.dim("Installing newest version...")}\n`);
52
+ const result = await installLatestUpdate({ verbose: options.verbose });
53
+ if (result.success) {
54
+ console.log(` ${c.success("✓")} ${result.message}`);
55
+ console.log(`\n ${c.dim("Restart OpenCode if it is running to load the new version.")}\n`);
56
+ }
57
+ else {
58
+ console.error(` ${c.error("✗")} ${result.message}`);
59
+ console.error(`\n ${c.error("Update failed. You can retry with `opencode-rag update` or install manually:")}\n`);
60
+ console.error(` ${c.dim(" npm install -g opencode-rag-plugin@latest && opencode-rag setup")}\n`);
61
+ process.exit(1);
62
+ }
63
+ });
64
+ }
65
+ //# sourceMappingURL=update.js.map
package/dist/cli/index.js CHANGED
@@ -14,7 +14,7 @@ import { realpathSync } from "node:fs";
14
14
  import { basename, dirname } from "node:path";
15
15
  import { fileURLToPath } from "node:url";
16
16
  import { getPackageMetadata } from "./helpers.js";
17
- import { registerIndexCommand, registerQueryCommand, registerClearCommand, registerStatusCommand, registerListCommand, registerShowCommand, registerDumpCommand, registerInitCommand, registerUiCommand, registerMcpCommand, registerSetupCommand, registerEvalSessionsCommand, registerEvalAnalyzeCommand, registerEvalCompareCommand, registerDescribeImageCommand, } from "./commands/index.js";
17
+ import { registerIndexCommand, registerQueryCommand, registerClearCommand, registerStatusCommand, registerListCommand, registerShowCommand, registerDumpCommand, registerInitCommand, registerUiCommand, registerMcpCommand, registerSetupCommand, registerUpdateCommand, registerEvalSessionsCommand, registerEvalAnalyzeCommand, registerEvalCompareCommand, registerDescribeImageCommand, } from "./commands/index.js";
18
18
  /**
19
19
  * The top-level Commander program instance that defines the `opencode-rag` CLI.
20
20
  *
@@ -39,6 +39,7 @@ registerDescribeImageCommand(program);
39
39
  registerUiCommand(program);
40
40
  registerMcpCommand(program);
41
41
  registerSetupCommand(program);
42
+ registerUpdateCommand(program);
42
43
  registerEvalSessionsCommand(program);
43
44
  registerEvalAnalyzeCommand(program);
44
45
  registerEvalCompareCommand(program);
@@ -114,6 +114,13 @@ export interface DocumentationModeConfig {
114
114
  /** System prompt for the documentation agent. */
115
115
  systemPrompt: string;
116
116
  }
117
+ /** Configuration for the wiki mode that instructs the AI agent to maintain a persistent knowledge wiki. */
118
+ export interface WikiModeConfig {
119
+ /** Whether wiki mode is enabled. */
120
+ enabled: boolean;
121
+ /** System prompt for the wiki maintainer agent. */
122
+ systemPrompt: string;
123
+ }
117
124
  /** Configuration for the terminal UI (TUI) keybindings. */
118
125
  export interface TuiConfig {
119
126
  /** Keybinding to toggle the file list panel. */
@@ -128,7 +135,7 @@ export interface McpConfig {
128
135
  }
129
136
  /** Configuration for automatic self-updates. */
130
137
  export interface AutoUpdateConfig {
131
- /** Whether auto-update checking is enabled. */
138
+ /** Whether update checking is enabled (defaults to true). */
132
139
  enabled: boolean;
133
140
  }
134
141
  /** Configuration for post-retrieval context window optimization (adjacent merge, similarity dedup, file diversity cap). */
@@ -251,6 +258,8 @@ export interface RagConfig {
251
258
  imageDescription?: ImageDescriptionConfig;
252
259
  /** Automated documentation mode config. */
253
260
  documentationMode?: DocumentationModeConfig;
261
+ /** Wiki mode config that instructs the AI agent to maintain a persistent knowledge wiki. */
262
+ wikiMode?: WikiModeConfig;
254
263
  /** MCP server integration config. */
255
264
  mcp?: McpConfig;
256
265
  /** Auto-update checking config. */
@@ -10,7 +10,7 @@ export const DEFAULT_CONFIG = {
10
10
  provider: "ollama",
11
11
  baseUrl: "http://127.0.0.1:11434/api",
12
12
  model: "qwen3-embedding:0.6b",
13
- timeoutMs: 30000,
13
+ timeoutMs: 120000,
14
14
  },
15
15
  indexing: {
16
16
  includeExtensions: [
@@ -104,7 +104,7 @@ export const DEFAULT_CONFIG = {
104
104
  concurrency: 8,
105
105
  embedBatchSize: 100,
106
106
  embedConcurrency: 6,
107
- ollamaMaxBatchSize: 4000,
107
+ ollamaMaxBatchSize: 500,
108
108
  descriptionConcurrency: 4,
109
109
  maxSvgSizeBytes: 1_048_576,
110
110
  },
@@ -183,11 +183,72 @@ export const DEFAULT_CONFIG = {
183
183
  "## Output format\n\n" +
184
184
  "Return your changes as a list of file paths with the full new content of the comment block for each modified symbol. Do NOT output the entire file unless asked.",
185
185
  },
186
+ wikiMode: {
187
+ enabled: false,
188
+ systemPrompt: "You are a wiki maintainer for this codebase. You incrementally build and maintain " +
189
+ "a persistent knowledge wiki at `.opencode/wiki/` — a structured, interlinked collection " +
190
+ "of markdown files that synthesizes knowledge from the codebase, documentation, and your conversations.\n\n" +
191
+ "## Three layers\n\n" +
192
+ "- **Raw sources**: code, docs, READMEs (immutable — read with `search_semantic`, `get_file_skeleton`, `find_usages`, `read`)\n" +
193
+ "- **Wiki**: `.opencode/wiki/*.md` (you own this layer — create, update, and maintain ALL pages)\n" +
194
+ "- **Schema**: this prompt (the protocol you follow)\n\n" +
195
+ "## Wiki layout\n\n" +
196
+ "`.opencode/wiki/`\n" +
197
+ "- `index.md` — content catalog: every page listed with a link and one-line summary\n" +
198
+ "- `log.md` — append-only timeline: `## [date] op | title`\n" +
199
+ "- `entities/` — pages for modules, services, key classes, components\n" +
200
+ "- `concepts/` — pages for patterns, conventions, architectural decisions, gotchas\n" +
201
+ "- `sources/` — summaries of external sources (articles, specs, discussions)\n\n" +
202
+ "## Page frontmatter\n\n" +
203
+ "Every page should start with:\n" +
204
+ "```\n" +
205
+ "---\n" +
206
+ "title: <page title>\n" +
207
+ "tags: [relevant tags]\n" +
208
+ "sourceRefs: [file paths or URLs this page synthesizes]\n" +
209
+ "lastReviewed: <date>\n" +
210
+ "---\n" +
211
+ "```\n\n" +
212
+ "## Operations\n\n" +
213
+ "### Ingest (when you learn something new)\n" +
214
+ "1. Read the source (code, doc, external article)\n" +
215
+ "2. Extract key information — what it does, why it exists, how it connects to existing knowledge\n" +
216
+ "3. Create or update relevant entity/concept pages\n" +
217
+ "4. Add cross-references with `[[wiki/page-name]]` links between related pages\n" +
218
+ "5. Update `index.md` with any new pages\n" +
219
+ "6. Append to `log.md`: `## [date] ingest | page title`\n\n" +
220
+ "### Query (when the user asks a question)\n" +
221
+ "1. Read `index.md` first to find relevant wiki pages\n" +
222
+ "2. Read those pages with the `read` tool\n" +
223
+ "3. If the wiki is insufficient, use `search_semantic` for code-level retrieval\n" +
224
+ "4. Synthesize an answer citing wiki pages where possible\n" +
225
+ "5. If the answer produced valuable analysis, file it back as a new wiki page\n\n" +
226
+ "### Lint (periodically, or when `/wiki lint` is invoked)\n" +
227
+ "Check for:\n" +
228
+ "- Orphan pages with no inbound `[[links]]`\n" +
229
+ "- Stale pages whose `sourceRefs` files changed since `lastReviewed`\n" +
230
+ "- Contradictions between pages\n" +
231
+ "- Important concepts mentioned but lacking their own page\n" +
232
+ "Append findings to `log.md`.\n\n" +
233
+ "### Seed (when `/wiki seed` is invoked)\n" +
234
+ "Generate initial wiki from existing sources:\n" +
235
+ "1. Read README and top-level docs for project overview → create overview page\n" +
236
+ "2. Scan file structure with `get_file_skeleton` for major modules → create entity pages\n" +
237
+ "3. Identify common conventions from code patterns → create concept pages\n" +
238
+ "4. Create `index.md` listing all pages\n" +
239
+ "5. Create `log.md` with initial entry\n" +
240
+ "6. Keep pages focused — one concept per page, short prose\n\n" +
241
+ "## Principles\n\n" +
242
+ "- You write and maintain ALL wiki pages — never ask the user to do it\n" +
243
+ "- Good answers and analyses get filed back as new pages — explorations compound\n" +
244
+ "- One concept per page — short, focused, cross-referenced\n" +
245
+ "- The wiki is git-trackable in `.opencode/` — every edit is a diff",
246
+ },
186
247
  mcp: {
187
248
  enabled: true,
188
249
  },
189
250
  autoUpdate: {
190
- enabled: false,
251
+ enabled: true,
191
252
  },
192
253
  ui: {
193
254
  port: 3210,
@@ -215,7 +276,7 @@ export function validateConfig(config) {
215
276
  const KNOWN_TOP_KEYS = new Set([
216
277
  "embedding", "indexing", "vectorStore", "retrieval",
217
278
  "openCode", "chunkers", "chunking", "description",
218
- "imageDescription", "documentationMode", "mcp", "autoUpdate", "ui", "tui", "logging",
279
+ "imageDescription", "documentationMode", "wikiMode", "mcp", "autoUpdate", "ui", "tui", "logging",
219
280
  ]);
220
281
  const topKeys = new Set(Object.keys(config));
221
282
  for (const key of topKeys) {
@@ -333,6 +394,8 @@ export function loadConfig(filePath, validate = true) {
333
394
  let parsed;
334
395
  try {
335
396
  raw = readFileSync(filePath, "utf-8");
397
+ if (raw.charCodeAt(0) === 0xfeff)
398
+ raw = raw.slice(1);
336
399
  parsed = JSON.parse(raw);
337
400
  }
338
401
  catch (err) {
@@ -399,6 +462,10 @@ export function loadConfig(filePath, validate = true) {
399
462
  ...DEFAULT_CONFIG.documentationMode,
400
463
  ...(safeObj(parsed.documentationMode) ?? {}),
401
464
  },
465
+ wikiMode: {
466
+ ...DEFAULT_CONFIG.wikiMode,
467
+ ...(safeObj(parsed.wikiMode) ?? {}),
468
+ },
402
469
  mcp: {
403
470
  ...DEFAULT_CONFIG.mcp,
404
471
  ...(safeObj(parsed.mcp) ?? {}),
@@ -11,7 +11,9 @@ export function loadRuntimeOverrides(storePath) {
11
11
  if (!existsSync(overridePath))
12
12
  return {};
13
13
  try {
14
- return JSON.parse(readFileSync(overridePath, "utf-8"));
14
+ const raw = readFileSync(overridePath, "utf-8");
15
+ const stripped = raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw;
16
+ return JSON.parse(stripped);
15
17
  }
16
18
  catch {
17
19
  return {};
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import os from "node:os";
3
- import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync, symlinkSync, } from "node:fs";
3
+ import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync, symlinkSync, lstatSync, } from "node:fs";
4
4
  import { execSync } from "node:child_process";
5
5
  const PLUGIN_NAME = "opencode-rag-plugin";
6
6
  export function getRuntimeDir() {
@@ -131,38 +131,86 @@ export async function setupRuntime(options) {
131
131
  writeFileSync(runtimePkg, JSON.stringify({ private: true, type: "module" }, null, 2) + "\n", "utf-8");
132
132
  }
133
133
  removeIfExists(runtimePluginDir);
134
+ // Ensure stale directory is fully gone before creating junction
135
+ let retries = 3;
136
+ while (retries > 0 && existsSync(runtimePluginDir)) {
137
+ try {
138
+ rmSync(runtimePluginDir, { recursive: true, force: true });
139
+ }
140
+ catch { /* retry */ }
141
+ retries--;
142
+ }
134
143
  mkdirSync(path.dirname(runtimePluginDir), { recursive: true });
144
+ let junctionOk = false;
135
145
  try {
136
146
  createJunction(globalPluginDir, runtimePluginDir);
147
+ junctionOk = process.platform !== "win32";
148
+ if (process.platform === "win32") {
149
+ const stat = lstatSync(runtimePluginDir);
150
+ junctionOk = stat.isSymbolicLink();
151
+ }
137
152
  }
138
153
  catch {
154
+ // fall through to cpSync
155
+ }
156
+ if (!junctionOk) {
157
+ if (!options?.silent) {
158
+ console.error(" [warn] Junction not supported, falling back to copy...");
159
+ }
139
160
  const { cpSync } = await import("node:fs");
140
161
  cpSync(globalPluginDir, runtimePluginDir, { recursive: true });
141
162
  }
163
+ // Ensure the @opencode-ai/plugin SDK is available globally.
164
+ // We must NOT run `npm install` inside the runtime dir because npm
165
+ // re-resolves all of node_modules/ and replaces the plugin junction
166
+ // with the published npm version (corrupting the local link).
167
+ if (!existsSync(globalSdkPluginDir)) {
168
+ try {
169
+ execSync(`npm install -g @opencode-ai/plugin`, {
170
+ stdio: "pipe",
171
+ timeout: 60_000,
172
+ });
173
+ }
174
+ catch (cause) {
175
+ errors.push(`Failed to install @opencode-ai/plugin SDK globally: ${cause.message}`);
176
+ return { success: false, errors };
177
+ }
178
+ }
179
+ // Create junction from global SDK to runtime (same pattern as plugin above)
142
180
  if (existsSync(globalSdkPluginDir)) {
143
181
  removeIfExists(runtimeSdkPluginDir);
182
+ retries = 3;
183
+ while (retries > 0 && existsSync(runtimeSdkPluginDir)) {
184
+ try {
185
+ rmSync(runtimeSdkPluginDir, { recursive: true, force: true });
186
+ }
187
+ catch { /* retry */ }
188
+ retries--;
189
+ }
144
190
  mkdirSync(runtimeSdkDir, { recursive: true });
191
+ let sdkJunctionOk = false;
145
192
  try {
146
193
  createJunction(globalSdkPluginDir, runtimeSdkPluginDir);
194
+ sdkJunctionOk = process.platform !== "win32";
195
+ if (process.platform === "win32") {
196
+ const stat = lstatSync(runtimeSdkPluginDir);
197
+ sdkJunctionOk = stat.isSymbolicLink();
198
+ }
147
199
  }
148
200
  catch {
201
+ // fall through to cpSync
202
+ }
203
+ if (!sdkJunctionOk) {
204
+ if (!options?.silent) {
205
+ console.error(" [warn] SDK junction not supported, falling back to copy...");
206
+ }
149
207
  const { cpSync } = await import("node:fs");
150
208
  cpSync(globalSdkPluginDir, runtimeSdkPluginDir, { recursive: true });
151
209
  }
152
210
  }
153
211
  else {
154
- mkdirSync(runtimeSdkDir, { recursive: true });
155
- try {
156
- execSync(`npm install @opencode-ai/plugin --no-save`, {
157
- cwd: runtimeDir,
158
- stdio: "pipe",
159
- timeout: 60_000,
160
- });
161
- }
162
- catch (cause) {
163
- errors.push(`Failed to install @opencode-ai/plugin SDK: ${cause.message}`);
164
- return { success: false, errors };
165
- }
212
+ errors.push(`@opencode-ai/plugin SDK not available after global install`);
213
+ return { success: false, errors };
166
214
  }
167
215
  writeFileSync(versionFile, pluginVersion, "utf-8");
168
216
  patchWindowsWrappers(npmGlobalRoot);
@@ -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,9 @@ 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 } from "./core/version-check.js";
26
26
  import { destroyAllPooledConnections } from "./embedder/http.js";
27
- import { existsSync, readFileSync, unlinkSync } from "node:fs";
27
+ import { existsSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
28
28
  import path from "node:path";
29
29
  import { fileURLToPath } from "node:url";
30
30
  import { spawn, execSync } from "node:child_process";
@@ -39,6 +39,8 @@ const backgroundIndexers = new Map();
39
39
  const mcpServers = new Map();
40
40
  /** Pending update notifications keyed by workspace directory. */
41
41
  const pendingUpdateInfo = new Map();
42
+ /** Workspace directories that have already been prompted about an update this session. */
43
+ const notifiedUpdateDirs = new Set();
42
44
  /** Guard flag to prevent re-entrant shutdown. */
43
45
  let shutdownRegistered = false;
44
46
  /** Close all active background indexers and MCP servers, then destroy idle sockets. */
@@ -59,6 +61,7 @@ async function shutdownPluginResources() {
59
61
  }
60
62
  configCache.clear();
61
63
  pendingUpdateInfo.clear();
64
+ notifiedUpdateDirs.clear();
62
65
  destroyAllPooledConnections();
63
66
  }
64
67
  /** Register a process.beforeExit handler so that resources are cleaned up
@@ -685,17 +688,27 @@ export function createRagHooks(options) {
685
688
  ];
686
689
  output.system.unshift(guidance.join("\n"));
687
690
  }
688
- // Inject update notification if available
691
+ // Inject a one-time update prompt so the agent asks the user to install.
692
+ // Fires only once per workspace per session to avoid nagging on every turn.
689
693
  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.`);
694
+ if (updateInfo && !notifiedUpdateDirs.has(options.worktree)) {
695
+ notifiedUpdateDirs.add(options.worktree);
696
+ pendingUpdateInfo.delete(options.worktree);
697
+ output.system.unshift(`A newer OpenCodeRAG version is available: ${updateInfo.currentVersion} → ${updateInfo.latestVersion}. ` +
698
+ `At the very start of your next response, briefly tell the user a new version is available and ask whether ` +
699
+ `they would like you to install it. If they agree, run \`opencode-rag update\` via the bash tool, then tell ` +
700
+ `them to restart OpenCode to load the new version. Do not mention this notice again.`);
693
701
  }
694
702
  // Inject documentation mode system prompt if enabled
695
703
  const docMode = getEffectiveCfg().documentationMode;
696
704
  if (docMode?.enabled && docMode.systemPrompt) {
697
705
  output.system.unshift(docMode.systemPrompt);
698
706
  }
707
+ // Inject wiki mode system prompt if enabled
708
+ const wikiMode = getEffectiveCfg().wikiMode;
709
+ if (wikiMode?.enabled && wikiMode.systemPrompt) {
710
+ output.system.unshift(wikiMode.systemPrompt);
711
+ }
699
712
  },
700
713
  async "chat.message"(input, output) {
701
714
  try {
@@ -797,6 +810,64 @@ export function createRagHooks(options) {
797
810
  }
798
811
  return;
799
812
  }
813
+ // Handle /wiki slash command
814
+ if (text.startsWith("/wiki")) {
815
+ const wikiMode = getEffectiveCfg().wikiMode;
816
+ if (!wikiMode?.enabled) {
817
+ const parts = output?.parts ?? output?.message?.parts;
818
+ if (Array.isArray(parts) && parts.length > 0) {
819
+ const first = parts[0];
820
+ if (typeof first.text === "string") {
821
+ parts[0] = { ...first, text: "Wiki mode is not enabled. Set `wikiMode.enabled` to `true` in opencode-rag.json." };
822
+ }
823
+ }
824
+ return;
825
+ }
826
+ const arg = text.slice(5).trim().toLowerCase();
827
+ const wikiDir = path.join(options.worktree, ".opencode", "wiki");
828
+ const lines = [];
829
+ if (arg === "lint") {
830
+ 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.");
831
+ }
832
+ else if (arg === "seed") {
833
+ 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]]");
834
+ }
835
+ else {
836
+ if (existsSync(wikiDir)) {
837
+ const entries = readdirSync(wikiDir);
838
+ const pages = entries.filter((e) => e.endsWith(".md") && e !== "log.md").length;
839
+ const hasIndex = entries.includes("index.md");
840
+ const hasLog = entries.includes("log.md");
841
+ const dirs = entries.filter((e) => !e.includes(".")).length;
842
+ let lastLogEntry = "";
843
+ const logPath = path.join(wikiDir, "log.md");
844
+ if (hasLog && existsSync(logPath)) {
845
+ const logContent = readFileSync(logPath, "utf-8");
846
+ const logLines = logContent.trim().split("\n").filter((l) => l.startsWith("## ["));
847
+ if (logLines.length > 0) {
848
+ lastLogEntry = logLines[logLines.length - 1] ?? "";
849
+ }
850
+ }
851
+ lines.push("## Wiki Status", "", `Wiki: \`.opencode/wiki/\``, `Pages: ~${pages} (.md files)`, `Subdirectories: \`${dirs}\``, `Index: ${hasIndex ? "yes" : "no"} | Log: ${hasLog ? "yes" : "no"}`);
852
+ if (lastLogEntry) {
853
+ lines.push(`Latest log: \`${lastLogEntry}\``);
854
+ }
855
+ lines.push("", "Commands:", "- `/wiki` — show this status", "- `/wiki lint` — health-check the wiki", "- `/wiki seed` — generate initial wiki pages from the codebase");
856
+ }
857
+ else {
858
+ lines.push("## Wiki Status", "", "No wiki yet at `.opencode/wiki/`.", "Use `/wiki seed` to generate the initial wiki from existing sources.");
859
+ }
860
+ }
861
+ const wikiMsg = lines.join("\n");
862
+ const parts = output?.parts ?? output?.message?.parts;
863
+ if (Array.isArray(parts) && parts.length > 0) {
864
+ const first = parts[0];
865
+ if (typeof first.text === "string") {
866
+ parts[0] = { ...first, text: wikiMsg };
867
+ }
868
+ }
869
+ return;
870
+ }
800
871
  // Handle hotkey-triggered RAG injection (Ctrl+Enter / Ctrl+Alt+Enter)
801
872
  const pendingInjection = consumePendingRagInjection(options.storePath);
802
873
  if (pendingInjection) {
@@ -1090,6 +1161,7 @@ export const ragPlugin = async (input, _options) => {
1090
1161
  // Clean up stale config cache and pending update info for this directory
1091
1162
  configCache.delete(input.directory);
1092
1163
  pendingUpdateInfo.delete(input.directory);
1164
+ notifiedUpdateDirs.delete(input.directory);
1093
1165
  // Clean up idle HTTP sockets from previous provider connections
1094
1166
  destroyAllPooledConnections();
1095
1167
  appendDebugLog(logFilePath, {
@@ -1165,6 +1237,7 @@ export const ragPlugin = async (input, _options) => {
1165
1237
  logLevel,
1166
1238
  keywordIndex,
1167
1239
  descriptionProvider,
1240
+ dimension: vectorDimension,
1168
1241
  });
1169
1242
  backgroundIndexers.set(input.directory, indexer);
1170
1243
  }
@@ -1187,18 +1260,13 @@ export const ragPlugin = async (input, _options) => {
1187
1260
  const mcpInstance = startMcpServerProcess(input.directory, logFilePath, logLevel);
1188
1261
  mcpServers.set(input.directory, mcpInstance);
1189
1262
  }
1190
- // Auto-update check (non-blocking, best-effort)
1263
+ // Auto-update check (non-blocking, best-effort). On by default; can be
1264
+ // 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).
1191
1267
  const autoUpdateCfg = effectiveCfg.autoUpdate;
1192
1268
  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
- })();
1269
+ const currentVersion = getCurrentVersion();
1202
1270
  checkForUpdate(currentVersion)
1203
1271
  .then((info) => {
1204
1272
  if (info.updateAvailable) {
package/dist/tui.js CHANGED
@@ -212,7 +212,9 @@ function getConfigPath(worktree) {
212
212
  /** Read and parse a JSON file, returning undefined on failure. */
213
213
  function readJsonFile(filePath) {
214
214
  try {
215
- return JSON.parse(readFileSync(filePath, "utf-8"));
215
+ const raw = readFileSync(filePath, "utf-8");
216
+ const stripped = raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw;
217
+ return JSON.parse(stripped);
216
218
  }
217
219
  catch {
218
220
  return undefined;
@@ -341,6 +343,8 @@ function buildSettingCategories(cfg, ro, providers) {
341
343
  const descRo = (ro.description ?? {});
342
344
  const docModeCfg = (cfg.documentationMode ?? {});
343
345
  const docModeRo = (ro.documentationMode ?? {});
346
+ const wikiModeCfg = (cfg.wikiMode ?? {});
347
+ const wikiModeRo = (ro.wikiMode ?? {});
344
348
  const embeddingCfg = (cfg.embedding ?? {});
345
349
  const embeddingRo = (ro.embedding ?? {});
346
350
  const tuiCfg = (cfg.tui ?? {});
@@ -471,6 +475,19 @@ function buildSettingCategories(cfg, ro, providers) {
471
475
  },
472
476
  ],
473
477
  },
478
+ {
479
+ id: "wiki",
480
+ label: "Wiki Mode",
481
+ description: "Configure the AI-maintained knowledge wiki that synthesizes codebase knowledge over time",
482
+ entries: [
483
+ {
484
+ path: ["wikiMode", "enabled"],
485
+ label: "Wiki mode",
486
+ type: "boolean",
487
+ currentValue: wikiModeRo.enabled ?? wikiModeCfg.enabled ?? false,
488
+ },
489
+ ],
490
+ },
474
491
  {
475
492
  id: "keybindings",
476
493
  label: "Keybindings",
package/dist/watcher.d.ts CHANGED
@@ -30,6 +30,8 @@ export interface CreateBackgroundIndexerOptions {
30
30
  keywordIndex?: KeywordIndex;
31
31
  /** Optional provider for generating LLM-based chunk descriptions. */
32
32
  descriptionProvider?: DescriptionProvider;
33
+ /** Embedding vector dimension — enables atomic rebuilds instead of destructive clear. */
34
+ dimension?: number;
33
35
  }
34
36
  /** The current operational status of the background indexer watcher. */
35
37
  export type WatcherStatus = {
package/dist/watcher.js CHANGED
@@ -28,7 +28,7 @@ function writeWatcherStatus(storePath, status) {
28
28
  * @returns A BackgroundIndexer handle with a close() method for shutdown.
29
29
  */
30
30
  export function createBackgroundIndexer(options) {
31
- const { cwd, storePath, config, store, embedder, logFilePath, logLevel, keywordIndex, descriptionProvider } = options;
31
+ const { cwd, storePath, config, store, embedder, logFilePath, logLevel, keywordIndex, descriptionProvider, dimension } = options;
32
32
  writeWatcherStatus(storePath, { running: false, lastRunAt: undefined });
33
33
  const ac = new AbortController();
34
34
  const updateStatus = (partial) => {
@@ -45,6 +45,7 @@ export function createBackgroundIndexer(options) {
45
45
  embedder,
46
46
  keywordIndex,
47
47
  descriptionProvider,
48
+ dimension,
48
49
  filterPaths,
49
50
  abortSignal: ac.signal,
50
51
  logger: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-rag-plugin",
3
- "version": "1.17.0",
3
+ "version": "1.17.2",
4
4
  "description": "OpenCode plugin for local-first RAG-based semantic code search",
5
5
  "type": "module",
6
6
  "main": "./dist/plugin-entry.js",