opencode-rag-plugin 1.15.1 → 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.
Files changed (240) hide show
  1. package/ReadMe.md +52 -9
  2. package/dist/api.js +44 -24
  3. package/dist/chunker/base.d.ts +34 -3
  4. package/dist/chunker/base.js +70 -21
  5. package/dist/chunker/factory.d.ts +4 -1
  6. package/dist/chunker/factory.js +12 -1
  7. package/dist/chunker/grammar.js +3 -0
  8. package/dist/chunker/image.js +8 -8
  9. package/dist/chunker/pdf.js +11 -13
  10. package/dist/chunker/xml.d.ts +2 -0
  11. package/dist/chunker/xml.js +2 -0
  12. package/dist/cli/commands/index-command.js +0 -4
  13. package/dist/cli/commands/index.d.ts +1 -0
  14. package/dist/cli/commands/index.js +1 -0
  15. package/dist/cli/commands/init-helpers.d.ts +12 -0
  16. package/dist/cli/commands/init-helpers.js +87 -2
  17. package/dist/cli/commands/init.js +20 -2
  18. package/dist/cli/commands/query.js +1 -0
  19. package/dist/cli/commands/setup.d.ts +2 -0
  20. package/dist/cli/commands/setup.js +113 -0
  21. package/dist/cli/commands/status.js +64 -2
  22. package/dist/cli/commands/update.d.ts +4 -6
  23. package/dist/cli/commands/update.js +44 -58
  24. package/dist/cli/index.js +2 -1
  25. package/dist/content/image.js +24 -3
  26. package/dist/content/reader.d.ts +6 -1
  27. package/dist/content/reader.js +49 -4
  28. package/dist/core/config.d.ts +29 -1
  29. package/dist/core/config.js +105 -8
  30. package/dist/core/desc-cache.d.ts +31 -0
  31. package/dist/core/desc-cache.js +124 -0
  32. package/dist/core/interfaces.d.ts +58 -3
  33. package/dist/core/manifest.d.ts +24 -1
  34. package/dist/core/manifest.js +34 -3
  35. package/dist/core/resolve-api-key.js +4 -2
  36. package/dist/core/runtime-overrides.js +12 -8
  37. package/dist/core/setup-runtime.d.ts +23 -0
  38. package/dist/core/setup-runtime.js +231 -0
  39. package/dist/core/version-check.d.ts +85 -0
  40. package/dist/core/version-check.js +160 -0
  41. package/dist/describer/anthropic.d.ts +2 -2
  42. package/dist/describer/anthropic.js +5 -7
  43. package/dist/describer/describer.d.ts +2 -2
  44. package/dist/describer/describer.js +6 -8
  45. package/dist/describer/gemini.d.ts +2 -2
  46. package/dist/describer/gemini.js +5 -7
  47. package/dist/embedder/cohere.d.ts +1 -1
  48. package/dist/embedder/cohere.js +2 -2
  49. package/dist/embedder/factory.d.ts +3 -1
  50. package/dist/embedder/factory.js +8 -2
  51. package/dist/embedder/health.js +5 -1
  52. package/dist/embedder/http.d.ts +1 -1
  53. package/dist/embedder/http.js +74 -43
  54. package/dist/embedder/ollama.d.ts +1 -1
  55. package/dist/embedder/ollama.js +2 -2
  56. package/dist/embedder/openai.d.ts +1 -1
  57. package/dist/embedder/openai.js +2 -2
  58. package/dist/eval/compare-merge.d.ts +10 -0
  59. package/dist/eval/compare-merge.js +537 -0
  60. package/dist/eval/compare-rankings.d.ts +10 -0
  61. package/dist/eval/compare-rankings.js +245 -0
  62. package/dist/eval/dump-descriptions.d.ts +7 -0
  63. package/dist/eval/dump-descriptions.js +58 -0
  64. package/dist/eval/fast-index.d.ts +8 -0
  65. package/dist/eval/fast-index.js +283 -0
  66. package/dist/eval/run-branch-compare.d.ts +7 -0
  67. package/dist/eval/run-branch-compare.js +220 -0
  68. package/dist/eval/run-token-test.js +1 -0
  69. package/dist/eval/test-kw.d.ts +1 -0
  70. package/dist/eval/test-kw.js +22 -0
  71. package/dist/eval/update-descriptions.d.ts +7 -0
  72. package/dist/eval/update-descriptions.js +84 -0
  73. package/dist/index.d.ts +4 -0
  74. package/dist/index.js +3 -0
  75. package/dist/indexer/embed-stage.js +2 -1
  76. package/dist/indexer/git-diff.js +21 -9
  77. package/dist/indexer/pipeline.d.ts +6 -0
  78. package/dist/indexer/pipeline.js +304 -40
  79. package/dist/indexer/watch.js +1 -3
  80. package/dist/indexer/worker.d.ts +15 -2
  81. package/dist/indexer/worker.js +25 -12
  82. package/dist/mcp/handlers.d.ts +9 -0
  83. package/dist/mcp/handlers.js +23 -6
  84. package/dist/mcp/server.js +2 -0
  85. package/dist/opencode/create-read-tool.d.ts +2 -0
  86. package/dist/opencode/create-read-tool.js +8 -2
  87. package/dist/opencode/read-fallback.d.ts +1 -5
  88. package/dist/opencode/read-fallback.js +1 -18
  89. package/dist/opencode/read-format.js +5 -3
  90. package/dist/opencode/tools.js +5 -7
  91. package/dist/plugin.js +223 -71
  92. package/dist/retriever/keyword-index.d.ts +3 -2
  93. package/dist/retriever/keyword-index.js +25 -1
  94. package/dist/retriever/retriever.d.ts +4 -1
  95. package/dist/retriever/retriever.js +34 -56
  96. package/dist/tui.js +18 -1
  97. package/dist/vectorstore/lancedb.d.ts +35 -4
  98. package/dist/vectorstore/lancedb.js +146 -23
  99. package/dist/vectorstore/memory.d.ts +6 -1
  100. package/dist/vectorstore/memory.js +58 -0
  101. package/dist/watcher.d.ts +2 -0
  102. package/dist/watcher.js +5 -1
  103. package/dist/web/api.js +10 -2
  104. package/dist/web/server.js +18 -3
  105. package/package.json +8 -9
  106. package/scripts/postinstall-setup.js +82 -0
  107. package/dist/api.js.map +0 -1
  108. package/dist/chunker/base.js.map +0 -1
  109. package/dist/chunker/bash.js.map +0 -1
  110. package/dist/chunker/c.js.map +0 -1
  111. package/dist/chunker/cpp.js.map +0 -1
  112. package/dist/chunker/csharp.js.map +0 -1
  113. package/dist/chunker/css.js.map +0 -1
  114. package/dist/chunker/doc.js.map +0 -1
  115. package/dist/chunker/dockerfile.js.map +0 -1
  116. package/dist/chunker/docx.js.map +0 -1
  117. package/dist/chunker/excel.js.map +0 -1
  118. package/dist/chunker/factory.js.map +0 -1
  119. package/dist/chunker/fallback.js.map +0 -1
  120. package/dist/chunker/go.js.map +0 -1
  121. package/dist/chunker/grammar.js.map +0 -1
  122. package/dist/chunker/html.js.map +0 -1
  123. package/dist/chunker/image.js.map +0 -1
  124. package/dist/chunker/ini.js.map +0 -1
  125. package/dist/chunker/java.js.map +0 -1
  126. package/dist/chunker/javascript.js.map +0 -1
  127. package/dist/chunker/json.js.map +0 -1
  128. package/dist/chunker/kotlin.js.map +0 -1
  129. package/dist/chunker/loader.js.map +0 -1
  130. package/dist/chunker/markdown.js.map +0 -1
  131. package/dist/chunker/pdf.js.map +0 -1
  132. package/dist/chunker/php.js.map +0 -1
  133. package/dist/chunker/powershell.js.map +0 -1
  134. package/dist/chunker/python.js.map +0 -1
  135. package/dist/chunker/razor.js.map +0 -1
  136. package/dist/chunker/ruby.js.map +0 -1
  137. package/dist/chunker/rust.js.map +0 -1
  138. package/dist/chunker/sln.js.map +0 -1
  139. package/dist/chunker/sql.js.map +0 -1
  140. package/dist/chunker/ssl.js.map +0 -1
  141. package/dist/chunker/swift.js.map +0 -1
  142. package/dist/chunker/tex.js.map +0 -1
  143. package/dist/chunker/toml.js.map +0 -1
  144. package/dist/chunker/typescript.js.map +0 -1
  145. package/dist/chunker/uuid.js.map +0 -1
  146. package/dist/chunker/xml.js.map +0 -1
  147. package/dist/chunker/yaml.js.map +0 -1
  148. package/dist/cli/commands/clear.js.map +0 -1
  149. package/dist/cli/commands/describe-image.js.map +0 -1
  150. package/dist/cli/commands/dump.js.map +0 -1
  151. package/dist/cli/commands/eval.js.map +0 -1
  152. package/dist/cli/commands/index-command.js.map +0 -1
  153. package/dist/cli/commands/index.js.map +0 -1
  154. package/dist/cli/commands/init-helpers.js.map +0 -1
  155. package/dist/cli/commands/init.js.map +0 -1
  156. package/dist/cli/commands/list.js.map +0 -1
  157. package/dist/cli/commands/mcp.js.map +0 -1
  158. package/dist/cli/commands/query.js.map +0 -1
  159. package/dist/cli/commands/show.js.map +0 -1
  160. package/dist/cli/commands/status.js.map +0 -1
  161. package/dist/cli/commands/ui.js.map +0 -1
  162. package/dist/cli/commands/update.js.map +0 -1
  163. package/dist/cli/format.js.map +0 -1
  164. package/dist/cli/helpers.js.map +0 -1
  165. package/dist/cli/index.js.map +0 -1
  166. package/dist/cli/progress.d.ts +0 -42
  167. package/dist/cli/progress.js +0 -137
  168. package/dist/cli/progress.js.map +0 -1
  169. package/dist/cli/types.js.map +0 -1
  170. package/dist/cli.js.map +0 -1
  171. package/dist/content/doc.js.map +0 -1
  172. package/dist/content/docx.js.map +0 -1
  173. package/dist/content/excel.js.map +0 -1
  174. package/dist/content/image.js.map +0 -1
  175. package/dist/content/pdf.js.map +0 -1
  176. package/dist/content/reader.js.map +0 -1
  177. package/dist/content/types.js.map +0 -1
  178. package/dist/core/bootstrap.js.map +0 -1
  179. package/dist/core/config.js.map +0 -1
  180. package/dist/core/doc-progress.js.map +0 -1
  181. package/dist/core/fileLogger.js.map +0 -1
  182. package/dist/core/interfaces.js.map +0 -1
  183. package/dist/core/manifest.js.map +0 -1
  184. package/dist/core/provider-defaults.js.map +0 -1
  185. package/dist/core/rag-injection-flag.js.map +0 -1
  186. package/dist/core/resolve-api-key.js.map +0 -1
  187. package/dist/core/runtime-overrides.js.map +0 -1
  188. package/dist/describer/anthropic.js.map +0 -1
  189. package/dist/describer/describer.js.map +0 -1
  190. package/dist/describer/factory.js.map +0 -1
  191. package/dist/describer/gemini.js.map +0 -1
  192. package/dist/describer/shared.js.map +0 -1
  193. package/dist/embedder/cohere.js.map +0 -1
  194. package/dist/embedder/factory.js.map +0 -1
  195. package/dist/embedder/health.js.map +0 -1
  196. package/dist/embedder/http.js.map +0 -1
  197. package/dist/embedder/ollama.js.map +0 -1
  198. package/dist/embedder/openai.js.map +0 -1
  199. package/dist/eval/index.js.map +0 -1
  200. package/dist/eval/run-token-test.js.map +0 -1
  201. package/dist/eval/session-logger.js.map +0 -1
  202. package/dist/eval/storage.js.map +0 -1
  203. package/dist/eval/token-analysis.js.map +0 -1
  204. package/dist/eval/token-counter.js.map +0 -1
  205. package/dist/eval/types.js.map +0 -1
  206. package/dist/index.js.map +0 -1
  207. package/dist/indexer/description-stage.js.map +0 -1
  208. package/dist/indexer/embed-stage.js.map +0 -1
  209. package/dist/indexer/git-diff.js.map +0 -1
  210. package/dist/indexer/metadata.js.map +0 -1
  211. package/dist/indexer/pipeline.js.map +0 -1
  212. package/dist/indexer/stats.js.map +0 -1
  213. package/dist/indexer/watch.js.map +0 -1
  214. package/dist/indexer/worker.js.map +0 -1
  215. package/dist/indexer.js.map +0 -1
  216. package/dist/mcp/cli.js.map +0 -1
  217. package/dist/mcp/handlers.js.map +0 -1
  218. package/dist/mcp/server.js.map +0 -1
  219. package/dist/opencode/create-read-tool.js.map +0 -1
  220. package/dist/opencode/read-fallback.js.map +0 -1
  221. package/dist/opencode/read-format.js.map +0 -1
  222. package/dist/opencode/read-query.js.map +0 -1
  223. package/dist/opencode/tool-args.js.map +0 -1
  224. package/dist/opencode/tools.js.map +0 -1
  225. package/dist/plugin-entry.js.map +0 -1
  226. package/dist/plugin.js.map +0 -1
  227. package/dist/retriever/context-optimizer.js.map +0 -1
  228. package/dist/retriever/keyword-index.js.map +0 -1
  229. package/dist/retriever/retriever.js.map +0 -1
  230. package/dist/tui.js.map +0 -1
  231. package/dist/updater.d.ts +0 -45
  232. package/dist/updater.js +0 -175
  233. package/dist/updater.js.map +0 -1
  234. package/dist/vectorstore/factory.js.map +0 -1
  235. package/dist/vectorstore/lancedb.js.map +0 -1
  236. package/dist/vectorstore/memory.js.map +0 -1
  237. package/dist/watcher.js.map +0 -1
  238. package/dist/web/api.js.map +0 -1
  239. package/dist/web/server.js.map +0 -1
  240. package/dist/web/static.js.map +0 -1
