deepseek-web-retrieval 0.4.0 → 0.5.1
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 +99 -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/domains.ts
CHANGED
|
@@ -1,30 +1,25 @@
|
|
|
1
|
-
import { isIP } from "node:net"
|
|
2
|
-
import type { FetchOptions } from "./types"
|
|
1
|
+
import { isIP } from "node:net";
|
|
2
|
+
import type { FetchOptions } from "./types";
|
|
3
3
|
|
|
4
4
|
export function normalizeDomain(value: string): string | undefined {
|
|
5
|
-
const trimmed = value.trim()
|
|
6
|
-
if (!trimmed || /\s/.test(trimmed)) return undefined
|
|
7
|
-
const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)
|
|
8
|
-
if (hasScheme && !/^https?:\/\//i.test(trimmed)) return undefined
|
|
5
|
+
const trimmed = value.trim();
|
|
6
|
+
if (!trimmed || /\s/.test(trimmed)) return undefined;
|
|
7
|
+
const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed);
|
|
8
|
+
if (hasScheme && !/^https?:\/\//i.test(trimmed)) return undefined;
|
|
9
9
|
|
|
10
|
-
let hostname: string
|
|
10
|
+
let hostname: string;
|
|
11
11
|
try {
|
|
12
12
|
// Parsing with a synthetic scheme also strips paths, ports, and userinfo.
|
|
13
|
-
hostname = new URL(hasScheme ? trimmed : `http://${trimmed}`).hostname
|
|
13
|
+
hostname = new URL(hasScheme ? trimmed : `http://${trimmed}`).hostname;
|
|
14
14
|
} catch {
|
|
15
|
-
return undefined
|
|
15
|
+
return undefined;
|
|
16
16
|
}
|
|
17
|
-
return normalizeHostname(hostname)
|
|
17
|
+
return normalizeHostname(hostname);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/** Lowercases a URL hostname and removes brackets and trailing root-label dots. */
|
|
21
21
|
export function normalizeHostname(value: string): string {
|
|
22
|
-
return value
|
|
23
|
-
.trim()
|
|
24
|
-
.toLowerCase()
|
|
25
|
-
.replace(/^\[/, "")
|
|
26
|
-
.replace(/\]$/, "")
|
|
27
|
-
.replace(/\.+$/, "")
|
|
22
|
+
return value.trim().toLowerCase().replace(/^\[/, "").replace(/\]$/, "").replace(/\.+$/, "");
|
|
28
23
|
}
|
|
29
24
|
|
|
30
25
|
/**
|
|
@@ -32,21 +27,21 @@ export function normalizeHostname(value: string): string {
|
|
|
32
27
|
* but not `notexample.com` or `example.com.evil.test`.
|
|
33
28
|
*/
|
|
34
29
|
export function domainMatches(hostname: string, domain: string): boolean {
|
|
35
|
-
const host = normalizeHostname(hostname)
|
|
36
|
-
const target = normalizeHostname(domain)
|
|
37
|
-
if (!host || !target) return false
|
|
38
|
-
return host === target || host.endsWith(`.${target}`)
|
|
30
|
+
const host = normalizeHostname(hostname);
|
|
31
|
+
const target = normalizeHostname(domain);
|
|
32
|
+
if (!host || !target) return false;
|
|
33
|
+
return host === target || host.endsWith(`.${target}`);
|
|
39
34
|
}
|
|
40
35
|
|
|
41
36
|
export function normalizeDomains(values: unknown): string[] {
|
|
42
|
-
if (!Array.isArray(values)) return []
|
|
43
|
-
const domains = new Set<string>()
|
|
37
|
+
if (!Array.isArray(values)) return [];
|
|
38
|
+
const domains = new Set<string>();
|
|
44
39
|
for (const value of values) {
|
|
45
|
-
if (typeof value !== "string") continue
|
|
46
|
-
const domain = normalizeDomain(value)
|
|
47
|
-
if (domain) domains.add(domain)
|
|
40
|
+
if (typeof value !== "string") continue;
|
|
41
|
+
const domain = normalizeDomain(value);
|
|
42
|
+
if (domain) domains.add(domain);
|
|
48
43
|
}
|
|
49
|
-
return [...domains]
|
|
44
|
+
return [...domains];
|
|
50
45
|
}
|
|
51
46
|
|
|
52
47
|
/**
|
|
@@ -55,9 +50,9 @@ export function normalizeDomains(values: unknown): string[] {
|
|
|
55
50
|
* to a remote service and fails early instead of producing a confusing 404.
|
|
56
51
|
*/
|
|
57
52
|
export function isPrivateHostname(value: string): boolean {
|
|
58
|
-
const host = normalizeHostname(value)
|
|
59
|
-
if (!host) return true
|
|
60
|
-
if (host === "localhost" || host === "local") return true
|
|
53
|
+
const host = normalizeHostname(value);
|
|
54
|
+
if (!host) return true;
|
|
55
|
+
if (host === "localhost" || host === "local") return true;
|
|
61
56
|
if (
|
|
62
57
|
host.endsWith(".localhost") ||
|
|
63
58
|
host.endsWith(".local") ||
|
|
@@ -65,48 +60,43 @@ export function isPrivateHostname(value: string): boolean {
|
|
|
65
60
|
host.endsWith(".lan") ||
|
|
66
61
|
host.endsWith(".home.arpa")
|
|
67
62
|
) {
|
|
68
|
-
return true
|
|
63
|
+
return true;
|
|
69
64
|
}
|
|
70
65
|
|
|
71
|
-
const version = isIP(host)
|
|
72
|
-
if (version === 4) return isPrivateIpv4(host)
|
|
73
|
-
if (version === 6) return isPrivateIpv6(host)
|
|
74
|
-
return false
|
|
66
|
+
const version = isIP(host);
|
|
67
|
+
if (version === 4) return isPrivateIpv4(host);
|
|
68
|
+
if (version === 6) return isPrivateIpv6(host);
|
|
69
|
+
return false;
|
|
75
70
|
}
|
|
76
71
|
|
|
77
72
|
export function assertFetchHostPolicy(
|
|
78
73
|
url: URL,
|
|
79
74
|
options: Pick<FetchOptions, "allowedDomains" | "blockedDomains" | "blockPrivateHosts">,
|
|
80
75
|
): void {
|
|
81
|
-
const host = normalizeHostname(url.hostname)
|
|
76
|
+
const host = normalizeHostname(url.hostname);
|
|
82
77
|
|
|
83
78
|
if (options.blockPrivateHosts && isPrivateHostname(host)) {
|
|
84
79
|
throw new Error(
|
|
85
80
|
`Fetch blocked for ${host}: local/private addresses are not reachable by DeepSeek. ` +
|
|
86
81
|
"Set fetchBlockPrivateHosts=false only if you really need to send the URL.",
|
|
87
|
-
)
|
|
82
|
+
);
|
|
88
83
|
}
|
|
89
84
|
|
|
90
|
-
const blocked = options.blockedDomains.find((domain) => domainMatches(host, domain))
|
|
85
|
+
const blocked = options.blockedDomains.find((domain) => domainMatches(host, domain));
|
|
91
86
|
if (blocked) {
|
|
92
|
-
throw new Error(`Fetch blocked for ${host}: matched fetchBlockedDomains entry "${blocked}"`)
|
|
87
|
+
throw new Error(`Fetch blocked for ${host}: matched fetchBlockedDomains entry "${blocked}"`);
|
|
93
88
|
}
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
!options.allowedDomains.some((domain) => domainMatches(host, domain))
|
|
97
|
-
) {
|
|
98
|
-
throw new Error(
|
|
99
|
-
`Fetch not allowed for ${host}: add it to fetchAllowedDomains (exact host or subdomain)`,
|
|
100
|
-
)
|
|
89
|
+
if (options.allowedDomains.length > 0 && !options.allowedDomains.some((domain) => domainMatches(host, domain))) {
|
|
90
|
+
throw new Error(`Fetch not allowed for ${host}: add it to fetchAllowedDomains (exact host or subdomain)`);
|
|
101
91
|
}
|
|
102
92
|
}
|
|
103
93
|
|
|
104
94
|
function isPrivateIpv4(host: string): boolean {
|
|
105
|
-
const parts = host.split(".").map((part) => Number(part))
|
|
95
|
+
const parts = host.split(".").map((part) => Number(part));
|
|
106
96
|
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
|
107
|
-
return true
|
|
97
|
+
return true;
|
|
108
98
|
}
|
|
109
|
-
const [a, b] = parts
|
|
99
|
+
const [a, b] = parts;
|
|
110
100
|
return (
|
|
111
101
|
a === 0 ||
|
|
112
102
|
a === 10 ||
|
|
@@ -117,37 +107,36 @@ function isPrivateIpv4(host: string): boolean {
|
|
|
117
107
|
(a === 192 && b === 168) ||
|
|
118
108
|
(a === 198 && (b === 18 || b === 19)) ||
|
|
119
109
|
a >= 224
|
|
120
|
-
)
|
|
110
|
+
);
|
|
121
111
|
}
|
|
122
112
|
|
|
123
113
|
function isPrivateIpv6(host: string): boolean {
|
|
124
|
-
const value = host.toLowerCase()
|
|
125
|
-
if (value === "::" || value === "::1") return true
|
|
126
|
-
if (value.startsWith("fc") || value.startsWith("fd")) return true
|
|
127
|
-
if (/^fe[89ab]/.test(value)) return true
|
|
114
|
+
const value = host.toLowerCase();
|
|
115
|
+
if (value === "::" || value === "::1") return true;
|
|
116
|
+
if (value.startsWith("fc") || value.startsWith("fd")) return true;
|
|
117
|
+
if (/^fe[89ab]/.test(value)) return true;
|
|
128
118
|
|
|
129
119
|
// IPv4-mapped IPv6, e.g. ::ffff:127.0.0.1. URL parsers normalize the dotted
|
|
130
120
|
// form to hex (`::ffff:7f00:1`), so both representations must be handled.
|
|
131
|
-
const dottedMapped = value.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/)?.[1]
|
|
132
|
-
if (dottedMapped) return isPrivateIpv4(dottedMapped)
|
|
121
|
+
const dottedMapped = value.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/)?.[1];
|
|
122
|
+
if (dottedMapped) return isPrivateIpv4(dottedMapped);
|
|
133
123
|
|
|
134
|
-
const mapped = ipv4FromIpv6Hextets(value, /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/)
|
|
135
|
-
if (mapped) return isPrivateIpv4(mapped)
|
|
124
|
+
const mapped = ipv4FromIpv6Hextets(value, /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
|
|
125
|
+
if (mapped) return isPrivateIpv4(mapped);
|
|
136
126
|
|
|
137
127
|
// IPv4-compatible IPv6, e.g. ::127.0.0.1, which URL parsers normalize to
|
|
138
128
|
// ::7f00:1.
|
|
139
|
-
const compatible = ipv4FromIpv6Hextets(value, /^::([0-9a-f]{1,4}):([0-9a-f]{1,4})$/)
|
|
140
|
-
if (compatible) return isPrivateIpv4(compatible)
|
|
129
|
+
const compatible = ipv4FromIpv6Hextets(value, /^::([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
|
|
130
|
+
if (compatible) return isPrivateIpv4(compatible);
|
|
141
131
|
|
|
142
|
-
return false
|
|
132
|
+
return false;
|
|
143
133
|
}
|
|
144
134
|
|
|
145
135
|
function ipv4FromIpv6Hextets(value: string, pattern: RegExp): string | undefined {
|
|
146
|
-
const match = value.match(pattern)
|
|
147
|
-
if (!match) return undefined
|
|
148
|
-
const high = Number.parseInt(match[1], 16)
|
|
149
|
-
const low = Number.parseInt(match[2], 16)
|
|
150
|
-
if (!Number.isFinite(high) || !Number.isFinite(low)) return undefined
|
|
151
|
-
return `${high >> 8}.${high & 0xff}.${low >> 8}.${low & 0xff}
|
|
136
|
+
const match = value.match(pattern);
|
|
137
|
+
if (!match) return undefined;
|
|
138
|
+
const high = Number.parseInt(match[1], 16);
|
|
139
|
+
const low = Number.parseInt(match[2], 16);
|
|
140
|
+
if (!Number.isFinite(high) || !Number.isFinite(low)) return undefined;
|
|
141
|
+
return `${high >> 8}.${high & 0xff}.${low >> 8}.${low & 0xff}`;
|
|
152
142
|
}
|
|
153
|
-
|
package/src/core/env.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { ENABLE_FETCH_ENV, ENABLE_SEARCH_ENV } from "./constants";
|
|
2
|
+
import { resolvePluginOptions } from "./options";
|
|
3
|
+
import type { PluginOptions, ResolvedOptions } from "./types";
|
|
4
|
+
|
|
5
|
+
/** The environment an adapter reads its key and options from. */
|
|
6
|
+
export type Env = Readonly<Record<string, string | undefined>>;
|
|
7
|
+
|
|
8
|
+
/** Each supported option mapped onto its DEEPSEEK_* variable, grouped by value type. */
|
|
9
|
+
const NUMBER_ENV = {
|
|
10
|
+
count: "DEEPSEEK_SEARCH_COUNT",
|
|
11
|
+
maxResults: "DEEPSEEK_SEARCH_MAX_RESULTS",
|
|
12
|
+
minRerank: "DEEPSEEK_SEARCH_MIN_RERANK",
|
|
13
|
+
minAuthority: "DEEPSEEK_SEARCH_MIN_AUTHORITY",
|
|
14
|
+
maxSummaryChars: "DEEPSEEK_SEARCH_MAX_SUMMARY_CHARS",
|
|
15
|
+
searchTimeoutMs: "DEEPSEEK_SEARCH_TIMEOUT_MS",
|
|
16
|
+
fetchMaxChars: "DEEPSEEK_FETCH_MAX_CHARS",
|
|
17
|
+
fetchTimeoutMs: "DEEPSEEK_FETCH_TIMEOUT_MS",
|
|
18
|
+
maxRetries: "DEEPSEEK_MAX_RETRIES",
|
|
19
|
+
retryBaseDelayMs: "DEEPSEEK_RETRY_BASE_DELAY_MS",
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
const TEXT_ENV = {
|
|
23
|
+
language: "DEEPSEEK_SEARCH_LANGUAGE",
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
const DOMAINS_ENV = {
|
|
27
|
+
blockedDomains: "DEEPSEEK_SEARCH_BLOCKED_DOMAINS",
|
|
28
|
+
includeDomains: "DEEPSEEK_SEARCH_INCLUDE_DOMAINS",
|
|
29
|
+
fetchAllowedDomains: "DEEPSEEK_FETCH_ALLOWED_DOMAINS",
|
|
30
|
+
fetchBlockedDomains: "DEEPSEEK_FETCH_BLOCKED_DOMAINS",
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
/** Shared defaults are true; only an explicit false value disables them. */
|
|
34
|
+
const FALSE_ONLY_ENV = {
|
|
35
|
+
allowUnknownAuthority: "DEEPSEEK_SEARCH_ALLOW_UNKNOWN_AUTHORITY",
|
|
36
|
+
fetchBlockPrivateHosts: "DEEPSEEK_FETCH_BLOCK_PRIVATE_HOSTS",
|
|
37
|
+
searchEnabled: ENABLE_SEARCH_ENV,
|
|
38
|
+
fetchEnabled: ENABLE_FETCH_ENV,
|
|
39
|
+
} as const;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Maps `DEEPSEEK_*` environment variables onto the shared plugin option keys and
|
|
43
|
+
* resolves them through the shared pipeline, so defaults and clamping stay
|
|
44
|
+
* identical across every adapter. Invalid values are dropped and the shared
|
|
45
|
+
* default applies.
|
|
46
|
+
*/
|
|
47
|
+
export function resolveEnvOptions(env: Env = process.env): ResolvedOptions {
|
|
48
|
+
return resolvePluginOptions(pluginOptionsFromEnv(env));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function pluginOptionsFromEnv(env: Env): PluginOptions {
|
|
52
|
+
const options: Record<string, unknown> = {};
|
|
53
|
+
|
|
54
|
+
putNumber(options, "count", env);
|
|
55
|
+
putNumber(options, "maxResults", env);
|
|
56
|
+
putNumber(options, "minRerank", env);
|
|
57
|
+
putNumber(options, "minAuthority", env);
|
|
58
|
+
putFalseOnly(options, "allowUnknownAuthority", env);
|
|
59
|
+
putDomains(options, "blockedDomains", env);
|
|
60
|
+
putDomains(options, "includeDomains", env);
|
|
61
|
+
putText(options, "language", env);
|
|
62
|
+
putNumber(options, "maxSummaryChars", env);
|
|
63
|
+
putNumber(options, "searchTimeoutMs", env);
|
|
64
|
+
|
|
65
|
+
putNumber(options, "fetchMaxChars", env);
|
|
66
|
+
putDomains(options, "fetchAllowedDomains", env);
|
|
67
|
+
putDomains(options, "fetchBlockedDomains", env);
|
|
68
|
+
putNumber(options, "fetchTimeoutMs", env);
|
|
69
|
+
putFalseOnly(options, "fetchBlockPrivateHosts", env);
|
|
70
|
+
putFalseOnly(options, "searchEnabled", env);
|
|
71
|
+
putFalseOnly(options, "fetchEnabled", env);
|
|
72
|
+
|
|
73
|
+
putNumber(options, "maxRetries", env);
|
|
74
|
+
putNumber(options, "retryBaseDelayMs", env);
|
|
75
|
+
|
|
76
|
+
return options;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function putNumber(target: Record<string, unknown>, key: keyof typeof NUMBER_ENV, env: Env): void {
|
|
80
|
+
const value = env[NUMBER_ENV[key]];
|
|
81
|
+
if (value === undefined) return;
|
|
82
|
+
const trimmed = value.trim();
|
|
83
|
+
if (!trimmed) return;
|
|
84
|
+
const parsed = Number(trimmed);
|
|
85
|
+
if (Number.isFinite(parsed)) target[key] = parsed;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function putText(target: Record<string, unknown>, key: keyof typeof TEXT_ENV, env: Env): void {
|
|
89
|
+
const value = env[TEXT_ENV[key]];
|
|
90
|
+
if (value === undefined) return;
|
|
91
|
+
const trimmed = value.trim();
|
|
92
|
+
if (trimmed) target[key] = trimmed;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function putFalseOnly(target: Record<string, unknown>, key: keyof typeof FALSE_ONLY_ENV, env: Env): void {
|
|
96
|
+
if (isFalseValue(env[FALSE_ONLY_ENV[key]])) target[key] = false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function putDomains(target: Record<string, unknown>, key: keyof typeof DOMAINS_ENV, env: Env): void {
|
|
100
|
+
const value = env[DOMAINS_ENV[key]];
|
|
101
|
+
if (value === undefined || !value.trim()) return;
|
|
102
|
+
const domains = value
|
|
103
|
+
.split(",")
|
|
104
|
+
.map((domain) => domain.trim())
|
|
105
|
+
.filter((domain) => domain.length > 0);
|
|
106
|
+
if (domains.length > 0) target[key] = domains;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isFalseValue(value: string | undefined): boolean {
|
|
110
|
+
const trimmed = value?.trim().toLowerCase();
|
|
111
|
+
return trimmed === "false" || trimmed === "0" || trimmed === "no" || trimmed === "off";
|
|
112
|
+
}
|
package/src/core/format.ts
CHANGED
|
@@ -1,18 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
MAX_ERROR_BODY_CHARS,
|
|
4
|
-
} from "./constants"
|
|
5
|
-
import type { FormattedFetchContent, SearchResult } from "./types"
|
|
1
|
+
import { FETCH_TRUNCATION_MARKER, MAX_ERROR_BODY_CHARS } from "./constants";
|
|
2
|
+
import type { FormattedFetchContent, SearchResult } from "./types";
|
|
6
3
|
|
|
7
4
|
/** Truncates by Unicode code points so emoji and surrogate pairs are not split. */
|
|
8
5
|
export function truncate(value: string, maxChars: number): string {
|
|
9
|
-
if (maxChars <= 0) return ""
|
|
10
|
-
if (value.length <= maxChars) return value
|
|
11
|
-
return Array.from(value).slice(0, maxChars).join("")
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function formatFetchContent(title: string, content: string, maxChars: number): string {
|
|
15
|
-
return truncate(withTitle(title, content), maxChars)
|
|
6
|
+
if (maxChars <= 0) return "";
|
|
7
|
+
if (value.length <= maxChars) return value;
|
|
8
|
+
return Array.from(value).slice(0, maxChars).join("");
|
|
16
9
|
}
|
|
17
10
|
|
|
18
11
|
/**
|
|
@@ -20,40 +13,36 @@ export function formatFetchContent(title: string, content: string, maxChars: num
|
|
|
20
13
|
* truncation marker counts toward `maxChars`, so the returned content never
|
|
21
14
|
* exceeds the configured budget.
|
|
22
15
|
*/
|
|
23
|
-
export function formatFetchedContent(
|
|
24
|
-
title
|
|
25
|
-
|
|
26
|
-
maxChars:
|
|
27
|
-
):
|
|
28
|
-
const document = withTitle(title, content)
|
|
29
|
-
const length = Array.from(document).length
|
|
30
|
-
if (maxChars <= 0) return { content: "", truncated: length > 0 }
|
|
31
|
-
if (length <= maxChars) return { content: document, truncated: false }
|
|
16
|
+
export function formatFetchedContent(title: string, content: string, maxChars: number): FormattedFetchContent {
|
|
17
|
+
const document = withTitle(title, content);
|
|
18
|
+
const length = Array.from(document).length;
|
|
19
|
+
if (maxChars <= 0) return { content: "", truncated: length > 0 };
|
|
20
|
+
if (length <= maxChars) return { content: document, truncated: false };
|
|
32
21
|
|
|
33
|
-
const markerLength = Array.from(FETCH_TRUNCATION_MARKER).length
|
|
22
|
+
const markerLength = Array.from(FETCH_TRUNCATION_MARKER).length;
|
|
34
23
|
if (maxChars <= markerLength) {
|
|
35
|
-
return { content: truncate(document, maxChars), truncated: true }
|
|
24
|
+
return { content: truncate(document, maxChars), truncated: true };
|
|
36
25
|
}
|
|
37
26
|
return {
|
|
38
27
|
content: `${truncate(document, maxChars - markerLength)}${FETCH_TRUNCATION_MARKER}`,
|
|
39
28
|
truncated: true,
|
|
40
|
-
}
|
|
29
|
+
};
|
|
41
30
|
}
|
|
42
31
|
|
|
43
32
|
function withTitle(title: string, content: string): string {
|
|
44
|
-
const heading = title.trim()
|
|
45
|
-
if (!heading) return content
|
|
33
|
+
const heading = title.trim();
|
|
34
|
+
if (!heading) return content;
|
|
46
35
|
|
|
47
|
-
const body = content.trimStart()
|
|
48
|
-
const lines = body.split(/\r?\n/)
|
|
49
|
-
const firstHeading = lines[0]?.match(/^\\?#\s+(.+?)\s*$/)?.[1]
|
|
36
|
+
const body = content.trimStart();
|
|
37
|
+
const lines = body.split(/\r?\n/);
|
|
38
|
+
const firstHeading = lines[0]?.match(/^\\?#\s+(.+?)\s*$/)?.[1];
|
|
50
39
|
if (firstHeading && normalizeHeading(firstHeading) === normalizeHeading(heading)) {
|
|
51
40
|
// DeepSeek sometimes escapes the leading "#" as "\#"; normalize it so the
|
|
52
41
|
// result is a real Markdown heading instead of a duplicated literal line.
|
|
53
|
-
lines[0] = `# ${heading}
|
|
54
|
-
return lines.join("\n")
|
|
42
|
+
lines[0] = `# ${heading}`;
|
|
43
|
+
return lines.join("\n");
|
|
55
44
|
}
|
|
56
|
-
return `# ${heading}\n\n${content}
|
|
45
|
+
return `# ${heading}\n\n${content}`;
|
|
57
46
|
}
|
|
58
47
|
|
|
59
48
|
function normalizeHeading(value: string): string {
|
|
@@ -61,23 +50,23 @@ function normalizeHeading(value: string): string {
|
|
|
61
50
|
.trim()
|
|
62
51
|
.replace(/\s+/g, " ")
|
|
63
52
|
.replace(/\s+#+\s*$/, "")
|
|
64
|
-
.toLowerCase()
|
|
53
|
+
.toLowerCase();
|
|
65
54
|
}
|
|
66
55
|
|
|
67
56
|
export function redactSecrets(value: string): string {
|
|
68
57
|
return value
|
|
69
58
|
.replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, "sk-***")
|
|
70
|
-
.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi, "Bearer ***")
|
|
59
|
+
.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi, "Bearer ***");
|
|
71
60
|
}
|
|
72
61
|
|
|
73
62
|
export function formatApiError(operation: string, status: number, body: string): string {
|
|
74
|
-
const detail = redactSecrets(body.trim())
|
|
75
|
-
const detailChars = Array.from(detail)
|
|
63
|
+
const detail = redactSecrets(body.trim());
|
|
64
|
+
const detailChars = Array.from(detail);
|
|
76
65
|
const text = detail
|
|
77
66
|
? detailChars.length > MAX_ERROR_BODY_CHARS
|
|
78
67
|
? `${detailChars.slice(0, MAX_ERROR_BODY_CHARS).join("")}…`
|
|
79
68
|
: detail
|
|
80
|
-
: "empty response body"
|
|
69
|
+
: "empty response body";
|
|
81
70
|
const hint =
|
|
82
71
|
status === 401
|
|
83
72
|
? "invalid API key"
|
|
@@ -87,13 +76,13 @@ export function formatApiError(operation: string, status: number, body: string):
|
|
|
87
76
|
? "permission denied"
|
|
88
77
|
: status === 429
|
|
89
78
|
? "rate limited"
|
|
90
|
-
: undefined
|
|
91
|
-
const label = hint ? `${operation} failed (HTTP ${status}, ${hint})` : `${operation} failed (HTTP ${status})
|
|
92
|
-
return `${label}: ${text}
|
|
79
|
+
: undefined;
|
|
80
|
+
const label = hint ? `${operation} failed (HTTP ${status}, ${hint})` : `${operation} failed (HTTP ${status})`;
|
|
81
|
+
return `${label}: ${text}`;
|
|
93
82
|
}
|
|
94
83
|
|
|
95
84
|
export function formatTimeoutError(operation: string, timeoutMs: number): string {
|
|
96
|
-
return `${operation} timed out after ${timeoutMs} ms
|
|
85
|
+
return `${operation} timed out after ${timeoutMs} ms`;
|
|
97
86
|
}
|
|
98
87
|
|
|
99
88
|
/**
|
|
@@ -101,27 +90,41 @@ export function formatTimeoutError(operation: string, timeoutMs: number): string
|
|
|
101
90
|
* Hosts that ignore structured output read this text.
|
|
102
91
|
*/
|
|
103
92
|
export function formatSearchResults(results: readonly SearchResult[]): string {
|
|
104
|
-
if (results.length === 0) return "No results found."
|
|
93
|
+
if (results.length === 0) return "No results found.";
|
|
105
94
|
|
|
106
95
|
return results
|
|
107
96
|
.map((result, index) => {
|
|
108
|
-
const title = result.title?.trim() || result.url
|
|
109
|
-
const lines = [`${index + 1}. [${escapeBrackets(title)}](${result.url})`]
|
|
97
|
+
const title = result.title?.trim() || result.url;
|
|
98
|
+
const lines = [`${index + 1}. [${escapeBrackets(title)}](${result.url})`];
|
|
110
99
|
if (result.content) {
|
|
111
|
-
lines.push(` ${result.content.replaceAll("\n", " ")}`)
|
|
100
|
+
lines.push(` ${result.content.replaceAll("\n", " ")}`);
|
|
112
101
|
}
|
|
113
102
|
if (result.published !== undefined) {
|
|
114
|
-
lines.push(` Published: ${formatPublished(result.published)}`)
|
|
103
|
+
lines.push(` Published: ${formatPublished(result.published)}`);
|
|
115
104
|
}
|
|
116
|
-
return lines.join("\n")
|
|
105
|
+
return lines.join("\n");
|
|
117
106
|
})
|
|
118
|
-
.join("\n\n")
|
|
107
|
+
.join("\n\n");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Renders one search result as the structured mirror of its text form; hosts
|
|
112
|
+
* that read structuredContent (every JSON-replacing client excepted) get this
|
|
113
|
+
* shape from both adapters.
|
|
114
|
+
*/
|
|
115
|
+
export function toStructuredResult(result: SearchResult) {
|
|
116
|
+
return {
|
|
117
|
+
url: result.url,
|
|
118
|
+
...(result.title ? { title: result.title } : {}),
|
|
119
|
+
...(result.content ? { content: result.content } : {}),
|
|
120
|
+
...(result.published !== undefined ? { published: result.published } : {}),
|
|
121
|
+
};
|
|
119
122
|
}
|
|
120
123
|
|
|
121
124
|
function escapeBrackets(value: string): string {
|
|
122
|
-
return value.replaceAll("[", "\\[").replaceAll("]", "\\]")
|
|
125
|
+
return value.replaceAll("[", "\\[").replaceAll("]", "\\]");
|
|
123
126
|
}
|
|
124
127
|
|
|
125
128
|
function formatPublished(epochMs: number): string {
|
|
126
|
-
return new Date(epochMs).toISOString().slice(0, 10)
|
|
129
|
+
return new Date(epochMs).toISOString().slice(0, 10);
|
|
127
130
|
}
|
package/src/core/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
export * from "./auth"
|
|
2
|
-
export * from "./client"
|
|
3
|
-
export * from "./constants"
|
|
4
|
-
export * from "./domains"
|
|
5
|
-
export * from "./format"
|
|
6
|
-
export * from "./options"
|
|
7
|
-
export * from "./search"
|
|
8
|
-
export * from "./types"
|
|
9
|
-
export * from "./url"
|
|
1
|
+
export * from "./auth";
|
|
2
|
+
export * from "./client";
|
|
3
|
+
export * from "./constants";
|
|
4
|
+
export * from "./domains";
|
|
5
|
+
export * from "./format";
|
|
6
|
+
export * from "./options";
|
|
7
|
+
export * from "./search";
|
|
8
|
+
export * from "./types";
|
|
9
|
+
export * from "./url";
|
package/src/core/jsonc.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parse } from "jsonc-parser/lib/esm/main.js"
|
|
1
|
+
import { parse } from "jsonc-parser/lib/esm/main.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Parses JSONC (JSON with comments and trailing commas) via jsonc-parser,
|
|
@@ -6,11 +6,11 @@ import { parse } from "jsonc-parser/lib/esm/main.js"
|
|
|
6
6
|
* text has errors beyond tolerated comments and trailing commas.
|
|
7
7
|
*/
|
|
8
8
|
export function parseJsonc(text: string): unknown {
|
|
9
|
-
const errors: { error: number; offset: number; length: number }[] = []
|
|
10
|
-
const value = parse(text, errors, { allowTrailingComma: true })
|
|
9
|
+
const errors: { error: number; offset: number; length: number }[] = [];
|
|
10
|
+
const value = parse(text, errors, { allowTrailingComma: true });
|
|
11
11
|
if (errors.length > 0) {
|
|
12
|
-
const first = errors[0]
|
|
13
|
-
throw new SyntaxError(`JSONC parse error at offset ${first.offset} (code ${first.error})`)
|
|
12
|
+
const first = errors[0];
|
|
13
|
+
throw new SyntaxError(`JSONC parse error at offset ${first.offset} (code ${first.error})`);
|
|
14
14
|
}
|
|
15
|
-
return value
|
|
15
|
+
return value;
|
|
16
16
|
}
|
package/src/core/options.ts
CHANGED
|
@@ -2,8 +2,8 @@ import {
|
|
|
2
2
|
DEFAULT_API_KEY_SOURCE,
|
|
3
3
|
DEFAULT_FETCH_MAX_CHARS,
|
|
4
4
|
DEFAULT_MAX_RESULTS,
|
|
5
|
-
DEFAULT_MAX_SUMMARY_CHARS,
|
|
6
5
|
DEFAULT_MAX_RETRIES,
|
|
6
|
+
DEFAULT_MAX_SUMMARY_CHARS,
|
|
7
7
|
DEFAULT_MIN_AUTHORITY,
|
|
8
8
|
DEFAULT_MIN_RERANK,
|
|
9
9
|
DEFAULT_RETRY_BASE_DELAY_MS,
|
|
@@ -13,8 +13,8 @@ import {
|
|
|
13
13
|
MAX_RETRY_DELAY_MS,
|
|
14
14
|
MAX_TIMEOUT_MS,
|
|
15
15
|
MIN_TIMEOUT_MS,
|
|
16
|
-
} from "./constants"
|
|
17
|
-
import { normalizeDomains } from "./domains"
|
|
16
|
+
} from "./constants";
|
|
17
|
+
import { normalizeDomains } from "./domains";
|
|
18
18
|
import type {
|
|
19
19
|
ApiKeySource,
|
|
20
20
|
AuthOptions,
|
|
@@ -23,7 +23,7 @@ import type {
|
|
|
23
23
|
ResolvedOptions,
|
|
24
24
|
RetryOptions,
|
|
25
25
|
SearchOptions,
|
|
26
|
-
} from "./types"
|
|
26
|
+
} from "./types";
|
|
27
27
|
|
|
28
28
|
export function resolvePluginOptions(input?: PluginOptions): ResolvedOptions {
|
|
29
29
|
return {
|
|
@@ -31,7 +31,7 @@ export function resolvePluginOptions(input?: PluginOptions): ResolvedOptions {
|
|
|
31
31
|
fetch: resolveFetchOptions(input),
|
|
32
32
|
auth: resolveAuthOptions(input),
|
|
33
33
|
retry: resolveRetryOptions(input),
|
|
34
|
-
}
|
|
34
|
+
};
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export function resolveSearchOptions(input?: PluginOptions): SearchOptions {
|
|
@@ -47,7 +47,7 @@ export function resolveSearchOptions(input?: PluginOptions): SearchOptions {
|
|
|
47
47
|
maxSummaryChars: clampInt(input?.maxSummaryChars, DEFAULT_MAX_SUMMARY_CHARS, 0, 1_000_000),
|
|
48
48
|
timeoutMs: clampInt(input?.searchTimeoutMs, DEFAULT_TIMEOUT_MS, MIN_TIMEOUT_MS, MAX_TIMEOUT_MS),
|
|
49
49
|
enabled: input?.searchEnabled !== false,
|
|
50
|
-
}
|
|
50
|
+
};
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
export function resolveFetchOptions(input?: PluginOptions): FetchOptions {
|
|
@@ -58,26 +58,21 @@ export function resolveFetchOptions(input?: PluginOptions): FetchOptions {
|
|
|
58
58
|
timeoutMs: clampInt(input?.fetchTimeoutMs, DEFAULT_TIMEOUT_MS, MIN_TIMEOUT_MS, MAX_TIMEOUT_MS),
|
|
59
59
|
blockPrivateHosts: input?.fetchBlockPrivateHosts !== false,
|
|
60
60
|
enabled: input?.fetchEnabled !== false,
|
|
61
|
-
}
|
|
61
|
+
};
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export function resolveAuthOptions(input?: PluginOptions): AuthOptions {
|
|
65
|
-
const value = input?.apiKeySource
|
|
65
|
+
const value = input?.apiKeySource;
|
|
66
66
|
return {
|
|
67
67
|
source: value === "opencode" || value === "env" ? value : (DEFAULT_API_KEY_SOURCE as ApiKeySource),
|
|
68
|
-
}
|
|
68
|
+
};
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
export function resolveRetryOptions(input?: PluginOptions): RetryOptions {
|
|
72
72
|
return {
|
|
73
73
|
maxRetries: clampInt(input?.maxRetries, DEFAULT_MAX_RETRIES, 0, MAX_RETRIES),
|
|
74
|
-
baseDelayMs: clampInt(
|
|
75
|
-
|
|
76
|
-
DEFAULT_RETRY_BASE_DELAY_MS,
|
|
77
|
-
0,
|
|
78
|
-
MAX_RETRY_DELAY_MS,
|
|
79
|
-
),
|
|
80
|
-
}
|
|
74
|
+
baseDelayMs: clampInt(input?.retryBaseDelayMs, DEFAULT_RETRY_BASE_DELAY_MS, 0, MAX_RETRY_DELAY_MS),
|
|
75
|
+
};
|
|
81
76
|
}
|
|
82
77
|
|
|
83
78
|
/**
|
|
@@ -86,23 +81,23 @@ export function resolveRetryOptions(input?: PluginOptions): RetryOptions {
|
|
|
86
81
|
*/
|
|
87
82
|
export function resolveFetchTimeoutMs(configuredMs: number, inputSeconds?: unknown): number {
|
|
88
83
|
if (typeof inputSeconds !== "number" || !Number.isFinite(inputSeconds) || inputSeconds <= 0) {
|
|
89
|
-
return configuredMs
|
|
84
|
+
return configuredMs;
|
|
90
85
|
}
|
|
91
|
-
const inputMs = Math.min(MAX_TIMEOUT_MS, Math.max(1, Math.round(inputSeconds * 1000)))
|
|
92
|
-
return Math.min(configuredMs, inputMs)
|
|
86
|
+
const inputMs = Math.min(MAX_TIMEOUT_MS, Math.max(1, Math.round(inputSeconds * 1000)));
|
|
87
|
+
return Math.min(configuredMs, inputMs);
|
|
93
88
|
}
|
|
94
89
|
|
|
95
90
|
function optionalText(value: unknown): string | undefined {
|
|
96
|
-
if (typeof value !== "string") return undefined
|
|
97
|
-
const trimmed = value.trim()
|
|
98
|
-
return trimmed ? trimmed : undefined
|
|
91
|
+
if (typeof value !== "string") return undefined;
|
|
92
|
+
const trimmed = value.trim();
|
|
93
|
+
return trimmed ? trimmed : undefined;
|
|
99
94
|
}
|
|
100
95
|
|
|
101
96
|
function finiteNumber(value: unknown, fallback: number): number {
|
|
102
|
-
return typeof value === "number" && Number.isFinite(value) ? value : fallback
|
|
97
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
103
98
|
}
|
|
104
99
|
|
|
105
100
|
function clampInt(value: unknown, fallback: number, min: number, max: number): number {
|
|
106
|
-
const number = typeof value === "number" && Number.isFinite(value) ? Math.trunc(value) : fallback
|
|
107
|
-
return Math.min(max, Math.max(min, number))
|
|
101
|
+
const number = typeof value === "number" && Number.isFinite(value) ? Math.trunc(value) : fallback;
|
|
102
|
+
return Math.min(max, Math.max(min, number));
|
|
108
103
|
}
|