opencode-rag-plugin 1.19.4 → 1.19.8

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 (87) hide show
  1. package/dist/chunker/base.js +19 -5
  2. package/dist/chunker/factory.js +27 -9
  3. package/dist/chunker/grammar.d.ts +18 -1
  4. package/dist/chunker/grammar.js +48 -10
  5. package/dist/chunker/image.js +5 -0
  6. package/dist/chunker/pdf.js +30 -14
  7. package/dist/cli/commands/backend-detect.d.ts +61 -0
  8. package/dist/cli/commands/backend-detect.js +119 -0
  9. package/dist/cli/commands/index-command.js +11 -0
  10. package/dist/cli/commands/init-helpers.d.ts +4 -1
  11. package/dist/cli/commands/init-helpers.js +21 -4
  12. package/dist/cli/commands/init.js +61 -24
  13. package/dist/cli/commands/quirk.js +9 -3
  14. package/dist/cli/commands/setup.js +12 -3
  15. package/dist/cli/commands/status.js +14 -5
  16. package/dist/cli/commands/ui.js +23 -9
  17. package/dist/cli/commands/update.js +4 -5
  18. package/dist/cli/format.d.ts +5 -2
  19. package/dist/cli/format.js +14 -5
  20. package/dist/content/image.js +33 -11
  21. package/dist/content/reader.js +74 -13
  22. package/dist/core/bootstrap.js +10 -3
  23. package/dist/core/config.d.ts +27 -1
  24. package/dist/core/config.js +41 -2
  25. package/dist/core/desc-cache.d.ts +8 -2
  26. package/dist/core/desc-cache.js +10 -3
  27. package/dist/core/doc-progress.js +5 -2
  28. package/dist/core/interfaces.d.ts +32 -4
  29. package/dist/core/manifest.js +1 -1
  30. package/dist/core/provider-defaults.d.ts +2 -0
  31. package/dist/core/provider-defaults.js +19 -4
  32. package/dist/core/runtime-overrides.d.ts +0 -6
  33. package/dist/core/version-check.d.ts +5 -0
  34. package/dist/core/version-check.js +8 -2
  35. package/dist/describer/anthropic.d.ts +2 -2
  36. package/dist/describer/anthropic.js +19 -5
  37. package/dist/describer/describer.d.ts +20 -0
  38. package/dist/describer/describer.js +132 -16
  39. package/dist/describer/gemini.js +25 -10
  40. package/dist/describer/shared.d.ts +28 -0
  41. package/dist/describer/shared.js +60 -0
  42. package/dist/embedder/factory.d.ts +5 -3
  43. package/dist/embedder/factory.js +42 -9
  44. package/dist/embedder/health.js +19 -19
  45. package/dist/embedder/http.d.ts +14 -1
  46. package/dist/embedder/http.js +60 -6
  47. package/dist/embedder/ollama.d.ts +3 -1
  48. package/dist/embedder/ollama.js +12 -2
  49. package/dist/eval/session-logger.js +7 -0
  50. package/dist/eval/storage.js +8 -0
  51. package/dist/indexer/git-diff.d.ts +1 -1
  52. package/dist/indexer/git-diff.js +5 -1
  53. package/dist/indexer/pipeline.js +511 -346
  54. package/dist/indexer/stats.d.ts +2 -0
  55. package/dist/indexer/stats.js +1 -0
  56. package/dist/indexer/watch.js +8 -1
  57. package/dist/indexer/worker.js +21 -0
  58. package/dist/mcp/cli.js +4 -0
  59. package/dist/mcp/handlers.js +16 -5
  60. package/dist/mcp/server.js +3 -0
  61. package/dist/opencode/create-read-tool.js +14 -3
  62. package/dist/opencode/tool-args.js +23 -1
  63. package/dist/plugin.js +66 -152
  64. package/dist/quirks/auto-capture.js +5 -0
  65. package/dist/quirks/quirk-store.d.ts +1 -1
  66. package/dist/quirks/quirk-store.js +56 -17
  67. package/dist/retriever/context-optimizer.js +18 -4
  68. package/dist/retriever/keyword-index.d.ts +2 -0
  69. package/dist/retriever/keyword-index.js +38 -4
  70. package/dist/retriever/retriever.js +6 -1
  71. package/dist/tui.js +41 -4
  72. package/dist/vectorstore/lancedb.d.ts +104 -8
  73. package/dist/vectorstore/lancedb.js +345 -71
  74. package/dist/vectorstore/memory.d.ts +8 -3
  75. package/dist/vectorstore/memory.js +27 -4
  76. package/dist/watcher.d.ts +8 -0
  77. package/dist/watcher.js +237 -85
  78. package/dist/web/api.d.ts +5 -1
  79. package/dist/web/api.js +195 -69
  80. package/dist/web/server.d.ts +2 -0
  81. package/dist/web/server.js +66 -28
  82. package/dist/web/static.d.ts +5 -2
  83. package/dist/web/static.js +9 -5
  84. package/dist/web/ui/assets/index-BDPYdtA1.js +3 -0
  85. package/dist/web/ui/index.html +1 -1
  86. package/package.json +1 -1
  87. package/dist/web/ui/assets/index-CJBvt6e0.js +0 -3
