opencode-rag-plugin 1.15.1 → 1.17.0

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 (232) hide show
  1. package/ReadMe.md +4 -6
  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 -1
  14. package/dist/cli/commands/index.js +1 -1
  15. package/dist/cli/commands/init-helpers.d.ts +12 -0
  16. package/dist/cli/commands/init-helpers.js +84 -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/index.js +2 -2
  23. package/dist/content/image.js +24 -3
  24. package/dist/content/reader.d.ts +6 -1
  25. package/dist/content/reader.js +49 -4
  26. package/dist/core/config.d.ts +19 -0
  27. package/dist/core/config.js +34 -4
  28. package/dist/core/desc-cache.d.ts +31 -0
  29. package/dist/core/desc-cache.js +124 -0
  30. package/dist/core/interfaces.d.ts +58 -3
  31. package/dist/core/manifest.d.ts +24 -1
  32. package/dist/core/manifest.js +34 -3
  33. package/dist/core/resolve-api-key.js +4 -2
  34. package/dist/core/runtime-overrides.js +9 -7
  35. package/dist/core/setup-runtime.d.ts +23 -0
  36. package/dist/core/setup-runtime.js +183 -0
  37. package/dist/core/version-check.d.ts +9 -0
  38. package/dist/core/version-check.js +49 -0
  39. package/dist/describer/anthropic.d.ts +2 -2
  40. package/dist/describer/anthropic.js +5 -7
  41. package/dist/describer/describer.d.ts +2 -2
  42. package/dist/describer/describer.js +6 -8
  43. package/dist/describer/gemini.d.ts +2 -2
  44. package/dist/describer/gemini.js +5 -7
  45. package/dist/embedder/factory.d.ts +3 -1
  46. package/dist/embedder/factory.js +7 -1
  47. package/dist/embedder/health.js +4 -0
  48. package/dist/embedder/http.d.ts +1 -1
  49. package/dist/embedder/http.js +74 -43
  50. package/dist/eval/compare-merge.d.ts +10 -0
  51. package/dist/eval/compare-merge.js +537 -0
  52. package/dist/eval/compare-rankings.d.ts +10 -0
  53. package/dist/eval/compare-rankings.js +245 -0
  54. package/dist/eval/dump-descriptions.d.ts +7 -0
  55. package/dist/eval/dump-descriptions.js +58 -0
  56. package/dist/eval/fast-index.d.ts +8 -0
  57. package/dist/eval/fast-index.js +283 -0
  58. package/dist/eval/run-branch-compare.d.ts +7 -0
  59. package/dist/eval/run-branch-compare.js +220 -0
  60. package/dist/eval/run-token-test.js +1 -0
  61. package/dist/eval/test-kw.d.ts +1 -0
  62. package/dist/eval/test-kw.js +22 -0
  63. package/dist/eval/update-descriptions.d.ts +7 -0
  64. package/dist/eval/update-descriptions.js +84 -0
  65. package/dist/index.d.ts +1 -0
  66. package/dist/index.js +1 -0
  67. package/dist/indexer/embed-stage.js +2 -1
  68. package/dist/indexer/git-diff.js +21 -9
  69. package/dist/indexer/pipeline.d.ts +6 -0
  70. package/dist/indexer/pipeline.js +290 -37
  71. package/dist/indexer/watch.js +1 -3
  72. package/dist/indexer/worker.d.ts +15 -2
  73. package/dist/indexer/worker.js +25 -12
  74. package/dist/mcp/handlers.d.ts +9 -0
  75. package/dist/mcp/handlers.js +23 -6
  76. package/dist/mcp/server.js +2 -0
  77. package/dist/opencode/create-read-tool.d.ts +2 -0
  78. package/dist/opencode/create-read-tool.js +8 -2
  79. package/dist/opencode/read-fallback.d.ts +1 -5
  80. package/dist/opencode/read-fallback.js +1 -18
  81. package/dist/opencode/read-format.js +5 -3
  82. package/dist/opencode/tools.js +5 -7
  83. package/dist/plugin.js +141 -57
  84. package/dist/retriever/keyword-index.d.ts +3 -2
  85. package/dist/retriever/keyword-index.js +25 -1
  86. package/dist/retriever/retriever.d.ts +4 -1
  87. package/dist/retriever/retriever.js +34 -56
  88. package/dist/vectorstore/lancedb.d.ts +35 -4
  89. package/dist/vectorstore/lancedb.js +146 -23
  90. package/dist/vectorstore/memory.d.ts +6 -1
  91. package/dist/vectorstore/memory.js +58 -0
  92. package/dist/watcher.js +3 -0
  93. package/dist/web/api.js +10 -2
  94. package/dist/web/server.js +18 -3
  95. package/package.json +8 -9
  96. package/scripts/postinstall-setup.js +82 -0
  97. package/dist/api.js.map +0 -1
  98. package/dist/chunker/base.js.map +0 -1
  99. package/dist/chunker/bash.js.map +0 -1
  100. package/dist/chunker/c.js.map +0 -1
  101. package/dist/chunker/cpp.js.map +0 -1
  102. package/dist/chunker/csharp.js.map +0 -1
  103. package/dist/chunker/css.js.map +0 -1
  104. package/dist/chunker/doc.js.map +0 -1
  105. package/dist/chunker/dockerfile.js.map +0 -1
  106. package/dist/chunker/docx.js.map +0 -1
  107. package/dist/chunker/excel.js.map +0 -1
  108. package/dist/chunker/factory.js.map +0 -1
  109. package/dist/chunker/fallback.js.map +0 -1
  110. package/dist/chunker/go.js.map +0 -1
  111. package/dist/chunker/grammar.js.map +0 -1
  112. package/dist/chunker/html.js.map +0 -1
  113. package/dist/chunker/image.js.map +0 -1
  114. package/dist/chunker/ini.js.map +0 -1
  115. package/dist/chunker/java.js.map +0 -1
  116. package/dist/chunker/javascript.js.map +0 -1
  117. package/dist/chunker/json.js.map +0 -1
  118. package/dist/chunker/kotlin.js.map +0 -1
  119. package/dist/chunker/loader.js.map +0 -1
  120. package/dist/chunker/markdown.js.map +0 -1
  121. package/dist/chunker/pdf.js.map +0 -1
  122. package/dist/chunker/php.js.map +0 -1
  123. package/dist/chunker/powershell.js.map +0 -1
  124. package/dist/chunker/python.js.map +0 -1
  125. package/dist/chunker/razor.js.map +0 -1
  126. package/dist/chunker/ruby.js.map +0 -1
  127. package/dist/chunker/rust.js.map +0 -1
  128. package/dist/chunker/sln.js.map +0 -1
  129. package/dist/chunker/sql.js.map +0 -1
  130. package/dist/chunker/ssl.js.map +0 -1
  131. package/dist/chunker/swift.js.map +0 -1
  132. package/dist/chunker/tex.js.map +0 -1
  133. package/dist/chunker/toml.js.map +0 -1
  134. package/dist/chunker/typescript.js.map +0 -1
  135. package/dist/chunker/uuid.js.map +0 -1
  136. package/dist/chunker/xml.js.map +0 -1
  137. package/dist/chunker/yaml.js.map +0 -1
  138. package/dist/cli/commands/clear.js.map +0 -1
  139. package/dist/cli/commands/describe-image.js.map +0 -1
  140. package/dist/cli/commands/dump.js.map +0 -1
  141. package/dist/cli/commands/eval.js.map +0 -1
  142. package/dist/cli/commands/index-command.js.map +0 -1
  143. package/dist/cli/commands/index.js.map +0 -1
  144. package/dist/cli/commands/init-helpers.js.map +0 -1
  145. package/dist/cli/commands/init.js.map +0 -1
  146. package/dist/cli/commands/list.js.map +0 -1
  147. package/dist/cli/commands/mcp.js.map +0 -1
  148. package/dist/cli/commands/query.js.map +0 -1
  149. package/dist/cli/commands/show.js.map +0 -1
  150. package/dist/cli/commands/status.js.map +0 -1
  151. package/dist/cli/commands/ui.js.map +0 -1
  152. package/dist/cli/commands/update.d.ts +0 -17
  153. package/dist/cli/commands/update.js +0 -79
  154. package/dist/cli/commands/update.js.map +0 -1
  155. package/dist/cli/format.js.map +0 -1
  156. package/dist/cli/helpers.js.map +0 -1
  157. package/dist/cli/index.js.map +0 -1
  158. package/dist/cli/progress.d.ts +0 -42
  159. package/dist/cli/progress.js +0 -137
  160. package/dist/cli/progress.js.map +0 -1
  161. package/dist/cli/types.js.map +0 -1
  162. package/dist/cli.js.map +0 -1
  163. package/dist/content/doc.js.map +0 -1
  164. package/dist/content/docx.js.map +0 -1
  165. package/dist/content/excel.js.map +0 -1
  166. package/dist/content/image.js.map +0 -1
  167. package/dist/content/pdf.js.map +0 -1
  168. package/dist/content/reader.js.map +0 -1
  169. package/dist/content/types.js.map +0 -1
  170. package/dist/core/bootstrap.js.map +0 -1
  171. package/dist/core/config.js.map +0 -1
  172. package/dist/core/doc-progress.js.map +0 -1
  173. package/dist/core/fileLogger.js.map +0 -1
  174. package/dist/core/interfaces.js.map +0 -1
  175. package/dist/core/manifest.js.map +0 -1
  176. package/dist/core/provider-defaults.js.map +0 -1
  177. package/dist/core/rag-injection-flag.js.map +0 -1
  178. package/dist/core/resolve-api-key.js.map +0 -1
  179. package/dist/core/runtime-overrides.js.map +0 -1
  180. package/dist/describer/anthropic.js.map +0 -1
  181. package/dist/describer/describer.js.map +0 -1
  182. package/dist/describer/factory.js.map +0 -1
  183. package/dist/describer/gemini.js.map +0 -1
  184. package/dist/describer/shared.js.map +0 -1
  185. package/dist/embedder/cohere.js.map +0 -1
  186. package/dist/embedder/factory.js.map +0 -1
  187. package/dist/embedder/health.js.map +0 -1
  188. package/dist/embedder/http.js.map +0 -1
  189. package/dist/embedder/ollama.js.map +0 -1
  190. package/dist/embedder/openai.js.map +0 -1
  191. package/dist/eval/index.js.map +0 -1
  192. package/dist/eval/run-token-test.js.map +0 -1
  193. package/dist/eval/session-logger.js.map +0 -1
  194. package/dist/eval/storage.js.map +0 -1
  195. package/dist/eval/token-analysis.js.map +0 -1
  196. package/dist/eval/token-counter.js.map +0 -1
  197. package/dist/eval/types.js.map +0 -1
  198. package/dist/index.js.map +0 -1
  199. package/dist/indexer/description-stage.js.map +0 -1
  200. package/dist/indexer/embed-stage.js.map +0 -1
  201. package/dist/indexer/git-diff.js.map +0 -1
  202. package/dist/indexer/metadata.js.map +0 -1
  203. package/dist/indexer/pipeline.js.map +0 -1
  204. package/dist/indexer/stats.js.map +0 -1
  205. package/dist/indexer/watch.js.map +0 -1
  206. package/dist/indexer/worker.js.map +0 -1
  207. package/dist/indexer.js.map +0 -1
  208. package/dist/mcp/cli.js.map +0 -1
  209. package/dist/mcp/handlers.js.map +0 -1
  210. package/dist/mcp/server.js.map +0 -1
  211. package/dist/opencode/create-read-tool.js.map +0 -1
  212. package/dist/opencode/read-fallback.js.map +0 -1
  213. package/dist/opencode/read-format.js.map +0 -1
  214. package/dist/opencode/read-query.js.map +0 -1
  215. package/dist/opencode/tool-args.js.map +0 -1
  216. package/dist/opencode/tools.js.map +0 -1
  217. package/dist/plugin-entry.js.map +0 -1
  218. package/dist/plugin.js.map +0 -1
  219. package/dist/retriever/context-optimizer.js.map +0 -1
  220. package/dist/retriever/keyword-index.js.map +0 -1
  221. package/dist/retriever/retriever.js.map +0 -1
  222. package/dist/tui.js.map +0 -1
  223. package/dist/updater.d.ts +0 -45
  224. package/dist/updater.js +0 -175
  225. package/dist/updater.js.map +0 -1
  226. package/dist/vectorstore/factory.js.map +0 -1
  227. package/dist/vectorstore/lancedb.js.map +0 -1
  228. package/dist/vectorstore/memory.js.map +0 -1
  229. package/dist/watcher.js.map +0 -1
  230. package/dist/web/api.js.map +0 -1
  231. package/dist/web/server.js.map +0 -1
  232. package/dist/web/static.js.map +0 -1
