deepseek-web-retrieval 0.4.0 → 0.5.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.
- package/CHANGELOG.md +93 -244
- package/README.md +16 -11
- package/dist/cli.js +1366 -615
- package/docs/agents/codex.md +28 -7
- package/docs/agents/opencode-v2.md +1 -1
- package/docs/agents/pi.md +88 -86
- package/docs/design/installer-design.md +72 -9
- package/docs/hosts.jsonc +2 -2
- package/docs/integration.md +14 -9
- package/docs/research/control-matrix.md +48 -13
- package/docs/research/mcp-compatibility.md +9 -4
- package/examples/pi.settings.jsonc +17 -0
- package/index.ts +2 -2
- package/package.json +35 -4
- package/src/core/auth.ts +4 -6
- package/src/core/client.ts +65 -53
- package/src/core/constants.ts +24 -24
- package/src/core/descriptions.ts +30 -6
- package/src/core/domains.ts +56 -67
- package/src/core/env.ts +112 -0
- package/src/core/format.ts +53 -50
- package/src/core/index.ts +9 -9
- package/src/core/jsonc.ts +6 -6
- package/src/core/object.ts +4 -0
- package/src/core/options.ts +20 -25
- package/src/core/search.ts +45 -44
- package/src/core/transport.ts +109 -113
- package/src/core/types.ts +47 -47
- package/src/core/url.ts +20 -20
- package/src/installer/apply.ts +256 -81
- package/src/installer/cli.ts +135 -104
- package/src/installer/detect.ts +46 -31
- package/src/installer/formats/index.ts +26 -28
- package/src/installer/formats/json.ts +81 -77
- package/src/installer/formats/rules.ts +55 -28
- package/src/installer/formats/toml.ts +88 -81
- package/src/installer/formats/value.ts +34 -30
- package/src/installer/hosts/codex.ts +100 -54
- package/src/installer/hosts/index.ts +126 -41
- package/src/installer/hosts/opencode.ts +70 -51
- package/src/installer/hosts/pi.ts +269 -75
- package/src/installer/hosts/shared.ts +19 -14
- package/src/installer/operation-lock.ts +54 -0
- package/src/installer/ownership.ts +129 -0
- package/src/installer/paths.ts +7 -5
- package/src/installer/persistence.ts +66 -0
- package/src/installer/render.ts +29 -29
- package/src/installer/revert.ts +33 -29
- package/src/installer/rulesContent.ts +24 -13
- package/src/installer/state.ts +116 -31
- package/src/installer/types.ts +74 -56
- package/src/installer/uninstall.ts +40 -17
- package/src/mcp/auth.ts +7 -8
- package/src/mcp/cli.ts +38 -20
- package/src/mcp/client.ts +35 -0
- package/src/mcp/server.ts +68 -97
- package/src/opencode/auth.ts +24 -26
- package/src/opencode/plugin.ts +99 -87
- package/src/pi/extension.ts +164 -0
- package/src/pi/settings.ts +49 -0
- package/src/mcp/config.ts +0 -112
package/src/core/search.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { normalizeDomains } from "./domains"
|
|
2
|
-
import { truncate } from "./format"
|
|
3
|
-
import type { DeepSeekSearchResult, SearchOptions, SearchResult } from "./types"
|
|
4
|
-
import { normalizeResultUrl } from "./url"
|
|
1
|
+
import { normalizeDomains } from "./domains";
|
|
2
|
+
import { truncate } from "./format";
|
|
3
|
+
import type { DeepSeekSearchResult, SearchOptions, SearchResult } from "./types";
|
|
4
|
+
import { normalizeResultUrl } from "./url";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Appends DeepSeek search operators:
|
|
@@ -13,72 +13,73 @@ export function buildQuery(
|
|
|
13
13
|
query: string,
|
|
14
14
|
options: Pick<SearchOptions, "blockedDomains" | "includeDomains" | "language">,
|
|
15
15
|
): string {
|
|
16
|
-
const parts = [query.trim()]
|
|
17
|
-
for (const domain of normalizeDomains(options.blockedDomains)) parts.push(`-site:${domain}`)
|
|
18
|
-
const include = normalizeDomains(options.includeDomains)
|
|
19
|
-
if (include.length) parts.push(include.map((domain) => `site:${domain}`).join(" OR "))
|
|
20
|
-
const language = options.language?.trim().replace(/^lang:/i, "")
|
|
21
|
-
if (language) parts.push(`lang:${language}`)
|
|
22
|
-
return parts.filter((part) => part.length > 0).join(" ")
|
|
16
|
+
const parts = [query.trim()];
|
|
17
|
+
for (const domain of normalizeDomains(options.blockedDomains)) parts.push(`-site:${domain}`);
|
|
18
|
+
const include = normalizeDomains(options.includeDomains);
|
|
19
|
+
if (include.length) parts.push(include.map((domain) => `site:${domain}`).join(" OR "));
|
|
20
|
+
const language = options.language?.trim().replace(/^lang:/i, "");
|
|
21
|
+
if (language) parts.push(`lang:${language}`);
|
|
22
|
+
return parts.filter((part) => part.length > 0).join(" ");
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export function parsePublished(value: unknown): number | undefined {
|
|
26
|
-
if (typeof value !== "string" || !value.trim()) return undefined
|
|
27
|
-
const timestamp = Date.parse(value)
|
|
28
|
-
return Number.isFinite(timestamp) ? timestamp : undefined
|
|
26
|
+
if (typeof value !== "string" || !value.trim()) return undefined;
|
|
27
|
+
const timestamp = Date.parse(value);
|
|
28
|
+
return Number.isFinite(timestamp) ? timestamp : undefined;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Filters and maps DeepSeek results into the neutral SearchResult shape.
|
|
33
|
-
*
|
|
33
|
+
* A present rerank_score always compares against minRerank; a missing or
|
|
34
|
+
* non-finite score passes only while no positive threshold is set, so the
|
|
35
|
+
* default returns exactly what DeepSeek ranked. Authority -1/missing is
|
|
34
36
|
* "unknown" and only passes when allowUnknownAuthority is enabled.
|
|
35
37
|
*/
|
|
36
|
-
export function processResults(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
): SearchResult[] {
|
|
40
|
-
const results: SearchResult[] = []
|
|
41
|
-
const seen = new Set<string>()
|
|
38
|
+
export function processResults(raw: readonly DeepSeekSearchResult[], options: SearchOptions): SearchResult[] {
|
|
39
|
+
const results: SearchResult[] = [];
|
|
40
|
+
const seen = new Set<string>();
|
|
42
41
|
|
|
43
42
|
for (const item of raw) {
|
|
44
|
-
if (results.length >= options.maxResults) break
|
|
45
|
-
if (!item || typeof item !== "object") continue
|
|
43
|
+
if (results.length >= options.maxResults) break;
|
|
44
|
+
if (!item || typeof item !== "object") continue;
|
|
46
45
|
|
|
47
|
-
const url = normalizeResultUrl(item.url)
|
|
48
|
-
if (!url || seen.has(url)) continue
|
|
46
|
+
const url = normalizeResultUrl(item.url);
|
|
47
|
+
if (!url || seen.has(url)) continue;
|
|
49
48
|
|
|
50
|
-
const rerank = item.rerank_score
|
|
51
|
-
if (typeof rerank
|
|
49
|
+
const rerank = item.rerank_score;
|
|
50
|
+
if (typeof rerank === "number" && Number.isFinite(rerank)) {
|
|
51
|
+
if (rerank < options.minRerank) continue;
|
|
52
|
+
} else if (options.minRerank > 0) {
|
|
53
|
+
// A threshold was explicitly raised, so a result that cannot be scored
|
|
54
|
+
// against it must not slip through.
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
52
57
|
|
|
53
|
-
const authority = item.authority
|
|
54
|
-
const knownAuthority =
|
|
55
|
-
typeof authority === "number" && Number.isFinite(authority) && authority >= 0
|
|
58
|
+
const authority = item.authority;
|
|
59
|
+
const knownAuthority = typeof authority === "number" && Number.isFinite(authority) && authority >= 0;
|
|
56
60
|
if (knownAuthority) {
|
|
57
|
-
if (authority < options.minAuthority) continue
|
|
61
|
+
if (authority < options.minAuthority) continue;
|
|
58
62
|
} else if (!options.allowUnknownAuthority) {
|
|
59
|
-
continue
|
|
63
|
+
continue;
|
|
60
64
|
}
|
|
61
65
|
|
|
62
|
-
seen.add(url)
|
|
63
|
-
const content = truncate(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
)
|
|
67
|
-
const published = parsePublished(item.date)
|
|
68
|
-
const title = optionalText(item.title)
|
|
66
|
+
seen.add(url);
|
|
67
|
+
const content = truncate(optionalText(item.summary) ?? optionalText(item.snippet) ?? "", options.maxSummaryChars);
|
|
68
|
+
const published = parsePublished(item.date);
|
|
69
|
+
const title = optionalText(item.title);
|
|
69
70
|
results.push({
|
|
70
71
|
url,
|
|
71
72
|
...(title ? { title } : {}),
|
|
72
73
|
...(content ? { content } : {}),
|
|
73
74
|
...(published !== undefined ? { published } : {}),
|
|
74
|
-
})
|
|
75
|
+
});
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
return results
|
|
78
|
+
return results;
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
function optionalText(value: unknown): string | undefined {
|
|
81
|
-
if (typeof value !== "string") return undefined
|
|
82
|
-
const trimmed = value.trim()
|
|
83
|
-
return trimmed ? trimmed : undefined
|
|
82
|
+
if (typeof value !== "string") return undefined;
|
|
83
|
+
const trimmed = value.trim();
|
|
84
|
+
return trimmed ? trimmed : undefined;
|
|
84
85
|
}
|
package/src/core/transport.ts
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
import { MAX_ERROR_BODY_CHARS, MAX_RESPONSE_BYTES, MAX_RETRY_DELAY_MS } from "./constants"
|
|
2
|
-
import { formatApiError, formatTimeoutError, redactSecrets, truncate } from "./format"
|
|
3
|
-
import type { RequestAuth, RetryOptions } from "./types"
|
|
1
|
+
import { MAX_ERROR_BODY_CHARS, MAX_RESPONSE_BYTES, MAX_RETRY_DELAY_MS } from "./constants";
|
|
2
|
+
import { formatApiError, formatTimeoutError, redactSecrets, truncate } from "./format";
|
|
3
|
+
import type { RequestAuth, RetryOptions } from "./types";
|
|
4
4
|
|
|
5
5
|
/** Everything one POST needs; `fetchImpl` is injectable so tests need no monkey-patching. */
|
|
6
6
|
export interface TransportOptions {
|
|
7
|
-
readonly signal: AbortSignal
|
|
8
|
-
readonly auth: RequestAuth
|
|
9
|
-
readonly retry: RetryOptions
|
|
10
|
-
readonly timeoutMs: number
|
|
11
|
-
readonly fetchImpl?: typeof fetch
|
|
7
|
+
readonly signal: AbortSignal;
|
|
8
|
+
readonly auth: RequestAuth;
|
|
9
|
+
readonly retry: RetryOptions;
|
|
10
|
+
readonly timeoutMs: number;
|
|
11
|
+
readonly fetchImpl?: typeof fetch;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export class DeepSeekApiError extends Error {
|
|
15
|
-
readonly operation: string
|
|
16
|
-
readonly status: number
|
|
17
|
-
readonly body: string
|
|
18
|
-
readonly retryable: boolean
|
|
19
|
-
readonly retryAfterMs?: number
|
|
15
|
+
readonly operation: string;
|
|
16
|
+
readonly status: number;
|
|
17
|
+
readonly body: string;
|
|
18
|
+
readonly retryable: boolean;
|
|
19
|
+
readonly retryAfterMs?: number;
|
|
20
20
|
|
|
21
21
|
constructor(input: {
|
|
22
|
-
operation: string
|
|
23
|
-
status: number
|
|
24
|
-
body: string
|
|
25
|
-
retryAfterMs?: number
|
|
22
|
+
operation: string;
|
|
23
|
+
status: number;
|
|
24
|
+
body: string;
|
|
25
|
+
retryAfterMs?: number;
|
|
26
26
|
}) {
|
|
27
|
-
const body = redactSecrets(input.body)
|
|
28
|
-
super(formatApiError(input.operation, input.status, body))
|
|
29
|
-
this.name = "DeepSeekApiError"
|
|
30
|
-
this.operation = input.operation
|
|
31
|
-
this.status = input.status
|
|
32
|
-
this.body = body
|
|
27
|
+
const body = redactSecrets(input.body);
|
|
28
|
+
super(formatApiError(input.operation, input.status, body));
|
|
29
|
+
this.name = "DeepSeekApiError";
|
|
30
|
+
this.operation = input.operation;
|
|
31
|
+
this.status = input.status;
|
|
32
|
+
this.body = body;
|
|
33
33
|
this.retryable =
|
|
34
34
|
input.status === 408 ||
|
|
35
35
|
input.status === 425 ||
|
|
36
36
|
input.status === 429 ||
|
|
37
|
-
(input.status >= 500 && input.status <= 599)
|
|
38
|
-
this.retryAfterMs = input.retryAfterMs
|
|
37
|
+
(input.status >= 500 && input.status <= 599);
|
|
38
|
+
this.retryAfterMs = input.retryAfterMs;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -45,23 +45,23 @@ export async function postJson<T>(
|
|
|
45
45
|
operation: string,
|
|
46
46
|
opts: TransportOptions,
|
|
47
47
|
): Promise<T> {
|
|
48
|
-
const fetchImpl = opts.fetchImpl ?? globalThis.fetch
|
|
49
|
-
const attempts = opts.retry.maxRetries + 1
|
|
50
|
-
let lastError: unknown
|
|
48
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
49
|
+
const attempts = opts.retry.maxRetries + 1;
|
|
50
|
+
let lastError: unknown;
|
|
51
51
|
|
|
52
52
|
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
53
53
|
try {
|
|
54
|
-
return await requestOnce<T>(endpoint, body, operation, opts, fetchImpl)
|
|
54
|
+
return await requestOnce<T>(endpoint, body, operation, opts, fetchImpl);
|
|
55
55
|
} catch (error) {
|
|
56
|
-
lastError = error
|
|
56
|
+
lastError = error;
|
|
57
57
|
if (!(error instanceof DeepSeekApiError) || !error.retryable || attempt === attempts - 1) {
|
|
58
|
-
throw error
|
|
58
|
+
throw error;
|
|
59
59
|
}
|
|
60
|
-
await sleep(retryDelay(error.retryAfterMs, opts.retry.baseDelayMs, attempt), opts.signal)
|
|
60
|
+
await sleep(retryDelay(error.retryAfterMs, opts.retry.baseDelayMs, attempt), opts.signal);
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
throw lastError
|
|
64
|
+
throw lastError;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
async function requestOnce<T>(
|
|
@@ -87,25 +87,25 @@ async function requestOnce<T>(
|
|
|
87
87
|
fetchImpl,
|
|
88
88
|
async (response) => {
|
|
89
89
|
if (!response.ok) {
|
|
90
|
-
const text = await readBoundedText(response, MAX_ERROR_BODY_CHARS)
|
|
90
|
+
const text = await readBoundedText(response, MAX_ERROR_BODY_CHARS);
|
|
91
91
|
throw new DeepSeekApiError({
|
|
92
92
|
operation,
|
|
93
93
|
status: response.status,
|
|
94
94
|
body: text,
|
|
95
95
|
retryAfterMs: parseRetryAfter(response.headers.get("retry-after")),
|
|
96
|
-
})
|
|
96
|
+
});
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
const text = await readResponseText(response, MAX_RESPONSE_BYTES, operation)
|
|
100
|
-
if (!text.trim()) throw new Error(`${operation} returned an empty response body`)
|
|
99
|
+
const text = await readResponseText(response, MAX_RESPONSE_BYTES, operation);
|
|
100
|
+
if (!text.trim()) throw new Error(`${operation} returned an empty response body`);
|
|
101
101
|
|
|
102
102
|
try {
|
|
103
|
-
return JSON.parse(text) as T
|
|
103
|
+
return JSON.parse(text) as T;
|
|
104
104
|
} catch {
|
|
105
|
-
throw new Error(`${operation} returned invalid JSON: ${truncate(redactSecrets(text), 500)}`)
|
|
105
|
+
throw new Error(`${operation} returned invalid JSON: ${truncate(redactSecrets(text), 500)}`);
|
|
106
106
|
}
|
|
107
107
|
},
|
|
108
|
-
)
|
|
108
|
+
);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
/**
|
|
@@ -122,132 +122,128 @@ async function fetchWithTimeout<T>(
|
|
|
122
122
|
fetchImpl: typeof fetch,
|
|
123
123
|
handleResponse: (response: Response) => Promise<T>,
|
|
124
124
|
): Promise<T> {
|
|
125
|
-
const controller = new AbortController()
|
|
126
|
-
let timedOut = false
|
|
127
|
-
const forwardAbort = () => controller.abort(signal.reason)
|
|
128
|
-
if (signal.aborted) forwardAbort()
|
|
129
|
-
else signal.addEventListener("abort", forwardAbort, { once: true })
|
|
125
|
+
const controller = new AbortController();
|
|
126
|
+
let timedOut = false;
|
|
127
|
+
const forwardAbort = () => controller.abort(signal.reason);
|
|
128
|
+
if (signal.aborted) forwardAbort();
|
|
129
|
+
else signal.addEventListener("abort", forwardAbort, { once: true });
|
|
130
130
|
|
|
131
131
|
const timer = signal.aborted
|
|
132
132
|
? undefined
|
|
133
133
|
: setTimeout(() => {
|
|
134
|
-
timedOut = true
|
|
135
|
-
controller.abort(new Error("timeout"))
|
|
136
|
-
}, timeoutMs)
|
|
134
|
+
timedOut = true;
|
|
135
|
+
controller.abort(new Error("timeout"));
|
|
136
|
+
}, timeoutMs);
|
|
137
137
|
|
|
138
138
|
try {
|
|
139
|
-
const response = await fetchImpl(endpoint, { ...init, signal: controller.signal })
|
|
140
|
-
return await handleResponse(response)
|
|
139
|
+
const response = await fetchImpl(endpoint, { ...init, signal: controller.signal });
|
|
140
|
+
return await handleResponse(response);
|
|
141
141
|
} catch (error) {
|
|
142
|
-
if (timedOut) throw new Error(formatTimeoutError(operation, timeoutMs))
|
|
143
|
-
if (signal.aborted) throw abortReason(signal, operation)
|
|
144
|
-
throw error
|
|
142
|
+
if (timedOut) throw new Error(formatTimeoutError(operation, timeoutMs));
|
|
143
|
+
if (signal.aborted) throw abortReason(signal, operation);
|
|
144
|
+
throw error;
|
|
145
145
|
} finally {
|
|
146
|
-
if (timer !== undefined) clearTimeout(timer)
|
|
147
|
-
signal.removeEventListener("abort", forwardAbort)
|
|
146
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
147
|
+
signal.removeEventListener("abort", forwardAbort);
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
export function parseRetryAfter(value: string | null): number | undefined {
|
|
152
|
-
if (!value) return undefined
|
|
153
|
-
const trimmed = value.trim()
|
|
154
|
-
if (!trimmed) return undefined
|
|
152
|
+
if (!value) return undefined;
|
|
153
|
+
const trimmed = value.trim();
|
|
154
|
+
if (!trimmed) return undefined;
|
|
155
155
|
|
|
156
156
|
if (/^\d+$/.test(trimmed)) {
|
|
157
|
-
return Math.min(MAX_RETRY_DELAY_MS, Number(trimmed) * 1000)
|
|
157
|
+
return Math.min(MAX_RETRY_DELAY_MS, Number(trimmed) * 1000);
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
const timestamp = Date.parse(trimmed)
|
|
161
|
-
if (!Number.isFinite(timestamp)) return undefined
|
|
162
|
-
return Math.min(MAX_RETRY_DELAY_MS, Math.max(0, timestamp - Date.now()))
|
|
160
|
+
const timestamp = Date.parse(trimmed);
|
|
161
|
+
if (!Number.isFinite(timestamp)) return undefined;
|
|
162
|
+
return Math.min(MAX_RETRY_DELAY_MS, Math.max(0, timestamp - Date.now()));
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
export function retryDelay(retryAfterMs: number | undefined, baseDelayMs: number, attempt: number): number {
|
|
166
|
-
if (retryAfterMs !== undefined) return retryAfterMs
|
|
167
|
-
return Math.min(MAX_RETRY_DELAY_MS, baseDelayMs * 2 ** attempt)
|
|
166
|
+
if (retryAfterMs !== undefined) return retryAfterMs;
|
|
167
|
+
return Math.min(MAX_RETRY_DELAY_MS, baseDelayMs * 2 ** attempt);
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
function sleep(ms: number, signal: AbortSignal): Promise<void> {
|
|
171
|
-
if (ms <= 0) return Promise.resolve()
|
|
171
|
+
if (ms <= 0) return Promise.resolve();
|
|
172
172
|
return new Promise((resolve, reject) => {
|
|
173
173
|
const timer = setTimeout(() => {
|
|
174
|
-
signal.removeEventListener("abort", onAbort)
|
|
175
|
-
resolve()
|
|
176
|
-
}, ms)
|
|
174
|
+
signal.removeEventListener("abort", onAbort);
|
|
175
|
+
resolve();
|
|
176
|
+
}, ms);
|
|
177
177
|
const onAbort = () => {
|
|
178
|
-
clearTimeout(timer)
|
|
179
|
-
reject(abortReason(signal, "DeepSeek request"))
|
|
180
|
-
}
|
|
178
|
+
clearTimeout(timer);
|
|
179
|
+
reject(abortReason(signal, "DeepSeek request"));
|
|
180
|
+
};
|
|
181
181
|
if (signal.aborted) {
|
|
182
|
-
clearTimeout(timer)
|
|
183
|
-
reject(abortReason(signal, "DeepSeek request"))
|
|
184
|
-
return
|
|
182
|
+
clearTimeout(timer);
|
|
183
|
+
reject(abortReason(signal, "DeepSeek request"));
|
|
184
|
+
return;
|
|
185
185
|
}
|
|
186
|
-
signal.addEventListener("abort", onAbort, { once: true })
|
|
187
|
-
})
|
|
186
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
187
|
+
});
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
function abortReason(signal: AbortSignal, operation: string): unknown {
|
|
191
|
-
return signal.reason instanceof Error ? signal.reason : new Error(`${operation} request aborted`)
|
|
191
|
+
return signal.reason instanceof Error ? signal.reason : new Error(`${operation} request aborted`);
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
/** Reads at most `maxChars + 1` characters, enough for formatApiError to add its marker. */
|
|
195
195
|
async function readBoundedText(response: Response, maxChars: number): Promise<string> {
|
|
196
|
-
if (!response.body) return truncate(await response.text(), maxChars + 1)
|
|
196
|
+
if (!response.body) return truncate(await response.text(), maxChars + 1);
|
|
197
197
|
|
|
198
|
-
const reader = response.body.getReader()
|
|
199
|
-
const decoder = new TextDecoder()
|
|
200
|
-
const limit = maxChars + 1
|
|
201
|
-
let text = ""
|
|
202
|
-
let chars = 0
|
|
198
|
+
const reader = response.body.getReader();
|
|
199
|
+
const decoder = new TextDecoder();
|
|
200
|
+
const limit = maxChars + 1;
|
|
201
|
+
let text = "";
|
|
202
|
+
let chars = 0;
|
|
203
203
|
try {
|
|
204
204
|
while (chars < limit) {
|
|
205
|
-
const { done, value } = await reader.read()
|
|
206
|
-
if (done) break
|
|
207
|
-
const chunk = decoder.decode(value, { stream: true })
|
|
208
|
-
text += chunk
|
|
209
|
-
chars += Array.from(chunk).length
|
|
205
|
+
const { done, value } = await reader.read();
|
|
206
|
+
if (done) break;
|
|
207
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
208
|
+
text += chunk;
|
|
209
|
+
chars += Array.from(chunk).length;
|
|
210
210
|
}
|
|
211
|
-
text += decoder.decode()
|
|
211
|
+
text += decoder.decode();
|
|
212
212
|
} finally {
|
|
213
|
-
await reader.cancel().catch(() => undefined)
|
|
214
|
-
reader.releaseLock()
|
|
213
|
+
await reader.cancel().catch(() => undefined);
|
|
214
|
+
reader.releaseLock();
|
|
215
215
|
}
|
|
216
|
-
return truncate(text, limit)
|
|
216
|
+
return truncate(text, limit);
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
/** Reads a successful response body, rejecting payloads above `maxBytes`. */
|
|
220
|
-
async function readResponseText(
|
|
221
|
-
response: Response,
|
|
222
|
-
maxBytes: number,
|
|
223
|
-
operation: string,
|
|
224
|
-
): Promise<string> {
|
|
220
|
+
async function readResponseText(response: Response, maxBytes: number, operation: string): Promise<string> {
|
|
225
221
|
if (!response.body) {
|
|
226
|
-
const text = await response.text()
|
|
222
|
+
const text = await response.text();
|
|
227
223
|
if (new TextEncoder().encode(text).byteLength > maxBytes) {
|
|
228
|
-
throw new Error(`${operation} response exceeded ${maxBytes} bytes`)
|
|
224
|
+
throw new Error(`${operation} response exceeded ${maxBytes} bytes`);
|
|
229
225
|
}
|
|
230
|
-
return text
|
|
226
|
+
return text;
|
|
231
227
|
}
|
|
232
228
|
|
|
233
|
-
const reader = response.body.getReader()
|
|
234
|
-
const decoder = new TextDecoder()
|
|
235
|
-
let text = ""
|
|
236
|
-
let bytes = 0
|
|
229
|
+
const reader = response.body.getReader();
|
|
230
|
+
const decoder = new TextDecoder();
|
|
231
|
+
let text = "";
|
|
232
|
+
let bytes = 0;
|
|
237
233
|
try {
|
|
238
234
|
while (true) {
|
|
239
|
-
const { done, value } = await reader.read()
|
|
240
|
-
if (done) break
|
|
241
|
-
bytes += value.byteLength
|
|
235
|
+
const { done, value } = await reader.read();
|
|
236
|
+
if (done) break;
|
|
237
|
+
bytes += value.byteLength;
|
|
242
238
|
if (bytes > maxBytes) {
|
|
243
|
-
throw new Error(`${operation} response exceeded ${maxBytes} bytes`)
|
|
239
|
+
throw new Error(`${operation} response exceeded ${maxBytes} bytes`);
|
|
244
240
|
}
|
|
245
|
-
text += decoder.decode(value, { stream: true })
|
|
241
|
+
text += decoder.decode(value, { stream: true });
|
|
246
242
|
}
|
|
247
|
-
text += decoder.decode()
|
|
243
|
+
text += decoder.decode();
|
|
248
244
|
} finally {
|
|
249
|
-
await reader.cancel().catch(() => undefined)
|
|
250
|
-
reader.releaseLock()
|
|
245
|
+
await reader.cancel().catch(() => undefined);
|
|
246
|
+
reader.releaseLock();
|
|
251
247
|
}
|
|
252
|
-
return text
|
|
248
|
+
return text;
|
|
253
249
|
}
|
package/src/core/types.ts
CHANGED
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
export type ApiKeySource = "auto" | "opencode" | "env"
|
|
1
|
+
export type ApiKeySource = "auto" | "opencode" | "env";
|
|
2
2
|
|
|
3
3
|
export interface PluginOptions {
|
|
4
|
-
readonly [key: string]: unknown
|
|
4
|
+
readonly [key: string]: unknown;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
export interface SearchOptions {
|
|
8
|
-
readonly count: number
|
|
9
|
-
readonly maxResults: number
|
|
10
|
-
readonly minRerank: number
|
|
11
|
-
readonly minAuthority: number
|
|
12
|
-
readonly allowUnknownAuthority: boolean
|
|
13
|
-
readonly blockedDomains: readonly string[]
|
|
14
|
-
readonly includeDomains: readonly string[]
|
|
15
|
-
readonly language?: string
|
|
16
|
-
readonly maxSummaryChars: number
|
|
17
|
-
readonly timeoutMs: number
|
|
18
|
-
readonly enabled: boolean
|
|
8
|
+
readonly count: number;
|
|
9
|
+
readonly maxResults: number;
|
|
10
|
+
readonly minRerank: number;
|
|
11
|
+
readonly minAuthority: number;
|
|
12
|
+
readonly allowUnknownAuthority: boolean;
|
|
13
|
+
readonly blockedDomains: readonly string[];
|
|
14
|
+
readonly includeDomains: readonly string[];
|
|
15
|
+
readonly language?: string;
|
|
16
|
+
readonly maxSummaryChars: number;
|
|
17
|
+
readonly timeoutMs: number;
|
|
18
|
+
readonly enabled: boolean;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export interface FetchOptions {
|
|
22
|
-
readonly maxChars: number
|
|
23
|
-
readonly allowedDomains: readonly string[]
|
|
24
|
-
readonly blockedDomains: readonly string[]
|
|
25
|
-
readonly timeoutMs: number
|
|
26
|
-
readonly blockPrivateHosts: boolean
|
|
27
|
-
readonly enabled: boolean
|
|
22
|
+
readonly maxChars: number;
|
|
23
|
+
readonly allowedDomains: readonly string[];
|
|
24
|
+
readonly blockedDomains: readonly string[];
|
|
25
|
+
readonly timeoutMs: number;
|
|
26
|
+
readonly blockPrivateHosts: boolean;
|
|
27
|
+
readonly enabled: boolean;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export interface AuthOptions {
|
|
31
|
-
readonly source: ApiKeySource
|
|
31
|
+
readonly source: ApiKeySource;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export interface RetryOptions {
|
|
35
|
-
readonly maxRetries: number
|
|
36
|
-
readonly baseDelayMs: number
|
|
35
|
+
readonly maxRetries: number;
|
|
36
|
+
readonly baseDelayMs: number;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export interface ResolvedOptions {
|
|
40
|
-
readonly search: SearchOptions
|
|
41
|
-
readonly fetch: FetchOptions
|
|
42
|
-
readonly auth: AuthOptions
|
|
43
|
-
readonly retry: RetryOptions
|
|
40
|
+
readonly search: SearchOptions;
|
|
41
|
+
readonly fetch: FetchOptions;
|
|
42
|
+
readonly auth: AuthOptions;
|
|
43
|
+
readonly retry: RetryOptions;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export interface DeepSeekSearchResult {
|
|
47
|
-
readonly rerank_score?: number
|
|
48
|
-
readonly authority?: number
|
|
49
|
-
readonly title?: string | null
|
|
50
|
-
readonly url?: string | null
|
|
51
|
-
readonly snippet?: string | null
|
|
52
|
-
readonly summary?: string | null
|
|
53
|
-
readonly date?: string | null
|
|
47
|
+
readonly rerank_score?: number;
|
|
48
|
+
readonly authority?: number;
|
|
49
|
+
readonly title?: string | null;
|
|
50
|
+
readonly url?: string | null;
|
|
51
|
+
readonly snippet?: string | null;
|
|
52
|
+
readonly summary?: string | null;
|
|
53
|
+
readonly date?: string | null;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/** A neutral, host-agnostic search result. */
|
|
57
57
|
export interface SearchResult {
|
|
58
|
-
readonly url: string
|
|
59
|
-
readonly title?: string
|
|
60
|
-
readonly content?: string
|
|
61
|
-
readonly published?: number
|
|
58
|
+
readonly url: string;
|
|
59
|
+
readonly title?: string;
|
|
60
|
+
readonly content?: string;
|
|
61
|
+
readonly published?: number;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export interface FetchedPage {
|
|
65
|
-
readonly url: string
|
|
66
|
-
readonly title: string
|
|
67
|
-
readonly content: string
|
|
65
|
+
readonly url: string;
|
|
66
|
+
readonly title: string;
|
|
67
|
+
readonly content: string;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
export interface FormattedFetchContent {
|
|
71
|
-
readonly content: string
|
|
72
|
-
readonly truncated: boolean
|
|
71
|
+
readonly content: string;
|
|
72
|
+
readonly truncated: boolean;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
/** A fetched page formatted for the model, with truncation reported. */
|
|
76
76
|
export interface FetchedDocument {
|
|
77
|
-
readonly url: string
|
|
78
|
-
readonly title: string
|
|
79
|
-
readonly content: string
|
|
80
|
-
readonly truncated: boolean
|
|
77
|
+
readonly url: string;
|
|
78
|
+
readonly title: string;
|
|
79
|
+
readonly content: string;
|
|
80
|
+
readonly truncated: boolean;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
export interface RequestAuth {
|
|
84
|
-
readonly apiKey: string
|
|
84
|
+
readonly apiKey: string;
|
|
85
85
|
}
|
package/src/core/url.ts
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
import { MAX_URL_CHARS } from "./constants"
|
|
2
|
-
import { normalizeHostname } from "./domains"
|
|
1
|
+
import { MAX_URL_CHARS } from "./constants";
|
|
2
|
+
import { normalizeHostname } from "./domains";
|
|
3
3
|
|
|
4
4
|
export function parseFetchUrl(value: string): URL {
|
|
5
|
-
const input = value.trim()
|
|
6
|
-
if (!input) throw new Error("URL must not be empty")
|
|
5
|
+
const input = value.trim();
|
|
6
|
+
if (!input) throw new Error("URL must not be empty");
|
|
7
7
|
if (input.length > MAX_URL_CHARS) {
|
|
8
|
-
throw new Error(`URL must be ${MAX_URL_CHARS} characters or fewer`)
|
|
8
|
+
throw new Error(`URL must be ${MAX_URL_CHARS} characters or fewer`);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
let url: URL
|
|
11
|
+
let url: URL;
|
|
12
12
|
try {
|
|
13
|
-
url = new URL(input)
|
|
13
|
+
url = new URL(input);
|
|
14
14
|
} catch {
|
|
15
|
-
throw new Error("URL must be an absolute http:// or https:// URL")
|
|
15
|
+
throw new Error("URL must be an absolute http:// or https:// URL");
|
|
16
16
|
}
|
|
17
17
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
18
|
-
throw new Error("URL must use http:// or https://")
|
|
18
|
+
throw new Error("URL must use http:// or https://");
|
|
19
19
|
}
|
|
20
20
|
if (!url.hostname || !normalizeHostname(url.hostname)) {
|
|
21
|
-
throw new Error("URL must include a host")
|
|
21
|
+
throw new Error("URL must include a host");
|
|
22
22
|
}
|
|
23
23
|
if (url.username || url.password) {
|
|
24
|
-
throw new Error("URL must not contain credentials")
|
|
24
|
+
throw new Error("URL must not contain credentials");
|
|
25
25
|
}
|
|
26
|
-
return url
|
|
26
|
+
return url;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/** Normalizes a search-result URL and drops anything that is not public HTTP(S). */
|
|
30
30
|
export function normalizeResultUrl(value: unknown): string | undefined {
|
|
31
|
-
if (typeof value !== "string") return undefined
|
|
32
|
-
const input = value.trim()
|
|
33
|
-
if (!input || input.length > MAX_URL_CHARS) return undefined
|
|
31
|
+
if (typeof value !== "string") return undefined;
|
|
32
|
+
const input = value.trim();
|
|
33
|
+
if (!input || input.length > MAX_URL_CHARS) return undefined;
|
|
34
34
|
try {
|
|
35
|
-
const url = new URL(input)
|
|
36
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") return undefined
|
|
37
|
-
if (!url.hostname || url.username || url.password) return undefined
|
|
38
|
-
return url.href
|
|
35
|
+
const url = new URL(input);
|
|
36
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return undefined;
|
|
37
|
+
if (!url.hostname || url.username || url.password) return undefined;
|
|
38
|
+
return url.href;
|
|
39
39
|
} catch {
|
|
40
|
-
return undefined
|
|
40
|
+
return undefined;
|
|
41
41
|
}
|
|
42
42
|
}
|