@@ -4,7 +4,8 @@
4
4
  import fs from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import pLimit from "p-limit";
7
- import { computeFileHash, normalizeFilePath } from "../core/manifest.js";
7
+ import { computeFileHash, computeDescriptionConfigHash, normalizeFilePath } from "../core/manifest.js";
8
+ import { DescriptionCache } from "../core/desc-cache.js";
8
9
  import { createImageVisionProvider, } from "../chunker/image.js";
9
10
  import * as pdfExtractor from "./pdf.js";
10
11
  import * as docxExtractor from "./docx.js";
@@ -84,8 +85,11 @@ async function dispatchExtraction(filePath, buffer, imageVisionProvider, imagePr
84
85
  * Scan the workspace directory for indexable files, reading content or dispatching
85
86
  * binary extraction (PDF, DOCX, DOC, Excel, images). Respects the file manifest
86
87
  * for incremental re-indexing by skipping unchanged files.
88
+ *
89
+ * @param descCache - Optional persistent description cache. When set, image
90
+ * descriptions are cached and reused across sessions (survives aborted runs).
87
91
  */
88
- export async function scanWorkspaceFiles(cwd, config, logger, manifest, filterPaths) {
92
+ export async function scanWorkspaceFiles(cwd, config, logger, manifest, filterPaths, injectedVisionProvider, descCache) {
89
93
  const extensions = new Set(config.indexing.includeExtensions);
90
94
  let imageVisionProvider = null;
91
95
  let imagePrompt;
@@ -95,7 +99,7 @@ export async function scanWorkspaceFiles(cwd, config, logger, manifest, filterPa
95
99
  for (const ext of imageExtractor.SUPPORTED_IMAGE_EXTENSIONS) {
96
100
  extensions.add(ext.toLowerCase());
97
101
  }
98
- imageVisionProvider = createImageVisionProvider(imageCfg);
102
+ imageVisionProvider = injectedVisionProvider ?? createImageVisionProvider(imageCfg);
99
103
  imagePrompt = imageCfg.prompt;
100
104
  imageResizeMaxDimension = imageCfg.resizeMaxDimension;
101
105
  }
@@ -135,6 +139,10 @@ export async function scanWorkspaceFiles(cwd, config, logger, manifest, filterPa
135
139
  const scanConcurrency = Math.min(config.indexing.concurrency * 2, 16);
136
140
  const textLimit = pLimit(scanConcurrency);
137
141
  const imageLimit = pLimit(1);
142
+ // Compute a hash of the image description config — used to build cache keys.
143
+ const imageDescConfigHash = config.imageDescription?.enabled
144
+ ? computeDescriptionConfigHash(config) ?? ""
145
+ : "";
138
146
  let completed = 0;
139
147
  async function processFile(filePath) {
140
148
  const normalizedPath = normalizeFilePath(filePath);
@@ -168,11 +176,48 @@ export async function scanWorkspaceFiles(cwd, config, logger, manifest, filterPa
168
176
  excelExtractor.EXCEL_EXTENSIONS.has(filePath.toLowerCase()) ||
169
177
  isImage;
170
178
  logger?.info(`Reading: ${filePath}`);
179
+ const buffer = isBinary ? await fs.readFile(filePath) : Buffer.alloc(0);
180
+ // For images, check the persistent description cache before calling the vision provider
181
+ if (isImage && descCache && imageDescConfigHash) {
182
+ const imageBytesHash = computeFileHash(buffer.toString("base64"));
183
+ const cacheKey = DescriptionCache.imageKey(imageBytesHash, imageDescConfigHash);
184
+ const cachedDesc = descCache.get(cacheKey);
185
+ if (cachedDesc) {
186
+ logger?.info(` Using cached image description: ${filePath}`);
187
+ const content = cachedDesc;
188
+ let fileMtime;
189
+ let fileSize;
190
+ try {
191
+ const stat = await fs.stat(filePath);
192
+ fileMtime = stat.mtimeMs;
193
+ fileSize = stat.size;
194
+ }
195
+ catch { /* best-effort stat */ }
196
+ completed++;
197
+ return {
198
+ filePath,
199
+ normalizedPath,
200
+ content,
201
+ hash: computeFileHash(content),
202
+ isEmpty: false,
203
+ isTooSmall: false,
204
+ extractionStatus: "ok",
205
+ mtime: fileMtime,
206
+ size: fileSize,
207
+ };
208
+ }
209
+ }
171
210
  if (isImage) {
172
211
  logger?.info(` Describing image: ${filePath}`);
173
212
  }
174
- const buffer = isBinary ? await fs.readFile(filePath) : Buffer.alloc(0);
175
213
  const result = await dispatchExtraction(filePath, buffer, imageVisionProvider, imagePrompt, imageResizeMaxDimension);
214
+ // Cache the image description for future runs
215
+ if (isImage && result.ok && descCache && imageDescConfigHash) {
216
+ const imageBytesHash = computeFileHash(buffer.toString("base64"));
217
+ const cacheKey = DescriptionCache.imageKey(imageBytesHash, imageDescConfigHash);
218
+ descCache.set(cacheKey, result.content);
219
+ await descCache.save();
220
+ }
176
221
  if (!result.ok) {
177
222
  logger?.warn(` ${filePath} (extraction failed: ${result.error})`);
178
223
  }
@@ -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). */
@@ -164,6 +171,8 @@ export interface RagConfig {
164
171
  documentPrefix?: string;
165
172
  /** Prefix prepended to queries before embedding (e.g. "search_query:"). */
166
173
  queryPrefix?: string;
174
+ /** Cached embedding vector dimension. Probed once on first startup, then persisted to config. */
175
+ vectorDimension?: number;
167
176
  };
168
177
  /** Indexing pipeline controls: what to index, concurrency, batch sizes. */
169
178
  indexing: {
@@ -187,6 +196,12 @@ export interface RagConfig {
187
196
  ollamaMaxBatchSize?: number;
188
197
  /** Maximum concurrent description generation requests. */
189
198
  descriptionConcurrency?: number;
199
+ /**
200
+ * Maximum file size in bytes for SVG/XML files before chunking is skipped.
201
+ * Large SVGs can cause tree-sitter to hang. Set to 0 for no limit.
202
+ * @default 1_048_576 (1 MB)
203
+ */
204
+ maxSvgSizeBytes?: number;
190
205
  };
191
206
  /** Vector storage backend configuration. */
192
207
  vectorStore: {
@@ -227,6 +242,8 @@ export interface RagConfig {
227
242
  readNoResultsBehavior?: ReadNoResultsBehavior;
228
243
  /** Maximum related files shown when read results are empty. */
229
244
  readRelatedFilesMax?: number;
245
+ /** Whether to inject RAG tool guidance into system prompts (default true). */
246
+ injectSystemPrompt?: boolean;
230
247
  };
231
248
  /** Custom chunker module registrations. */
232
249
  chunkers?: ChunkerConfig[];
@@ -241,6 +258,8 @@ export interface RagConfig {
241
258
  imageDescription?: ImageDescriptionConfig;
242
259
  /** Automated documentation mode config. */
243
260
  documentationMode?: DocumentationModeConfig;
261
+ /** Wiki mode config that instructs the AI agent to maintain a persistent knowledge wiki. */
262
+ wikiMode?: WikiModeConfig;
244
263
  /** MCP server integration config. */
245
264
  mcp?: McpConfig;
246
265
  /** Auto-update checking config. */
@@ -293,3 +312,12 @@ export declare const CONFIG_FILE_CANDIDATES: string[];
293
312
  export declare function findConfigFile(directory: string): string | undefined;
294
313
  /** Load and parse a JSON config file, deep-merge with defaults, optionally validate. */
295
314
  export declare function loadConfig(filePath: string, validate?: boolean): RagConfig;
315
+ /**
316
+ * Persist a probed embedding vector dimension into the config JSON file.
317
+ * Sets `embedding.vectorDimension` so subsequent startups skip the probe.
318
+ * This is a best-effort operation — failures are silently ignored.
319
+ *
320
+ * @param configPath - Absolute path to the config JSON file.
321
+ * @param dimension - The vector dimension to persist.
322
+ */
323
+ export declare function persistProbedDimension(configPath: string, dimension: number): void;
@@ -2,7 +2,7 @@
2
2
  * @fileoverview Configuration types and defaults for OpenCodeRAG. Defines RagConfig,
3
3
  * all sub-config interfaces, DEFAULT_CONFIG, validateConfig, and loadConfig.
4
4
  */
5
- import { existsSync, readFileSync } from "node:fs";
5
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
6
6
  import path from "node:path";
7
7
  import { env } from "node:process";
8
8
  export const DEFAULT_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,8 +104,9 @@ 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
+ maxSvgSizeBytes: 1_048_576,
109
110
  },
110
111
  vectorStore: {
111
112
  path: "./.opencode/rag_db",
@@ -113,7 +114,7 @@ export const DEFAULT_CONFIG = {
113
114
  },
114
115
  retrieval: {
115
116
  topK: 20,
116
- minScore: 0.5,
117
+ minScore: 0.35,
117
118
  hybridSearch: {
118
119
  enabled: true,
119
120
  keywordWeight: 0.4,
@@ -130,6 +131,7 @@ export const DEFAULT_CONFIG = {
130
131
  enabled: true,
131
132
  maxContextChunks: 10,
132
133
  readOverride: true,
134
+ injectSystemPrompt: true,
133
135
  autoIndex: {
134
136
  enabled: false,
135
137
  debounceMs: 2000,
@@ -143,7 +145,7 @@ export const DEFAULT_CONFIG = {
143
145
  model: "minicpm-v4.6:latest",
144
146
  baseUrl: "http://127.0.0.1:11434/api",
145
147
  timeoutMs: 60000,
146
- prompt: "Describe this image 10-20 comma-separated keywords.",
148
+ prompt: "Describe this image in 1-2 sentences: what it shows, its purpose, and key visual elements.",
147
149
  think: false,
148
150
  numCtx: 2048,
149
151
  resizeMaxDimension: 1024,
@@ -156,7 +158,7 @@ export const DEFAULT_CONFIG = {
156
158
  think: false,
157
159
  numCtx: 4096,
158
160
  timeoutMs: 60000,
159
- systemPrompt: "Describe this code in 10-20 comma-separated keywords.",
161
+ systemPrompt: "Describe this code in 2-3 sentences: purpose, key concepts, inputs/outputs, and dependencies. No code repetition.",
160
162
  batchConcurrency: 1,
161
163
  retryMax: 3,
162
164
  retryBaseDelayMs: 1000,
@@ -181,11 +183,72 @@ export const DEFAULT_CONFIG = {
181
183
  "## Output format\n\n" +
182
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.",
183
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
+ },
184
247
  mcp: {
185
248
  enabled: true,
186
249
  },
187
250
  autoUpdate: {
188
- enabled: false,
251
+ enabled: true,
189
252
  },
190
253
  ui: {
191
254
  port: 3210,
@@ -213,7 +276,7 @@ export function validateConfig(config) {
213
276
  const KNOWN_TOP_KEYS = new Set([
214
277
  "embedding", "indexing", "vectorStore", "retrieval",
215
278
  "openCode", "chunkers", "chunking", "description",
216
- "imageDescription", "documentationMode", "mcp", "autoUpdate", "ui", "tui", "logging",
279
+ "imageDescription", "documentationMode", "wikiMode", "mcp", "autoUpdate", "ui", "tui", "logging",
217
280
  ]);
218
281
  const topKeys = new Set(Object.keys(config));
219
282
  for (const key of topKeys) {
@@ -331,6 +394,8 @@ export function loadConfig(filePath, validate = true) {
331
394
  let parsed;
332
395
  try {
333
396
  raw = readFileSync(filePath, "utf-8");
397
+ if (raw.charCodeAt(0) === 0xfeff)
398
+ raw = raw.slice(1);
334
399
  parsed = JSON.parse(raw);
335
400
  }
336
401
  catch (err) {
@@ -397,6 +462,10 @@ export function loadConfig(filePath, validate = true) {
397
462
  ...DEFAULT_CONFIG.documentationMode,
398
463
  ...(safeObj(parsed.documentationMode) ?? {}),
399
464
  },
465
+ wikiMode: {
466
+ ...DEFAULT_CONFIG.wikiMode,
467
+ ...(safeObj(parsed.wikiMode) ?? {}),
468
+ },
400
469
  mcp: {
401
470
  ...DEFAULT_CONFIG.mcp,
402
471
  ...(safeObj(parsed.mcp) ?? {}),
@@ -426,4 +495,32 @@ export function loadConfig(filePath, validate = true) {
426
495
  }
427
496
  return cfg;
428
497
  }
498
+ /**
499
+ * Persist a probed embedding vector dimension into the config JSON file.
500
+ * Sets `embedding.vectorDimension` so subsequent startups skip the probe.
501
+ * This is a best-effort operation — failures are silently ignored.
502
+ *
503
+ * @param configPath - Absolute path to the config JSON file.
504
+ * @param dimension - The vector dimension to persist.
505
+ */
506
+ export function persistProbedDimension(configPath, dimension) {
507
+ let raw;
508
+ try {
509
+ raw = readFileSync(configPath, "utf-8");
510
+ }
511
+ catch {
512
+ return;
513
+ }
514
+ try {
515
+ const obj = JSON.parse(raw);
516
+ const embedding = (obj.embedding ??= {});
517
+ if (embedding.vectorDimension === dimension)
518
+ return; // already set
519
+ embedding.vectorDimension = dimension;
520
+ writeFileSync(configPath, JSON.stringify(obj, null, 2), "utf-8");
521
+ }
522
+ catch {
523
+ // best-effort
524
+ }
525
+ }
429
526
  //# sourceMappingURL=config.js.map
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @fileoverview Persistent description cache keyed by content hash + description config hash.
3
+ * Survives aborted index runs so that descriptions are not regenerated unnecessarily.
4
+ * Two namespaces: "code" (per-chunk descriptions) and "image" (per-file image descriptions).
5
+ */
6
+ export declare class DescriptionCache {
7
+ private cache;
8
+ private cacheDir;
9
+ private dirty;
10
+ private savePromise;
11
+ private loadPromise;
12
+ constructor(cacheDir: string);
13
+ /** Load the cache from disk. */
14
+ load(): Promise<void>;
15
+ /** Get a cached description for the given key. */
16
+ get(key: string): string | undefined;
17
+ /** Set a cached description for the given key. */
18
+ set(key: string, description: string): void;
19
+ /** Set multiple entries at once. */
20
+ setMany(entries: Array<[string, string]>): void;
21
+ /** Check if a key exists in the cache. */
22
+ has(key: string): boolean;
23
+ /** Persist the cache to disk if dirty. Safe to call multiple times. */
24
+ save(): Promise<void>;
25
+ /** Build a cache key for a code chunk. */
26
+ static codeKey(content: string, descConfigHash: string): string;
27
+ /** Build a cache key for an image file. */
28
+ static imageKey(imageBytesHash: string, imageDescConfigHash: string): string;
29
+ /** Remove old entries to stay under the limit. */
30
+ private maybeEvict;
31
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * @fileoverview Persistent description cache keyed by content hash + description config hash.
3
+ * Survives aborted index runs so that descriptions are not regenerated unnecessarily.
4
+ * Two namespaces: "code" (per-chunk descriptions) and "image" (per-file image descriptions).
5
+ */
6
+ import fs from "node:fs/promises";
7
+ import path from "node:path";
8
+ import { createHash } from "node:crypto";
9
+ const CACHE_VERSION = 1;
10
+ const MAX_ENTRIES = 50_000;
11
+ const MAX_AGE_DAYS = 30;
12
+ export class DescriptionCache {
13
+ cache = { version: CACHE_VERSION, entries: {} };
14
+ cacheDir;
15
+ dirty = false;
16
+ savePromise = Promise.resolve();
17
+ loadPromise = null;
18
+ constructor(cacheDir) {
19
+ this.cacheDir = cacheDir;
20
+ }
21
+ /** Load the cache from disk. */
22
+ async load() {
23
+ if (this.loadPromise)
24
+ return this.loadPromise;
25
+ this.loadPromise = (async () => {
26
+ const cachePath = path.join(this.cacheDir, ".desc-cache.json");
27
+ try {
28
+ const raw = await fs.readFile(cachePath, "utf-8");
29
+ const parsed = JSON.parse(raw);
30
+ if (parsed.version === CACHE_VERSION && parsed.entries) {
31
+ this.cache = parsed;
32
+ this.dirty = false;
33
+ }
34
+ }
35
+ catch {
36
+ // Cache file missing or corrupt — start fresh
37
+ }
38
+ })();
39
+ return this.loadPromise;
40
+ }
41
+ /** Get a cached description for the given key. */
42
+ get(key) {
43
+ return this.cache.entries[key]?.description;
44
+ }
45
+ /** Set a cached description for the given key. */
46
+ set(key, description) {
47
+ this.cache.entries[key] = {
48
+ description,
49
+ createdAt: Date.now(),
50
+ };
51
+ this.dirty = true;
52
+ this.maybeEvict();
53
+ }
54
+ /** Set multiple entries at once. */
55
+ setMany(entries) {
56
+ const now = Date.now();
57
+ for (const [key, description] of entries) {
58
+ this.cache.entries[key] = { description, createdAt: now };
59
+ }
60
+ this.dirty = true;
61
+ this.maybeEvict();
62
+ }
63
+ /** Check if a key exists in the cache. */
64
+ has(key) {
65
+ return key in this.cache.entries;
66
+ }
67
+ /** Persist the cache to disk if dirty. Safe to call multiple times. */
68
+ async save() {
69
+ if (!this.dirty)
70
+ return;
71
+ this.savePromise = this.savePromise.then(async () => {
72
+ const cachePath = path.join(this.cacheDir, ".desc-cache.json");
73
+ const tmpPath = cachePath + ".tmp";
74
+ try {
75
+ await fs.mkdir(this.cacheDir, { recursive: true });
76
+ await fs.writeFile(tmpPath, JSON.stringify(this.cache), "utf-8");
77
+ try {
78
+ await fs.rename(tmpPath, cachePath);
79
+ }
80
+ catch {
81
+ await fs.unlink(cachePath).catch(() => { });
82
+ await fs.rename(tmpPath, cachePath);
83
+ }
84
+ this.dirty = false;
85
+ }
86
+ catch {
87
+ // Best-effort save
88
+ }
89
+ });
90
+ return this.savePromise;
91
+ }
92
+ /** Build a cache key for a code chunk. */
93
+ static codeKey(content, descConfigHash) {
94
+ const contentHash = createHash("sha256").update(content).digest("hex").slice(0, 16);
95
+ return contentHash + "_" + descConfigHash.slice(0, 16);
96
+ }
97
+ /** Build a cache key for an image file. */
98
+ static imageKey(imageBytesHash, imageDescConfigHash) {
99
+ return imageBytesHash.slice(0, 16) + "_" + imageDescConfigHash.slice(0, 16);
100
+ }
101
+ /** Remove old entries to stay under the limit. */
102
+ maybeEvict() {
103
+ const keys = Object.keys(this.cache.entries);
104
+ if (keys.length <= MAX_ENTRIES)
105
+ return;
106
+ const cutoff = Date.now() - MAX_AGE_DAYS * 24 * 60 * 60 * 1000;
107
+ const entries = this.cache.entries;
108
+ for (const key of keys) {
109
+ if (entries[key].createdAt < cutoff) {
110
+ delete entries[key];
111
+ }
112
+ }
113
+ // If still over limit, delete oldest
114
+ if (Object.keys(entries).length > MAX_ENTRIES) {
115
+ const sorted = Object.entries(entries)
116
+ .sort(([, a], [, b]) => a.createdAt - b.createdAt);
117
+ const toDelete = Object.keys(entries).length - MAX_ENTRIES;
118
+ for (let i = 0; i < toDelete; i++) {
119
+ delete entries[sorted[i][0]];
120
+ }
121
+ }
122
+ }
123
+ }
124
+ //# sourceMappingURL=desc-cache.js.map
@@ -22,12 +22,26 @@ export interface DescriptionLogger {
22
22
  warn(message: string): void;
23
23
  debug(message: string): void;
24
24
  }
25
+ /** Progress callback invoked after each chunk is described during batch generation. */
26
+ export type ChunkProgressFn = (chunk: Chunk, completed: number, total: number) => void;
27
+ /** Optional progress reporting for batch description generation. */
28
+ export interface BatchDescriptionOptions {
29
+ /** Total number of chunks across the whole run (may exceed `chunks.length` when called per sub-batch). */
30
+ total?: number;
31
+ /** Called once per completed chunk with the chunk, the running completed count, and `total`. */
32
+ onProgress?: ChunkProgressFn;
33
+ }
25
34
  /** Generates natural-language descriptions for code chunks using an LLM. */
26
35
  export interface DescriptionProvider {
27
36
  /** Generate a description for a single chunk. */
28
37
  generateDescription(chunk: Chunk): Promise<string>;
29
- /** Generate descriptions for multiple chunks concurrently. Returns a Map of chunk ID to description. */
30
- generateBatchDescriptions(chunks: Chunk[], logger?: DescriptionLogger): Promise<Map<string, string>>;
38
+ /**
39
+ * Generate descriptions for multiple chunks concurrently. Returns a Map of chunk ID to description.
40
+ * @param chunks - Chunks to describe.
41
+ * @param logger - Optional logger for diagnostic messages.
42
+ * @param opts - Optional progress reporting options.
43
+ */
44
+ generateBatchDescriptions(chunks: Chunk[], logger?: DescriptionLogger, opts?: BatchDescriptionOptions): Promise<Map<string, string>>;
31
45
  }
32
46
  /** Explains how a search result score was computed, including vector and keyword contributions. */
33
47
  export interface SearchExplanation {
@@ -43,6 +57,10 @@ export interface SearchExplanation {
43
57
  rawKeywordScore: number;
44
58
  /** Weight applied to keyword score during fusion (0-1). */
45
59
  keywordWeight: number;
60
+ /** Rank (0-indexed) in the vector store results. */
61
+ vectorRank?: number;
62
+ /** Rank (0-indexed) in the keyword index results. */
63
+ keywordRank?: number;
46
64
  };
47
65
  /** Query terms that matched in the keyword index, if hybrid search was used. */
48
66
  matchedTerms?: string[];
@@ -86,12 +104,14 @@ export interface EmbeddingProvider {
86
104
  }
87
105
  /** In-memory TF-IDF inverted index for keyword-based search alongside vector search. */
88
106
  export interface KeywordIndex {
107
+ /** Release resources and free memory. */
108
+ close(): void;
89
109
  /** Index a set of chunks by tokenizing their content. */
90
110
  addChunks(chunks: Chunk[]): void;
91
111
  /** Remove all entries for a given file path from the index. */
92
112
  removeByFilePath(filePath: string): void;
93
113
  /** Search the index for the top-K matching chunks. */
94
- search(query: string, topK: number): SearchResult[];
114
+ search(query: string, topK: number, filter?: MetadataFilter): SearchResult[];
95
115
  /** Get the terms from a query that matched a specific chunk. */
96
116
  getMatchedTerms(query: string, chunkId: string): string[];
97
117
  /** Clear all indexed data. */
@@ -101,20 +121,55 @@ export interface KeywordIndex {
101
121
  /** Persist the index to disk as JSON. */
102
122
  save(filePath?: string): Promise<void>;
103
123
  }
124
+ /** A paginated chunk summary for dump/list operations. */
125
+ export interface ChunkSummary {
126
+ id: string;
127
+ filePath: string;
128
+ language: string;
129
+ startLine: number;
130
+ endLine: number;
131
+ content: string;
132
+ description: string;
133
+ }
134
+ /** A file summary for list operations. */
135
+ export interface FileSummary {
136
+ filePath: string;
137
+ language: string;
138
+ chunkCount: number;
139
+ }
104
140
  /** Persistent vector storage and retrieval backend (LanceDB or in-memory). */
105
141
  export interface VectorStore {
106
142
  /** Store a batch of chunks with their embeddings. */
107
143
  addChunks(chunks: Chunk[]): Promise<void>;
108
144
  /** Search for the top-K nearest neighbor chunks by embedding similarity. */
109
145
  search(embedding: number[], topK: number): Promise<SearchResult[]>;
146
+ /** Search with optional metadata filtering. */
147
+ searchWithFilter(embedding: number[], topK: number, filter?: MetadataFilter): Promise<SearchResult[]>;
110
148
  /** Return the total number of stored chunks. */
111
149
  count(): Promise<number>;
112
150
  /** Remove all stored data. */
113
151
  clear(): Promise<void>;
114
152
  /** Delete all chunks associated with a specific file path. */
115
153
  deleteByFilePath(filePath: string): Promise<void>;
154
+ /** Return all unique file paths currently stored in the index. */
155
+ getFilePaths(): Promise<string[]>;
116
156
  /** Release any held resources and close the store. */
117
157
  close(): Promise<void>;
158
+ /** Retrieve a paginated list of chunks without embeddings. */
159
+ getChunks(offset: number, limit: number): Promise<ChunkSummary[]>;
160
+ /** List all distinct file paths with language and chunk count. */
161
+ listFiles(): Promise<FileSummary[]>;
162
+ /** Retrieve all chunks for a specific file path, sorted by start line. */
163
+ getChunksByFilePath(filePath: string): Promise<Chunk[]>;
164
+ /** Re-open the store, optionally pointing at a new database path. */
165
+ reopen?(newPath?: string): Promise<void>;
166
+ }
167
+ /** Filter criteria for narrowing search results by file path patterns or language. */
168
+ export interface MetadataFilter {
169
+ /** Glob-style path patterns (e.g. "src/**", "lib/auth/*"). */
170
+ pathPatterns?: string[];
171
+ /** Language identifiers (e.g. ["typescript", "tsx"]). */
172
+ languages?: string[];
118
173
  }
119
174
  /** Callback interface for reporting indexing progress to the UI or CLI. */
120
175
  export interface IndexProgress {