@@ -4,7 +4,26 @@ import { optimizeContext, DEFAULT_CONTEXT_OPTIMIZATION } from "../retriever/cont
4
4
  import { Parser } from "web-tree-sitter";
5
5
  import { initParser, loadLanguage, walkTree } from "../chunker/grammar.js";
6
6
  import { readFileSync } from "node:fs";
7
- import { resolve, isAbsolute } from "node:path";
7
+ import { resolve, isAbsolute, relative } from "node:path";
8
+ /**
9
+ * Resolve a user-supplied file path against the worktree root, refusing to
10
+ * escape the worktree. Absolute paths are accepted only if they already reside
11
+ * under the worktree; relative paths are joined to the worktree and any `..`
12
+ * segments that would escape are rejected.
13
+ *
14
+ * @throws {Error} if the resolved path lies outside `worktree`.
15
+ */
16
+ export function resolveFilePath(filePath, worktree) {
17
+ const root = resolve(worktree);
18
+ const candidate = isAbsolute(filePath) ? resolve(filePath) : resolve(root, filePath);
19
+ const rel = relative(root, candidate);
20
+ if (rel === "")
21
+ return root;
22
+ if (rel.startsWith("..") || isAbsolute(rel)) {
23
+ throw new Error(`Path "${filePath}" escapes worktree root "${root}" — access denied.`);
24
+ }
25
+ return candidate;
26
+ }
8
27
  const SKELETON_CONFIGS = {
9
28
  ".ts": { grammarName: "typescript", nodeTypes: ["function_declaration", "method_definition", "class_declaration", "interface_declaration", "type_alias_declaration", "enum_declaration"] },
10
29
  ".tsx": { grammarName: "typescript", nodeTypes: ["function_declaration", "method_definition", "class_declaration", "interface_declaration", "type_alias_declaration", "enum_declaration", "arrow_function"] },
@@ -45,11 +64,6 @@ function getExtension(filePath) {
45
64
  const dot = filePath.lastIndexOf(".");
46
65
  return dot >= 0 ? filePath.slice(dot).toLowerCase() : "";
47
66
  }
48
- function resolveFilePath(filePath, worktree) {
49
- if (isAbsolute(filePath))
50
- return filePath;
51
- return resolve(worktree, filePath);
52
- }
53
67
  async function extractSkeleton(content, ext) {
54
68
  const config = SKELETON_CONFIGS[ext];
55
69
  if (!config) {
@@ -86,6 +100,8 @@ async function extractSkeleton(content, ext) {
86
100
  return [];
87
101
  const nodeTypes = new Set(config.nodeTypes);
88
102
  const astNodes = walkTree(tree.rootNode, nodeTypes, content, 15);
103
+ tree.delete();
104
+ parser.delete();
89
105
  return astNodes.map((node) => ({
90
106
  type: node.type,
91
107
  name: extractNodeName(node.text, node.type),
@@ -160,6 +176,7 @@ export async function handleSearchSemantic(params, embedder, store, cfg, keyword
160
176
  minScore: cfg.retrieval.minScore,
161
177
  keywordIndex,
162
178
  keywordWeight: cfg.retrieval.hybridSearch?.keywordWeight,
179
+ hybridEnabled: cfg.retrieval.hybridSearch?.enabled,
163
180
  queryPrefix: cfg.embedding.queryPrefix,
164
181
  };
165
182
  const rawResults = await retrieveFn_(query, embedder, store, retrieveOpts);
@@ -95,6 +95,8 @@ export async function createMcpServer(options) {
95
95
  server,
96
96
  close: async () => {
97
97
  await server.close();
98
+ await ctx.store.close();
99
+ ctx.keywordIndex.close();
98
100
  },
99
101
  };
100
102
  }
@@ -24,6 +24,8 @@ export interface RagReadToolOptions {
24
24
  }>;
25
25
  /** Optional keyword index for hybrid search. */
26
26
  keywordIndex?: KeywordIndex;
27
+ /** Maximum number of sessions to retain in the retrieval cache. */
28
+ maxSessionCacheSize?: number;
27
29
  }
28
30
  /**
29
31
  * Create the RAG-backed read tool for OpenCode plugin registration.
@@ -63,7 +63,13 @@ export function createRagReadTool(options) {
63
63
  }
64
64
  else {
65
65
  const retrievalQuery = buildSessionQuery(messageText, resolvedPath, normalized);
66
- rawResults = await retrieve(retrievalQuery, embedder, store, { topK: retrievalTopK, keywordIndex, queryPrefix: config.embedding.queryPrefix });
66
+ rawResults = await retrieve(retrievalQuery, embedder, store, { topK: retrievalTopK, keywordIndex, hybridEnabled: config.retrieval.hybridSearch?.enabled, queryPrefix: config.embedding.queryPrefix });
67
+ const maxSize = options.maxSessionCacheSize ?? 50;
68
+ if (!sessionRetrievalCache.has(sessionID) && sessionRetrievalCache.size >= maxSize) {
69
+ const oldest = sessionRetrievalCache.keys().next().value;
70
+ if (oldest !== undefined)
71
+ sessionRetrievalCache.delete(oldest);
72
+ }
67
73
  sessionRetrievalCache.set(sessionID, { messageText, rawResults });
68
74
  }
69
75
  }
@@ -74,7 +80,7 @@ export function createRagReadTool(options) {
74
80
  startLine: normalized.startLine,
75
81
  endLine: normalized.endLine,
76
82
  });
77
- rawResults = await retrieve(retrievalQuery, embedder, store, { topK: retrievalTopK, keywordIndex, queryPrefix: config.embedding.queryPrefix });
83
+ rawResults = await retrieve(retrievalQuery, embedder, store, { topK: retrievalTopK, keywordIndex, hybridEnabled: config.retrieval.hybridSearch?.enabled, queryPrefix: config.embedding.queryPrefix });
78
84
  }
79
85
  // Collect related files from raw results (before filtering)
80
86
  relatedFiles = collectRelatedFiles(rawResults, resolvedPath, readRelatedFilesMax);
@@ -1,12 +1,8 @@
1
1
  /**
2
2
  * @fileoverview Provides fallback error messages and no-results behavior dispatch for the read tool.
3
3
  */
4
- import type { ReadNoResultsBehavior } from "../core/config.js";
5
4
  /**
6
5
  * Error message wrapper for retrieval failures.
7
6
  */
8
7
  export declare function retrievalErrorMessage(shortError: string): string;
9
- /**
10
- * Dispatch to the correct fallback message based on behavior config.
11
- */
12
- export declare function getNoResultsMessage(behavior: ReadNoResultsBehavior, filePath?: string): string;
8
+ /** No results fallback messages are handled inline in create-read-tool.ts. */
@@ -12,22 +12,5 @@ export function retrievalErrorMessage(shortError) {
12
12
  shortError,
13
13
  ].join("\n");
14
14
  }
15
- /**
16
- * Dispatch to the correct fallback message based on behavior config.
17
- */
18
- export function getNoResultsMessage(behavior, filePath) {
19
- switch (behavior) {
20
- case "error":
21
- throw new Error("OpenCodeRAG read: no indexed chunks found." +
22
- (filePath ? ` File: ${filePath}` : ""));
23
- case "empty":
24
- return "No indexed chunks found.";
25
- case "hint":
26
- default:
27
- if (filePath) {
28
- return `No indexed chunks found for ${filePath}.`;
29
- }
30
- return "No relevant chunks found.";
31
- }
32
- }
15
+ /** No results fallback messages are handled inline in create-read-tool.ts. */
33
16
  //# sourceMappingURL=read-fallback.js.map
@@ -124,9 +124,11 @@ function formatChunk(index, result) {
124
124
  lines.push(`Score: ${score.toFixed(4)}`);
125
125
  lines.push("");
126
126
  lines.push("```" + language);
127
- lines.push(chunk.content);
128
- if (!chunk.content.endsWith("\n")) {
129
- // Ensure code block closes on its own line
127
+ if (chunk.content.endsWith("\n")) {
128
+ lines.push(chunk.content.slice(0, -1));
129
+ }
130
+ else {
131
+ lines.push(chunk.content);
130
132
  }
131
133
  lines.push("```");
132
134
  return lines.join("\n");
@@ -19,7 +19,7 @@ import { retrieve } from "../retriever/retriever.js";
19
19
  import { Parser } from "web-tree-sitter";
20
20
  import { initParser, loadLanguage, walkTree } from "../chunker/grammar.js";
21
21
  import { readFileSync } from "node:fs";
22
- import { resolve, isAbsolute } from "node:path";
22
+ import { resolveWorkspacePath } from "./tool-args.js";
23
23
  const SKELETON_CONFIGS = {
24
24
  ".ts": { grammarName: "typescript", nodeTypes: ["function_declaration", "method_definition", "class_declaration", "interface_declaration", "type_alias_declaration", "enum_declaration"] },
25
25
  ".tsx": { grammarName: "typescript", nodeTypes: ["function_declaration", "method_definition", "class_declaration", "interface_declaration", "type_alias_declaration", "enum_declaration", "arrow_function"] },
@@ -68,9 +68,7 @@ function getExtension(filePath) {
68
68
  * Resolve a file path relative to the workspace root.
69
69
  */
70
70
  function resolveFilePath(filePath, worktree) {
71
- if (isAbsolute(filePath))
72
- return filePath;
73
- return resolve(worktree, filePath);
71
+ return resolveWorkspacePath(worktree, filePath);
74
72
  }
75
73
  /**
76
74
  * Extract structural outline from source code using tree-sitter.
@@ -116,6 +114,8 @@ async function extractSkeleton(content, ext) {
116
114
  return [];
117
115
  const nodeTypes = new Set(config.nodeTypes);
118
116
  const astNodes = walkTree(tree.rootNode, nodeTypes, content, 15);
117
+ tree.delete();
118
+ parser.delete();
119
119
  return astNodes.map((node) => ({
120
120
  type: node.type,
121
121
  name: extractNodeName(node.text, node.type),
@@ -262,9 +262,7 @@ export function createDescribeImageTool(options) {
262
262
  try {
263
263
  const { existsSync, readFileSync } = await import("node:fs");
264
264
  const path = await import("node:path");
265
- const resolvedPath = isAbsolute(args.filePath)
266
- ? args.filePath
267
- : resolve(worktree, args.filePath);
265
+ const resolvedPath = resolveWorkspacePath(worktree, args.filePath);
268
266
  if (!existsSync(resolvedPath)) {
269
267
  return {
270
268
  title: "Describe image",
package/dist/plugin.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * automatic context injection and documentation mode.
5
5
  */
6
6
  import { tool } from "@opencode-ai/plugin/tool";
7
- import { loadConfig, findConfigFile, DEFAULT_CONFIG, resolveLogConfig } from "./core/config.js";
7
+ import { loadConfig, findConfigFile, DEFAULT_CONFIG, resolveLogConfig, persistProbedDimension } from "./core/config.js";
8
8
  import { createEmbedder } from "./embedder/factory.js";
9
9
  import { createDescriptionProvider } from "./describer/factory.js";
10
10
  import { createVectorStore } from "./vectorstore/factory.js";
@@ -22,12 +22,15 @@ 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 "./updater.js";
25
+ import { checkForUpdate } from "./core/version-check.js";
26
+ import { destroyAllPooledConnections } from "./embedder/http.js";
26
27
  import { existsSync, readFileSync, unlinkSync } from "node:fs";
27
28
  import path from "node:path";
28
29
  import { fileURLToPath } from "node:url";
29
30
  import { spawn, execSync } from "node:child_process";
30
31
  import { tmpdir } from "node:os";
32
+ /** Maximum entries per session map before oldest entries are evicted. */
33
+ const MAX_SESSION_MAP_SIZE = 50;
31
34
  /** Cache of loaded RAG configurations keyed by workspace directory. */
32
35
  const configCache = new Map();
33
36
  /** Active background indexer instances keyed by workspace directory. */
@@ -36,6 +39,52 @@ const backgroundIndexers = new Map();
36
39
  const mcpServers = new Map();
37
40
  /** Pending update notifications keyed by workspace directory. */
38
41
  const pendingUpdateInfo = new Map();
42
+ /** Guard flag to prevent re-entrant shutdown. */
43
+ let shutdownRegistered = false;
44
+ /** Close all active background indexers and MCP servers, then destroy idle sockets. */
45
+ async function shutdownPluginResources() {
46
+ for (const [dir, indexer] of backgroundIndexers) {
47
+ try {
48
+ await indexer.close();
49
+ }
50
+ catch { /* best-effort */ }
51
+ backgroundIndexers.delete(dir);
52
+ }
53
+ for (const [dir, server] of mcpServers) {
54
+ try {
55
+ await server.close();
56
+ }
57
+ catch { /* best-effort */ }
58
+ mcpServers.delete(dir);
59
+ }
60
+ configCache.clear();
61
+ pendingUpdateInfo.clear();
62
+ destroyAllPooledConnections();
63
+ }
64
+ /** Register a process.beforeExit handler so that resources are cleaned up
65
+ * when the OpenCode process exits (rather than only on plugin reload). */
66
+ function registerShutdownHandler() {
67
+ if (shutdownRegistered)
68
+ return;
69
+ shutdownRegistered = true;
70
+ process.once("beforeExit", () => {
71
+ void shutdownPluginResources();
72
+ });
73
+ }
74
+ registerShutdownHandler();
75
+ /** Set a bounded Map entry, evicting the oldest key if the map exceeds maximum size. */
76
+ function boundedSet(map, key, value, maxSize) {
77
+ if (map.has(key)) {
78
+ map.set(key, value);
79
+ return;
80
+ }
81
+ if (map.size >= maxSize) {
82
+ const oldest = map.keys().next().value;
83
+ if (oldest !== undefined)
84
+ map.delete(oldest);
85
+ }
86
+ map.set(key, value);
87
+ }
39
88
  /** Name of the semantic search tool as exposed to the LLM. */
40
89
  const CONTEXT_TOOL_NAME = "search_semantic";
41
90
  /** Marker string injected into context output to identify RAG-sourced content. */
@@ -230,10 +279,10 @@ function buildRetrievalQuery(hints) {
230
279
  * Perform a retrieval query against the vector store with the given parameters.
231
280
  * Returns an empty array for blank queries.
232
281
  */
233
- async function retrieveContext(query, embedder, store, topK, retrieveFn = retrieve, minScore = 0, keywordIndex, keywordWeight, queryPrefix, explain = false) {
282
+ async function retrieveContext(query, embedder, store, topK, retrieveFn = retrieve, minScore = 0, keywordIndex, keywordWeight, queryPrefix, explain = false, hybridEnabled) {
234
283
  if (query.trim().length === 0)
235
284
  return [];
236
- return retrieveFn(query, embedder, store, { topK, minScore, keywordIndex, keywordWeight, queryPrefix, explain });
285
+ return retrieveFn(query, embedder, store, { topK, minScore, keywordIndex, keywordWeight, hybridEnabled, queryPrefix, explain });
237
286
  }
238
287
  /**
239
288
  * Load results from one or two queries (primary + optional extra), optimize
@@ -243,9 +292,10 @@ async function retrieveContext(query, embedder, store, topK, retrieveFn = retrie
243
292
  async function loadRetrievedResults(query, embedder, store, cfg, retrieveFn = retrieve, topK = cfg.retrieval.topK, extraQuery, keywordIndex, queryPrefix, explain = false) {
244
293
  const minScore = cfg.retrieval.minScore;
245
294
  const kw = cfg.retrieval.hybridSearch?.keywordWeight;
246
- const primaryResults = await retrieveContext(query, embedder, store, topK, retrieveFn, minScore, keywordIndex, kw, queryPrefix, explain);
295
+ const hybridEnabled = cfg.retrieval.hybridSearch?.enabled;
296
+ const primaryResults = await retrieveContext(query, embedder, store, topK, retrieveFn, minScore, keywordIndex, kw, queryPrefix, explain, hybridEnabled);
247
297
  const extraResults = extraQuery
248
- ? await retrieveContext(extraQuery, embedder, store, topK, retrieveFn, minScore, keywordIndex, kw, queryPrefix, explain)
298
+ ? await retrieveContext(extraQuery, embedder, store, topK, retrieveFn, minScore, keywordIndex, kw, queryPrefix, explain, hybridEnabled)
249
299
  : [];
250
300
  const optCfg = cfg.retrieval.contextOptimization ?? DEFAULT_CONTEXT_OPTIMIZATION;
251
301
  return optimizeContext([...primaryResults, ...extraResults], { topK, config: optCfg })
@@ -355,12 +405,20 @@ export function createRagHooks(options) {
355
405
  let cachedOverrides = loadRuntimeOverrides(options.storePath);
356
406
  let overridesLastCheck = 0;
357
407
  const OVERRIDES_TTL_MS = Number(process.env.OPENCODE_RAG_OVERRIDES_TTL_MS) || 3600000;
408
+ let lastEffectiveCfg = null;
409
+ let lastEffectiveCfgTime = 0;
358
410
  function getEffectiveCfg() {
359
411
  if (Date.now() - overridesLastCheck > OVERRIDES_TTL_MS) {
360
412
  cachedOverrides = loadRuntimeOverrides(options.storePath);
361
413
  overridesLastCheck = Date.now();
414
+ lastEffectiveCfg = null;
415
+ }
416
+ if (lastEffectiveCfg && Date.now() - lastEffectiveCfgTime < 1000) {
417
+ return lastEffectiveCfg;
362
418
  }
363
- return applyRuntimeOverrides(options.cfg, cachedOverrides);
419
+ lastEffectiveCfg = applyRuntimeOverrides(options.cfg, cachedOverrides);
420
+ lastEffectiveCfgTime = Date.now();
421
+ return lastEffectiveCfg;
364
422
  }
365
423
  // Session-level caches for lazy retrieval
366
424
  const sessionLastMessage = new Map();
@@ -538,6 +596,7 @@ export function createRagHooks(options) {
538
596
  sessionLastMessage,
539
597
  sessionRetrievalCache,
540
598
  keywordIndex,
599
+ maxSessionCacheSize: MAX_SESSION_MAP_SIZE,
541
600
  });
542
601
  tools["read"] = readTool;
543
602
  appendDebugLog(options.logFilePath, {
@@ -562,8 +621,8 @@ export function createRagHooks(options) {
562
621
  if (msgInfo?.role === "assistant" &&
563
622
  typeof msgInfo.sessionID === "string" &&
564
623
  typeof msgInfo.id === "string") {
565
- sessionAssistantMessageId.set(msgInfo.sessionID, msgInfo.id);
566
- sessionAssistantText.set(msgInfo.sessionID, "");
624
+ boundedSet(sessionAssistantMessageId, msgInfo.sessionID, msgInfo.id, MAX_SESSION_MAP_SIZE);
625
+ boundedSet(sessionAssistantText, msgInfo.sessionID, "", MAX_SESSION_MAP_SIZE);
567
626
  }
568
627
  }
569
628
  else if (event.type === "message.part.updated") {
@@ -577,7 +636,7 @@ export function createRagHooks(options) {
577
636
  const assistantMsgId = sessionAssistantMessageId.get(part.sessionID);
578
637
  if (assistantMsgId && part.messageID === assistantMsgId) {
579
638
  const existing = sessionAssistantText.get(part.sessionID) ?? "";
580
- sessionAssistantText.set(part.sessionID, existing + delta);
639
+ boundedSet(sessionAssistantText, part.sessionID, existing + delta, MAX_SESSION_MAP_SIZE);
581
640
  }
582
641
  }
583
642
  }
@@ -592,42 +651,45 @@ export function createRagHooks(options) {
592
651
  scope: "experimental.chat.system.transform",
593
652
  message: "system guidance injected",
594
653
  });
595
- const guidance = [
596
- "MANDATORY: OpenCodeRAG tools MUST be used before any code task:",
597
- "- `search_semantic(query)`: retrieve relevant code chunks. Call BEFORE planning, editing, or answering. Accepts `pathHints` and `languageHints`.",
598
- "- `get_file_skeleton(filePath)`: structural overview of a file. Call BEFORE reading any file.",
599
- "- `find_usages(symbolName)`: find all references. Call BEFORE editing any function, class, or variable.",
600
- "- `describe_image(filePath)`: describe an image file using a vision model. Call when user refers to a screenshot, diagram, or image.",
601
- "",
602
- "Decision tree ALWAYS follow this order:",
603
- "1. User mentions code behavior/architecture → `search_semantic(query)`",
604
- "2. User mentions a file path → `get_file_skeleton(filePath)` THEN `read` on specific lines",
605
- "3. User mentions a function/class/variable to edit → `find_usages(symbolName)` THEN `search_semantic` THEN `edit`",
606
- "4. User asks a code question → `search_semantic` to gather context before answering",
607
- "5. User asks about an image or visual asset → `describe_image(filePath)` to retrieve its generated description, then optionally `search_semantic` for related code",
608
- "",
609
- "Proactive triggers you MUST call these tools when:",
610
- "- User asks about code behavior, architecture, or implementation details",
611
- "- User asks to edit, refactor, or fix code — call `find_usages` first",
612
- "- User references files or functions you haven't read yet",
613
- "- User says \"find\", \"search\", \"look up\", \"where is\", \"how does\"",
614
- "- User refers to an image, screenshot, diagram, or visual asset",
615
- "- Before answering ANY code-related question, retrieve context first",
616
- "- Before reading ANY file, call `get_file_skeleton` to orient first",
617
- "",
618
- "Anti-patterns NEVER do these:",
619
- "- Reading full files without calling `get_file_skeleton` first (wastes tokens)",
620
- "- Editing a function without calling `find_usages` first (breaks call sites)",
621
- "- Answering code questions without calling `search_semantic` first (you guess at behavior)",
622
- "- Using `grep`/`glob` when `search_semantic` would find the answer faster",
623
- "- Treating image files as text — use `describe_image` instead of reading raw bytes",
624
- ];
625
- output.system.unshift(guidance.join("\n"));
654
+ const cfg = getEffectiveCfg();
655
+ if (cfg.openCode.injectSystemPrompt !== false) {
656
+ const guidance = [
657
+ "MANDATORY: OpenCodeRAG tools MUST be used before any code task:",
658
+ "- `search_semantic(query)`: retrieve relevant code chunks. Call BEFORE planning, editing, or answering. Accepts `pathHints` and `languageHints`.",
659
+ "- `get_file_skeleton(filePath)`: structural overview of a file. Call BEFORE reading any file.",
660
+ "- `find_usages(symbolName)`: find all references. Call BEFORE editing any function, class, or variable.",
661
+ "- `describe_image(filePath)`: describe an image file using a vision model. Call when user refers to a screenshot, diagram, or image.",
662
+ "",
663
+ "Decision tree ALWAYS follow this order:",
664
+ "1. User mentions code behavior/architecture → `search_semantic(query)`",
665
+ "2. User mentions a file path → `get_file_skeleton(filePath)` THEN `read` on specific lines",
666
+ "3. User mentions a function/class/variable to edit → `find_usages(symbolName)` THEN `search_semantic` THEN `edit`",
667
+ "4. User asks a code question → `search_semantic` to gather context before answering",
668
+ "5. User asks about an image or visual asset → `describe_image(filePath)` to retrieve its generated description, then optionally `search_semantic` for related code",
669
+ "",
670
+ "Proactive triggers you MUST call these tools when:",
671
+ "- User asks about code behavior, architecture, or implementation details",
672
+ "- User asks to edit, refactor, or fix code call `find_usages` first",
673
+ "- User references files or functions you haven't read yet",
674
+ "- User says \"find\", \"search\", \"look up\", \"where is\", \"how does\"",
675
+ "- User refers to an image, screenshot, diagram, or visual asset",
676
+ "- Before answering ANY code-related question, retrieve context first",
677
+ "- Before reading ANY file, call `get_file_skeleton` to orient first",
678
+ "",
679
+ "Anti-patterns NEVER do these:",
680
+ "- Reading full files without calling `get_file_skeleton` first (wastes tokens)",
681
+ "- Editing a function without calling `find_usages` first (breaks call sites)",
682
+ "- Answering code questions without calling `search_semantic` first (you guess at behavior)",
683
+ "- Using `grep`/`glob` when `search_semantic` would find the answer faster",
684
+ "- Treating image files as text — use `describe_image` instead of reading raw bytes",
685
+ ];
686
+ output.system.unshift(guidance.join("\n"));
687
+ }
626
688
  // Inject update notification if available
627
689
  const updateInfo = pendingUpdateInfo.get(options.worktree);
628
690
  if (updateInfo) {
629
691
  output.system.unshift(`OpenCodeRAG update available: ${updateInfo.currentVersion} → ${updateInfo.latestVersion}. ` +
630
- `Run \`opencode-rag update\` to install.`);
692
+ `Run \`npm update -g opencode-rag-plugin && opencode-rag setup\` to install.`);
631
693
  }
632
694
  // Inject documentation mode system prompt if enabled
633
695
  const docMode = getEffectiveCfg().documentationMode;
@@ -650,7 +712,7 @@ export function createRagHooks(options) {
650
712
  });
651
713
  if (text.length === 0)
652
714
  return;
653
- sessionLastMessage.set(input.sessionID, text);
715
+ boundedSet(sessionLastMessage, input.sessionID, text, MAX_SESSION_MAP_SIZE);
654
716
  // Handle /doc slash command
655
717
  if (text.startsWith("/doc")) {
656
718
  const docMode = getEffectiveCfg().documentationMode;
@@ -1025,29 +1087,51 @@ export const ragPlugin = async (input, _options) => {
1025
1087
  }
1026
1088
  mcpServers.delete(input.directory);
1027
1089
  }
1090
+ // Clean up stale config cache and pending update info for this directory
1091
+ configCache.delete(input.directory);
1092
+ pendingUpdateInfo.delete(input.directory);
1093
+ // Clean up idle HTTP sockets from previous provider connections
1094
+ destroyAllPooledConnections();
1028
1095
  appendDebugLog(logFilePath, {
1029
1096
  scope: "plugin",
1030
1097
  message: `OpenCode plugin enabled for ${input.directory}`,
1031
1098
  }, logLevel);
1032
- // Probe vector dimension and create store with correct dimension
1099
+ // Use cached dimension from config if available (avoids blocking startup with an API call)
1100
+ // If not set, probe the embedding provider once and persist the result.
1033
1101
  const embedder = createEmbedder(effectiveCfg);
1034
- let vectorDimension = 384;
1035
- try {
1036
- const probe = await embedder.embed(["dimension-probe"], "query");
1037
- if (probe && probe[0] && probe[0].length > 0 && typeof probe[0][0] === "number") {
1038
- vectorDimension = probe[0].length;
1039
- }
1102
+ let vectorDimension = effectiveCfg.embedding.vectorDimension;
1103
+ if (vectorDimension && vectorDimension > 0) {
1040
1104
  appendDebugLog(logFilePath, {
1041
1105
  scope: "plugin",
1042
- message: `Vector dimension: ${vectorDimension}`,
1106
+ message: `Vector dimension: ${vectorDimension} (cached in config)`,
1043
1107
  }, logLevel);
1044
1108
  }
1045
- catch (err) {
1046
- appendDebugLog(logFilePath, {
1047
- scope: "plugin",
1048
- message: `Dimension probe failed, falling back to ${vectorDimension}`,
1049
- error: err,
1050
- }, logLevel);
1109
+ else {
1110
+ vectorDimension = 384;
1111
+ try {
1112
+ const probe = await embedder.embed(["dimension-probe"], "query");
1113
+ if (probe && probe[0] && probe[0].length > 0 && typeof probe[0][0] === "number") {
1114
+ vectorDimension = probe[0].length;
1115
+ const configPath = findConfigFile(input.directory);
1116
+ if (configPath) {
1117
+ try {
1118
+ persistProbedDimension(configPath, vectorDimension);
1119
+ }
1120
+ catch { /* best-effort */ }
1121
+ }
1122
+ }
1123
+ appendDebugLog(logFilePath, {
1124
+ scope: "plugin",
1125
+ message: `Vector dimension: ${vectorDimension}`,
1126
+ }, logLevel);
1127
+ }
1128
+ catch (err) {
1129
+ appendDebugLog(logFilePath, {
1130
+ scope: "plugin",
1131
+ message: `Dimension probe failed, falling back to ${vectorDimension}`,
1132
+ error: err,
1133
+ }, logLevel);
1134
+ }
1051
1135
  }
1052
1136
  const store = createVectorStore(effectiveCfg, storePath, vectorDimension);
1053
1137
  // Load or create keyword index for hybrid search
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @fileoverview In-memory inverted keyword index with stemming, tokenization, and optional serialization.
3
3
  */
4
- import type { Chunk, SearchResult } from "../core/interfaces.js";
4
+ import type { Chunk, SearchResult, MetadataFilter } from "../core/interfaces.js";
5
5
  /**
6
6
  * Tokenize text into normalized tokens including stems, camelCase parts, and snake_case parts.
7
7
  *
@@ -22,7 +22,8 @@ export declare class KeywordIndex {
22
22
  addChunks(chunks: Chunk[]): void;
23
23
  getMatchedTerms(query: string, chunkId: string): string[];
24
24
  removeByFilePath(filePath: string): void;
25
- search(query: string, topK: number): SearchResult[];
25
+ search(query: string, topK: number, filter?: MetadataFilter): SearchResult[];
26
+ close(): void;
26
27
  clear(): void;
27
28
  count(): number;
28
29
  save(storePath?: string): Promise<void>;
@@ -124,7 +124,7 @@ export class KeywordIndex {
124
124
  }
125
125
  }
126
126
  }
127
- search(query, topK) {
127
+ search(query, topK, filter) {
128
128
  if (this.chunkMap.size === 0)
129
129
  return [];
130
130
  const queryTokens = tokenize(query);
@@ -144,6 +144,7 @@ export class KeywordIndex {
144
144
  }
145
145
  }
146
146
  const sorted = [...scores.entries()]
147
+ .filter(([id]) => matchesFilter(this.chunkMap.get(id), filter))
147
148
  .sort((a, b) => b[1] - a[1])
148
149
  .slice(0, topK);
149
150
  return sorted.map(([chunkId, score]) => {
@@ -151,6 +152,10 @@ export class KeywordIndex {
151
152
  return { chunk, score };
152
153
  });
153
154
  }
155
+ close() {
156
+ this.invertedIndex.clear();
157
+ this.chunkMap.clear();
158
+ }
154
159
  clear() {
155
160
  this.invertedIndex.clear();
156
161
  this.chunkMap.clear();
@@ -232,4 +237,23 @@ export class KeywordIndex {
232
237
  await writeFile(targetPath, JSON.stringify({ version: INDEX_VERSION, tokens: [], chunkMap: {} }), "utf-8");
233
238
  }
234
239
  }
240
+ function globMatch(pattern, filePath) {
241
+ const GLOBSTAR = "\x00GS\x00";
242
+ let re = pattern.replace(/\*\*/g, GLOBSTAR);
243
+ re = re.replace(/([.+^${}()|[\]\\])/g, "\\$1");
244
+ re = re.replace(new RegExp(GLOBSTAR, "g"), ".*");
245
+ re = re.replace(/\*/g, "[^/]*");
246
+ re = re.replace(/\?/g, ".");
247
+ return new RegExp("^" + re + "$").test(filePath);
248
+ }
249
+ function matchesFilter(chunk, filter) {
250
+ if (!filter)
251
+ return true;
252
+ if (filter.languages?.length && !filter.languages.includes(chunk.metadata.language))
253
+ return false;
254
+ if (filter.pathPatterns?.length) {
255
+ return filter.pathPatterns.some((p) => globMatch(p, chunk.metadata.filePath));
256
+ }
257
+ return true;
258
+ }
235
259
  //# sourceMappingURL=keyword-index.js.map
@@ -1,15 +1,18 @@
1
1
  /**
2
2
  * @fileoverview Performs hybrid vector-keyword retrieval with configurable scoring and explanation.
3
3
  */
4
- import type { EmbeddingProvider, KeywordIndex, VectorStore, SearchResult } from "../core/interfaces.js";
4
+ import type { EmbeddingProvider, KeywordIndex, VectorStore, SearchResult, MetadataFilter } from "../core/interfaces.js";
5
5
  /** Options controlling the retrieval behavior. */
6
6
  export interface RetrieveOptions {
7
7
  topK?: number;
8
8
  minScore?: number;
9
9
  keywordIndex?: KeywordIndex;
10
10
  keywordWeight?: number;
11
+ /** Whether hybrid search is enabled. When false, keyword index is ignored. */
12
+ hybridEnabled?: boolean;
11
13
  queryPrefix?: string;
12
14
  explain?: boolean;
15
+ filter?: MetadataFilter;
13
16
  }
14
17
  /**
15
18
  * Perform hybrid vector-keyword retrieval with configurable scoring.