@@ -67,6 +67,48 @@ function destroyAllPooledConnections() {
67
67
  connectionPool.clear();
68
68
  }
69
69
  export { destroyAllPooledConnections };
70
+ /**
71
+ * Fetch a URL through the configured proxy, applying the proxy to
72
+ * HTTP(S)_PROXY env vars for the duration of the request (serialized).
73
+ *
74
+ * Used by health checks and other one-off requests that do not go
75
+ * through {@link postJson}.
76
+ */
77
+ export async function fetchWithProxy(urlString, init, proxy) {
78
+ const authHeader = buildProxyAuthHeader(proxy);
79
+ const envOverride = applyProxyEnv(proxy);
80
+ const [savedHttpProxy, savedHttpsProxy] = [process.env.HTTP_PROXY, process.env.HTTPS_PROXY];
81
+ return proxyRequestQueue.then(async () => {
82
+ try {
83
+ if (envOverride) {
84
+ process.env.HTTP_PROXY = envOverride.httpProxy;
85
+ process.env.HTTPS_PROXY = envOverride.httpsProxy;
86
+ }
87
+ const headers = { ...init.headers };
88
+ if (authHeader) {
89
+ headers["Proxy-Authorization"] = authHeader;
90
+ }
91
+ return await fetch(urlString, {
92
+ method: init.method ?? "GET",
93
+ headers,
94
+ body: init.body,
95
+ signal: init.signal,
96
+ });
97
+ }
98
+ finally {
99
+ if (envOverride) {
100
+ if (savedHttpProxy === undefined)
101
+ delete process.env.HTTP_PROXY;
102
+ else
103
+ process.env.HTTP_PROXY = savedHttpProxy;
104
+ if (savedHttpsProxy === undefined)
105
+ delete process.env.HTTPS_PROXY;
106
+ else
107
+ process.env.HTTPS_PROXY = savedHttpsProxy;
108
+ }
109
+ }
110
+ });
111
+ }
70
112
  /**
71
113
  * Check whether a hostname refers to the local machine.
72
114
  *
@@ -150,8 +192,8 @@ function applyProxyEnv(proxy) {
150
192
  * @param redirectCount - Internal redirect counter (starts at 0)
151
193
  * @returns A promise resolving to an HttpResponseLike
152
194
  */
153
- export function directRequest(url, body, headers, timeoutMs, redirectCount = 0) {
154
- return sendRawHttpRequest(url, body, headers, timeoutMs, redirectCount);
195
+ export function directRequest(url, body, headers, timeoutMs, redirectCount = 0, signal) {
196
+ return sendRawHttpRequest(url, body, headers, timeoutMs, redirectCount, signal);
155
197
  }
156
198
  const CRLF = Buffer.from("\r\n");
157
199
  /** Build the full raw HTTP/1.1 request buffer (headers + JSON body). */
@@ -241,7 +283,7 @@ function parseResponse(buffer) {
241
283
  return { status, headers, body };
242
284
  }
243
285
  /** Raw HTTP/1.1 POST over TCP/TLS with connection pooling, redirect following, and timeout. */
244
- async function sendRawHttpRequest(url, body, headers, timeoutMs, redirectCount) {
286
+ async function sendRawHttpRequest(url, body, headers, timeoutMs, redirectCount, signal) {
245
287
  const requestBuffer = buildRequestPayload(body, headers, url);
246
288
  const port = url.port ? Number(url.port) : url.protocol === "https:" ? 443 : 80;
247
289
  const isHttps = url.protocol === "https:";
@@ -259,6 +301,7 @@ async function sendRawHttpRequest(url, body, headers, timeoutMs, redirectCount)
259
301
  return;
260
302
  settled = true;
261
303
  clearTimeout(timeout);
304
+ signal?.removeEventListener("abort", onAbort);
262
305
  fn();
263
306
  };
264
307
  const timeout = setTimeout(() => {
@@ -267,6 +310,16 @@ async function sendRawHttpRequest(url, body, headers, timeoutMs, redirectCount)
267
310
  reject(new Error(`Request timed out after ${timeoutMs}ms`));
268
311
  });
269
312
  }, timeoutMs);
