opencode-rag-plugin 1.19.4 → 1.19.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunker/base.js +19 -5
- package/dist/chunker/factory.js +27 -9
- package/dist/chunker/grammar.d.ts +18 -1
- package/dist/chunker/grammar.js +48 -10
- package/dist/chunker/pdf.js +30 -14
- package/dist/cli/commands/init-helpers.js +15 -2
- package/dist/cli/commands/init.js +31 -21
- package/dist/cli/commands/quirk.js +9 -3
- package/dist/cli/commands/setup.js +5 -2
- package/dist/cli/commands/status.js +14 -5
- package/dist/cli/commands/ui.js +23 -9
- package/dist/cli/commands/update.js +4 -5
- package/dist/cli/format.d.ts +5 -2
- package/dist/cli/format.js +14 -5
- package/dist/content/image.js +33 -11
- package/dist/content/reader.js +74 -13
- package/dist/core/bootstrap.js +10 -3
- package/dist/core/config.js +31 -0
- package/dist/core/desc-cache.d.ts +8 -2
- package/dist/core/desc-cache.js +10 -3
- package/dist/core/doc-progress.js +5 -2
- package/dist/core/provider-defaults.d.ts +2 -0
- package/dist/core/provider-defaults.js +19 -4
- package/dist/core/runtime-overrides.d.ts +0 -6
- package/dist/core/version-check.d.ts +5 -0
- package/dist/core/version-check.js +8 -2
- package/dist/describer/anthropic.d.ts +2 -2
- package/dist/describer/anthropic.js +19 -5
- package/dist/describer/describer.js +15 -2
- package/dist/describer/gemini.js +25 -10
- package/dist/embedder/factory.d.ts +5 -3
- package/dist/embedder/factory.js +41 -8
- package/dist/embedder/health.js +19 -19
- package/dist/embedder/http.d.ts +14 -1
- package/dist/embedder/http.js +60 -6
- package/dist/eval/session-logger.js +7 -0
- package/dist/eval/storage.js +8 -0
- package/dist/indexer/git-diff.d.ts +1 -1
- package/dist/indexer/git-diff.js +5 -1
- package/dist/indexer/pipeline.js +421 -344
- package/dist/indexer/stats.d.ts +2 -0
- package/dist/indexer/stats.js +1 -0
- package/dist/indexer/watch.js +8 -1
- package/dist/indexer/worker.js +21 -0
- package/dist/mcp/cli.js +4 -0
- package/dist/mcp/handlers.js +16 -5
- package/dist/mcp/server.js +3 -0
- package/dist/opencode/create-read-tool.js +14 -3
- package/dist/opencode/tool-args.js +23 -1
- package/dist/plugin.js +66 -152
- package/dist/quirks/auto-capture.js +5 -0
- package/dist/quirks/quirk-store.d.ts +1 -1
- package/dist/quirks/quirk-store.js +56 -17
- package/dist/retriever/context-optimizer.js +18 -4
- package/dist/retriever/keyword-index.d.ts +2 -0
- package/dist/retriever/keyword-index.js +38 -4
- package/dist/retriever/retriever.js +6 -1
- package/dist/tui.js +41 -4
- package/dist/vectorstore/lancedb.d.ts +25 -1
- package/dist/vectorstore/lancedb.js +157 -11
- package/dist/vectorstore/memory.js +5 -1
- package/dist/watcher.js +30 -4
- package/dist/web/api.d.ts +5 -1
- package/dist/web/api.js +195 -69
- package/dist/web/server.d.ts +2 -0
- package/dist/web/server.js +66 -28
- package/dist/web/static.d.ts +5 -2
- package/dist/web/static.js +9 -5
- package/dist/web/ui/assets/index-BDPYdtA1.js +3 -0
- package/dist/web/ui/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/ui/assets/index-CJBvt6e0.js +0 -3
package/dist/embedder/factory.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isOpenAiCompatible } from "../core/provider-defaults.js";
|
|
1
|
+
import { isOpenAiCompatible, supportsEmbedding } from "../core/provider-defaults.js";
|
|
2
2
|
import { OllamaProvider } from "./ollama.js";
|
|
3
3
|
import { OpenAIProvider } from "./openai.js";
|
|
4
4
|
import { CohereProvider } from "./cohere.js";
|
|
@@ -17,6 +17,12 @@ import pLimit from "p-limit";
|
|
|
17
17
|
export function createEmbedder(config) {
|
|
18
18
|
const { provider, baseUrl, model, apiKey, proxy, timeoutMs } = config.embedding;
|
|
19
19
|
const effectiveTimeoutMs = timeoutMs ?? 120000;
|
|
20
|
+
// Fail fast for chat-only providers (groq, deepseek, anthropic, google)
|
|
21
|
+
// — proceeding would surface a cryptic failure at index time.
|
|
22
|
+
if (!supportsEmbedding(provider)) {
|
|
23
|
+
throw new Error(`Provider "${provider}" does not support embeddings. ` +
|
|
24
|
+
"Use an embedding-capable provider (ollama, openai, cohere, nvidia, azure, mistral, together, fireworks).");
|
|
25
|
+
}
|
|
20
26
|
if (provider === "ollama") {
|
|
21
27
|
return new OllamaProvider(baseUrl, model, apiKey, effectiveTimeoutMs, proxy, config.logging.level);
|
|
22
28
|
}
|
|
@@ -34,6 +40,15 @@ export function createEmbedder(config) {
|
|
|
34
40
|
}
|
|
35
41
|
throw new Error(`Unknown embedding provider: ${provider}`);
|
|
36
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* HTTP statuses that indicate a permanent failure — retrying cannot help
|
|
45
|
+
* (auth errors, bad requests, missing resources). Providers raise these as
|
|
46
|
+
* Error objects whose messages contain the status code.
|
|
47
|
+
*/
|
|
48
|
+
const PERMANENT_STATUS_RE = /\(?(400|401|403|404|422)\)?/;
|
|
49
|
+
function isPermanentError(err) {
|
|
50
|
+
return PERMANENT_STATUS_RE.test(err instanceof Error ? err.message : String(err));
|
|
51
|
+
}
|
|
37
52
|
/**
|
|
38
53
|
* Embed a list of texts in batches with optional concurrency control and per-batch retry.
|
|
39
54
|
*
|
|
@@ -41,9 +56,11 @@ export function createEmbedder(config) {
|
|
|
41
56
|
* (or concurrently when `concurrency > 1`). When concurrency is limited, uses
|
|
42
57
|
* `p-limit` to cap the number of in-flight requests.
|
|
43
58
|
*
|
|
44
|
-
* Each batch is retried up to `retryMax` times with exponential backoff
|
|
45
|
-
*
|
|
46
|
-
*
|
|
59
|
+
* Each batch is retried up to `retryMax` times with exponential backoff (only
|
|
60
|
+
* for transient failures — auth/validation errors are not retried). If all
|
|
61
|
+
* retries are exhausted or the provider returns a mismatched embedding count,
|
|
62
|
+
* the batch is skipped and empty arrays are returned for those texts so the
|
|
63
|
+
* caller can still process successfully embedded batches.
|
|
47
64
|
*
|
|
48
65
|
* @param embedder - The embedding provider to use
|
|
49
66
|
* @param texts - Array of text strings to embed
|
|
@@ -67,13 +84,27 @@ export async function embedBatch(embedder, texts, batchSize = 10, purpose, concu
|
|
|
67
84
|
async function embedWithRetry(batchTexts) {
|
|
68
85
|
for (let attempt = 0; attempt <= retryMax; attempt++) {
|
|
69
86
|
try {
|
|
70
|
-
|
|
87
|
+
const embeddings = await embedder.embed(batchTexts, purpose);
|
|
88
|
+
// Validate the response shape: a count mismatch would silently attach
|
|
89
|
+
// vectors to the wrong chunks downstream; a dimension mismatch would
|
|
90
|
+
// poison the store. Treat both as a retryable batch failure.
|
|
91
|
+
if (embeddings.length !== batchTexts.length) {
|
|
92
|
+
throw new Error(`Embedding provider returned ${embeddings.length} vectors for ${batchTexts.length} texts`);
|
|
93
|
+
}
|
|
94
|
+
const dims = new Set(embeddings.map((v) => v.length));
|
|
95
|
+
if (dims.size > 1 || dims.has(0)) {
|
|
96
|
+
throw new Error(`Embedding provider returned inconsistent vector dimensions: ${[...dims].join(", ")}`);
|
|
97
|
+
}
|
|
98
|
+
return embeddings;
|
|
71
99
|
}
|
|
72
100
|
catch (err) {
|
|
73
|
-
if (attempt < retryMax) {
|
|
74
|
-
const delay = retryBaseDelayMs * Math.pow(2, attempt);
|
|
101
|
+
if (attempt < retryMax && !isPermanentError(err)) {
|
|
102
|
+
const delay = retryBaseDelayMs * Math.pow(2, attempt) * (0.8 + Math.random() * 0.4);
|
|
75
103
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
76
104
|
}
|
|
105
|
+
else if (attempt >= retryMax || isPermanentError(err)) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
77
108
|
}
|
|
78
109
|
}
|
|
79
110
|
return null;
|
|
@@ -99,7 +130,9 @@ export async function embedBatch(embedder, texts, batchSize = 10, purpose, concu
|
|
|
99
130
|
const batchResults = await Promise.all(batches.map((batch) => limit(async () => {
|
|
100
131
|
const embeddings = await embedWithRetry(batch.texts);
|
|
101
132
|
const flatResult = embeddings ?? batch.texts.map(() => []);
|
|
102
|
-
|
|
133
|
+
// Count processed TEXTS (not returned vectors) so failed batches
|
|
134
|
+
// still advance progress towards the total.
|
|
135
|
+
completedCount += batch.texts.length;
|
|
103
136
|
onProgress?.(completedCount, texts.length);
|
|
104
137
|
return { index: batch.index, embeddings: flatResult };
|
|
105
138
|
})));
|
package/dist/embedder/health.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { postJson } from "./http.js";
|
|
1
|
+
import { fetchWithProxy, postJson } from "./http.js";
|
|
2
2
|
/**
|
|
3
3
|
* Check connectivity and model availability for all configured providers.
|
|
4
4
|
* Returns one result per configured model (embedding + description + image_description if enabled).
|
|
@@ -42,10 +42,10 @@ async function checkDescriptionModel(config, _timeoutMs) {
|
|
|
42
42
|
return checkOllamaChat(baseUrl, model, descTimeout, desc.proxy);
|
|
43
43
|
}
|
|
44
44
|
if (provider === "anthropic") {
|
|
45
|
-
return checkAnthropicChat(baseUrl, model, apiKey, descTimeout);
|
|
45
|
+
return checkAnthropicChat(baseUrl, model, apiKey, descTimeout, desc.proxy);
|
|
46
46
|
}
|
|
47
47
|
if (provider === "google") {
|
|
48
|
-
return checkGoogleChat(baseUrl, model, apiKey, descTimeout);
|
|
48
|
+
return checkGoogleChat(baseUrl, model, apiKey, descTimeout, desc.proxy);
|
|
49
49
|
}
|
|
50
50
|
// OpenAI-compatible chat endpoint
|
|
51
51
|
return checkOpenAiChat(baseUrl, model, apiKey, descTimeout, desc.proxy);
|
|
@@ -62,10 +62,10 @@ async function checkImageDescriptionModel(config, _timeoutMs) {
|
|
|
62
62
|
return checkOllamaChat(baseUrl, model, imgTimeout, img.proxy, "image_description");
|
|
63
63
|
}
|
|
64
64
|
if (provider === "anthropic") {
|
|
65
|
-
return checkAnthropicChat(baseUrl, model, apiKey, imgTimeout, "image_description");
|
|
65
|
+
return checkAnthropicChat(baseUrl, model, apiKey, imgTimeout, img.proxy, "image_description");
|
|
66
66
|
}
|
|
67
67
|
if (provider === "google") {
|
|
68
|
-
return checkGoogleChat(baseUrl, model, apiKey, imgTimeout, "image_description");
|
|
68
|
+
return checkGoogleChat(baseUrl, model, apiKey, imgTimeout, img.proxy, "image_description");
|
|
69
69
|
}
|
|
70
70
|
// OpenAI-compatible chat endpoint
|
|
71
71
|
return checkOpenAiChat(baseUrl, model, apiKey, imgTimeout, img.proxy, "image_description");
|
|
@@ -125,16 +125,16 @@ async function checkOllamaChat(baseUrl, model, timeoutMs, proxy, type = "descrip
|
|
|
125
125
|
* Check OpenAI-compatible embedding endpoint via the /models endpoint
|
|
126
126
|
* to validate the API key without consuming embedding tokens.
|
|
127
127
|
*/
|
|
128
|
-
async function checkOpenAiEmbed(baseUrl, model, apiKey, timeoutMs,
|
|
128
|
+
async function checkOpenAiEmbed(baseUrl, model, apiKey, timeoutMs, proxy) {
|
|
129
129
|
if (!apiKey) {
|
|
130
130
|
return { provider: "openai", model, type: "embedding", status: "error", error: "No API key configured" };
|
|
131
131
|
}
|
|
132
132
|
const url = `${baseUrl.replace(/\/+$/, "")}/models`;
|
|
133
133
|
try {
|
|
134
|
-
const response = await
|
|
134
|
+
const response = await fetchWithProxy(url, {
|
|
135
135
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
136
136
|
signal: AbortSignal.timeout(Math.min(timeoutMs ?? 15000, 15000)),
|
|
137
|
-
});
|
|
137
|
+
}, proxy);
|
|
138
138
|
if (response.ok) {
|
|
139
139
|
return { provider: "openai", model, type: "embedding", status: "ok" };
|
|
140
140
|
}
|
|
@@ -150,16 +150,16 @@ async function checkOpenAiEmbed(baseUrl, model, apiKey, timeoutMs, _proxy) {
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
/** Check OpenAI-compatible chat endpoint via the /models endpoint. */
|
|
153
|
-
async function checkOpenAiChat(baseUrl, model, apiKey, timeoutMs,
|
|
153
|
+
async function checkOpenAiChat(baseUrl, model, apiKey, timeoutMs, proxy, type = "description") {
|
|
154
154
|
if (!apiKey) {
|
|
155
155
|
return { provider: "openai", model, type, status: "error", error: "No API key configured" };
|
|
156
156
|
}
|
|
157
157
|
const url = `${baseUrl.replace(/\/+$/, "")}/models`;
|
|
158
158
|
try {
|
|
159
|
-
const response = await
|
|
159
|
+
const response = await fetchWithProxy(url, {
|
|
160
160
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
161
161
|
signal: AbortSignal.timeout(Math.min(timeoutMs ?? 15000, 15000)),
|
|
162
|
-
});
|
|
162
|
+
}, proxy);
|
|
163
163
|
if (response.ok) {
|
|
164
164
|
return { provider: "openai", model, type, status: "ok" };
|
|
165
165
|
}
|
|
@@ -196,13 +196,13 @@ async function checkCohereEmbed(baseUrl, model, apiKey, timeoutMs, proxy) {
|
|
|
196
196
|
}
|
|
197
197
|
// ── Anthropic check ────────────────────────────────────────────
|
|
198
198
|
/** Check Anthropic chat endpoint by sending a minimal message. */
|
|
199
|
-
async function checkAnthropicChat(baseUrl, model, apiKey, timeoutMs, type = "description") {
|
|
199
|
+
async function checkAnthropicChat(baseUrl, model, apiKey, timeoutMs, proxy, type = "description") {
|
|
200
200
|
if (!apiKey) {
|
|
201
201
|
return { provider: "anthropic", model, type, status: "error", error: "No API key configured" };
|
|
202
202
|
}
|
|
203
203
|
const url = `${baseUrl.replace(/\/+$/, "")}/messages`;
|
|
204
204
|
try {
|
|
205
|
-
const response = await
|
|
205
|
+
const response = await fetchWithProxy(url, {
|
|
206
206
|
method: "POST",
|
|
207
207
|
headers: {
|
|
208
208
|
"Content-Type": "application/json",
|
|
@@ -215,7 +215,7 @@ async function checkAnthropicChat(baseUrl, model, apiKey, timeoutMs, type = "des
|
|
|
215
215
|
messages: [{ role: "user", content: "hi" }],
|
|
216
216
|
}),
|
|
217
217
|
signal: AbortSignal.timeout(Math.min(timeoutMs ?? 15000, 15000)),
|
|
218
|
-
});
|
|
218
|
+
}, proxy);
|
|
219
219
|
if (response.ok) {
|
|
220
220
|
return { provider: "anthropic", model, type, status: "ok" };
|
|
221
221
|
}
|
|
@@ -232,20 +232,20 @@ async function checkAnthropicChat(baseUrl, model, apiKey, timeoutMs, type = "des
|
|
|
232
232
|
}
|
|
233
233
|
// ── Google Gemini check ────────────────────────────────────────
|
|
234
234
|
/** Check Google Gemini chat endpoint by sending a minimal generateContent request. */
|
|
235
|
-
async function checkGoogleChat(baseUrl, model, apiKey, timeoutMs, type = "description") {
|
|
235
|
+
async function checkGoogleChat(baseUrl, model, apiKey, timeoutMs, proxy, type = "description") {
|
|
236
236
|
if (!apiKey) {
|
|
237
237
|
return { provider: "google", model, type, status: "error", error: "No API key configured" };
|
|
238
238
|
}
|
|
239
|
-
const url = `${baseUrl.replace(/\/+$/, "")}/models/${model}:generateContent
|
|
239
|
+
const url = `${baseUrl.replace(/\/+$/, "")}/models/${model}:generateContent`;
|
|
240
240
|
try {
|
|
241
|
-
const response = await
|
|
241
|
+
const response = await fetchWithProxy(url, {
|
|
242
242
|
method: "POST",
|
|
243
|
-
headers: { "Content-Type": "application/json" },
|
|
243
|
+
headers: { "Content-Type": "application/json", "x-goog-api-key": apiKey },
|
|
244
244
|
body: JSON.stringify({
|
|
245
245
|
contents: [{ parts: [{ text: "hi" }] }],
|
|
246
246
|
}),
|
|
247
247
|
signal: AbortSignal.timeout(Math.min(timeoutMs ?? 15000, 15000)),
|
|
248
|
-
});
|
|
248
|
+
}, proxy);
|
|
249
249
|
if (response.ok) {
|
|
250
250
|
return { provider: "google", model, type, status: "ok" };
|
|
251
251
|
}
|
package/dist/embedder/http.d.ts
CHANGED
|
@@ -12,6 +12,19 @@ export interface HttpResponseLike {
|
|
|
12
12
|
/** Destroy all pooled TCP/TLS sockets and clear the connection pool. */
|
|
13
13
|
declare function destroyAllPooledConnections(): void;
|
|
14
14
|
export { destroyAllPooledConnections };
|
|
15
|
+
/**
|
|
16
|
+
* Fetch a URL through the configured proxy, applying the proxy to
|
|
17
|
+
* HTTP(S)_PROXY env vars for the duration of the request (serialized).
|
|
18
|
+
*
|
|
19
|
+
* Used by health checks and other one-off requests that do not go
|
|
20
|
+
* through {@link postJson}.
|
|
21
|
+
*/
|
|
22
|
+
export declare function fetchWithProxy(urlString: string, init: {
|
|
23
|
+
method?: string;
|
|
24
|
+
headers?: Record<string, string>;
|
|
25
|
+
body?: string;
|
|
26
|
+
signal?: AbortSignal;
|
|
27
|
+
}, proxy?: ProxyConfig): Promise<Response>;
|
|
15
28
|
/**
|
|
16
29
|
* Check whether a hostname refers to the local machine.
|
|
17
30
|
*
|
|
@@ -43,7 +56,7 @@ export declare function matchesNoProxy(hostname: string, noProxy?: string): bool
|
|
|
43
56
|
* @param redirectCount - Internal redirect counter (starts at 0)
|
|
44
57
|
* @returns A promise resolving to an HttpResponseLike
|
|
45
58
|
*/
|
|
46
|
-
export declare function directRequest(url: URL, body: unknown, headers: Record<string, string>, timeoutMs: number, redirectCount?: number): Promise<HttpResponseLike>;
|
|
59
|
+
export declare function directRequest(url: URL, body: unknown, headers: Record<string, string>, timeoutMs: number, redirectCount?: number, signal?: AbortSignal): Promise<HttpResponseLike>;
|
|
47
60
|
/**
|
|
48
61
|
* POST JSON to a URL, optionally routing through a proxy.
|
|
49
62
|
*
|
package/dist/embedder/http.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
|
@@ -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",
|
package/dist/eval/storage.js
CHANGED
|
@@ -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
|
/**
|
package/dist/indexer/git-diff.js
CHANGED
|
@@ -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"],
|