313
+ // Wire the caller's AbortSignal through to the raw socket (F2.1)
314
+ const onAbort = () => {
315
+ if (settled)
316
+ return;
317
+ settle(() => {
318
+ socket.destroy();
319
+ reject(new DOMException("The operation was aborted.", "AbortError"));
320
+ });
321
+ };
322
+ signal?.addEventListener("abort", onAbort, { once: true });
270
323
  const releaseOrDestroy = () => {
271
324
  if (responseConnectionClose) {
272
325
  socket.destroy();
@@ -369,7 +422,8 @@ async function sendRawHttpRequest(url, body, headers, timeoutMs, redirectCount)
369
422
  typeof location === "string" &&
370
423
  location.length > 0) {
371
424
  if (redirectCount >= 5) {
372
- const text = `Redirect limit exceeded for ${url.toString()}`;
425
+ // Strip the query string it may contain API keys (e.g. `?key=...`)
426
+ const text = `Redirect limit exceeded for ${url.origin}${url.pathname}`;
373
427
  return {
374
428
  ok: false,
375
429
  status: response.status,
@@ -389,7 +443,7 @@ async function sendRawHttpRequest(url, body, headers, timeoutMs, redirectCount)
389
443
  const safeHeaders = sameOrigin
390
444
  ? headers
391
445
  : Object.fromEntries(Object.entries(headers).filter(([key]) => key.toLowerCase() !== "authorization"));
392
- return sendRawHttpRequest(redirectUrl, body, safeHeaders, timeoutMs, redirectCount + 1);
446
+ return sendRawHttpRequest(redirectUrl, body, safeHeaders, timeoutMs, redirectCount + 1, signal);
393
447
  }
394
448
  const text = response.body.toString("utf8");
395
449
  return {
@@ -421,7 +475,7 @@ export async function postJson(urlString, body, headers, timeoutMs, proxy, signa
421
475
  const url = new URL(urlString);
422
476
  const bypassProxy = isLocalhost(url.hostname) || matchesNoProxy(url.hostname, proxy?.noProxy);
423
477
  if (bypassProxy || !proxy?.url) {
424
- return directRequest(url, body, headers, timeoutMs);
478
+ return directRequest(url, body, headers, timeoutMs, 0, signal);
425
479
  }
426
480
  return postJsonViaFetch(urlString, body, headers, timeoutMs, proxy, signal);
427
481
  }
@@ -12,6 +12,7 @@ import type { ProxyConfig } from "../core/config.js";
12
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
+ * @param keepAlive - Optional Ollama keep_alive value sent with /api/embed requests (e.g. "-1")
15
16
  */
16
17
  export declare class OllamaProvider implements EmbeddingProvider {
17
18
  readonly name = "ollama";
@@ -21,7 +22,8 @@ export declare class OllamaProvider implements EmbeddingProvider {
21
22
  private readonly timeoutMs;
22
23
  private proxy?;
23
24
  private readonly logLevel?;
24
- constructor(baseUrl: string, model: string, apiKey?: string, timeoutMs?: number, proxy?: ProxyConfig, logLevel?: string);
25
+ private readonly keepAlive?;
26
+ constructor(baseUrl: string, model: string, apiKey?: string, timeoutMs?: number, proxy?: ProxyConfig, logLevel?: string, keepAlive?: string);
25
27
  private getLogFilePath;
26
28
  private debug;
27
29
  embed(texts: string[], _purpose?: "query" | "document"): Promise<number[][]>;
@@ -10,6 +10,7 @@ import { appendDebugLog } from "../core/fileLogger.js";
10
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
+ * @param keepAlive - Optional Ollama keep_alive value sent with /api/embed requests (e.g. "-1")
13
14
  */
14
15
  export class OllamaProvider {
15
16
  name = "ollama";
@@ -19,13 +20,15 @@ export class OllamaProvider {
19
20
  timeoutMs;
20
21
  proxy;
21
22
  logLevel;
22
- constructor(baseUrl, model, apiKey, timeoutMs = 120000, proxy, logLevel) {
23
+ keepAlive;
24
+ constructor(baseUrl, model, apiKey, timeoutMs = 120000, proxy, logLevel, keepAlive) {
23
25
  this.baseUrl = baseUrl.replace(/\/+$/, "");
24
26
  this.model = model;
25
27
  this.apiKey = apiKey;
26
28
  this.timeoutMs = timeoutMs;
27
29
  this.proxy = proxy;
28
30
  this.logLevel = logLevel;
31
+ this.keepAlive = keepAlive;
29
32
  }
30
33
  getLogFilePath() {
31
34
  return path.resolve(process.cwd(), ".opencode", "opencode-rag.log");
@@ -44,7 +47,14 @@ export class OllamaProvider {
44
47
  }
45
48
  this.debug(`OllamaProvider requesting ${texts.length} embedding vector${texts.length === 1 ? "" : "s"}`);
46
49
  try {
47
- const response = await postJson(`${this.baseUrl}/embed`, { model: this.model, input: texts.length === 1 ? texts[0] : texts }, headers, this.timeoutMs, this.proxy);
50
+ const body = {
51
+ model: this.model,
52
+ input: texts.length === 1 ? texts[0] : texts,
53
+ };
54
+ if (this.keepAlive) {
55
+ body.keep_alive = this.keepAlive;
56
+ }
57
+ const response = await postJson(`${this.baseUrl}/embed`, body, headers, this.timeoutMs, this.proxy);
48
58
  if (!response.ok) {
49
59
  const body = await response.text();
50
60
  throw new Error(`Ollama embedding failed (${response.status}): ${body}`);
@@ -15,6 +15,13 @@ export function createSessionLogger(storePath) {
15
15
  const info = props.info;
16
16
  if (!info || info.role !== "assistant")
17
17
  return;
18
+ // message.updated fires repeatedly DURING streaming — only record
19
+ // completed messages (time.completed/finish/error set), otherwise
20
+ // every token chunk triggers a synchronous disk write on the
21
+ // event hot path and the log fills with redundant entries.
22
+ const isFinal = !!info.time?.completed || !!info.finish || !!info.error;
23
+ if (!isFinal)
24
+ return;
18
25
  const ev = {
19
26
  ts: Date.now(),
20
27
  event: "message",
@@ -19,6 +19,10 @@ export function validateSessionID(sessionID) {
19
19
  /** Append a single event to a session's JSONL log file. Creates the directory and file if needed. */
20
20
  export function appendSessionEvent(storePath, event) {
21
21
  try {
22
+ // Guard against path traversal / malformed IDs — never write outside the
23
+ // eval directory.
24
+ if (!validateSessionID(event.sessionID))
25
+ return;
22
26
  const dir = getEvalDir(storePath);
23
27
  mkdirSync(dir, { recursive: true });
24
28
  const filePath = getSessionPath(storePath, event.sessionID);
@@ -31,6 +35,8 @@ export function appendSessionEvent(storePath, event) {
31
35
  /** Read all events for a session from its JSONL log file. Returns an empty array if the file does not exist or cannot be read. */
32
36
  export function readSessionEvents(storePath, sessionID) {
33
37
  try {
38
+ if (!validateSessionID(sessionID))
39
+ return [];
34
40
  const filePath = getSessionPath(storePath, sessionID);
35
41
  const content = readFileSync(filePath, "utf8");
36
42
  const lines = content.split("\n").filter((l) => l.trim().length > 0);
@@ -58,6 +64,8 @@ export function listSessionIDs(storePath) {
58
64
  /** Delete a session's JSONL log file from disk. Silently succeeds if the file does not exist. */
59
65
  export function deleteSession(storePath, sessionID) {
60
66
  try {
67
+ if (!validateSessionID(sessionID))
68
+ return;
61
69
  const filePath = getSessionPath(storePath, sessionID);
62
70
  if (existsSync(filePath)) {
63
71
  unlinkSync(filePath);
@@ -23,7 +23,7 @@ export declare function getCurrentCommit(cwd: string): string | null;
23
23
  *
24
24
  * @param cwd - A path inside the repository.
25
25
  * @param fromCommit - The commit SHA to diff against.
26
- * @returns A diff result, or `null` if the git command fails.
26
+ * @returns A diff result, or `null` if the git command fails or the commit is invalid.
27
27
  */
28
28
  export declare function getChangedFilesSince(cwd: string, fromCommit: string): GitDiffResult | null;
29
29
  /**
@@ -45,9 +45,13 @@ export function getCurrentCommit(cwd) {
45
45
  *
46
46
  * @param cwd - A path inside the repository.
47
47
  * @param fromCommit - The commit SHA to diff against.
48
- * @returns A diff result, or `null` if the git command fails.
48
+ * @returns A diff result, or `null` if the git command fails or the commit is invalid.
49
49
  */
50
50
  export function getChangedFilesSince(cwd, fromCommit) {
51
+ // The commit value originates from a manifest file — validate it strictly
52
+ // so a crafted manifest can never inject git options via argv.
53
+ if (!/^[0-9a-f]{7,40}$/i.test(fromCommit))
54
+ return null;
51
55
  try {
52
56
  const changedRaw = execFileSync("git", ["diff", "--name-only", "--diff-filter=ACMRT", fromCommit, "HEAD"], {
53
57
  cwd, encoding: "utf-8", timeout: 10000, stdio: ["ignore", "pipe", "ignore"],