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.
Files changed (61) hide show
  1. package/CHANGELOG.md +99 -244
  2. package/README.md +16 -11
  3. package/dist/cli.js +1366 -615
  4. package/docs/agents/codex.md +28 -7
  5. package/docs/agents/opencode-v2.md +1 -1
  6. package/docs/agents/pi.md +88 -86
  7. package/docs/design/installer-design.md +72 -9
  8. package/docs/hosts.jsonc +2 -2
  9. package/docs/integration.md +14 -9
  10. package/docs/research/control-matrix.md +48 -13
  11. package/docs/research/mcp-compatibility.md +9 -4
  12. package/examples/pi.settings.jsonc +17 -0
  13. package/index.ts +2 -2
  14. package/package.json +35 -4
  15. package/src/core/auth.ts +4 -6
  16. package/src/core/client.ts +65 -53
  17. package/src/core/constants.ts +24 -24
  18. package/src/core/descriptions.ts +30 -6
  19. package/src/core/domains.ts +56 -67
  20. package/src/core/env.ts +112 -0
  21. package/src/core/format.ts +53 -50
  22. package/src/core/index.ts +9 -9
  23. package/src/core/jsonc.ts +6 -6
  24. package/src/core/object.ts +4 -0
  25. package/src/core/options.ts +20 -25
  26. package/src/core/search.ts +45 -44
  27. package/src/core/transport.ts +109 -113
  28. package/src/core/types.ts +47 -47
  29. package/src/core/url.ts +20 -20
  30. package/src/installer/apply.ts +256 -81
  31. package/src/installer/cli.ts +135 -104
  32. package/src/installer/detect.ts +46 -31
  33. package/src/installer/formats/index.ts +26 -28
  34. package/src/installer/formats/json.ts +81 -77
  35. package/src/installer/formats/rules.ts +55 -28
  36. package/src/installer/formats/toml.ts +88 -81
  37. package/src/installer/formats/value.ts +34 -30
  38. package/src/installer/hosts/codex.ts +100 -54
  39. package/src/installer/hosts/index.ts +126 -41
  40. package/src/installer/hosts/opencode.ts +70 -51
  41. package/src/installer/hosts/pi.ts +269 -75
  42. package/src/installer/hosts/shared.ts +19 -14
  43. package/src/installer/operation-lock.ts +54 -0
  44. package/src/installer/ownership.ts +129 -0
  45. package/src/installer/paths.ts +7 -5
  46. package/src/installer/persistence.ts +66 -0
  47. package/src/installer/render.ts +29 -29
  48. package/src/installer/revert.ts +33 -29
  49. package/src/installer/rulesContent.ts +24 -13
  50. package/src/installer/state.ts +116 -31
  51. package/src/installer/types.ts +74 -56
  52. package/src/installer/uninstall.ts +40 -17
  53. package/src/mcp/auth.ts +7 -8
  54. package/src/mcp/cli.ts +38 -20
  55. package/src/mcp/client.ts +35 -0
  56. package/src/mcp/server.ts +68 -97
  57. package/src/opencode/auth.ts +24 -26
  58. package/src/opencode/plugin.ts +99 -87
  59. package/src/pi/extension.ts +164 -0
  60. package/src/pi/settings.ts +49 -0
  61. package/src/mcp/config.ts +0 -112
@@ -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
- options.allowedDomains.length > 0 &&
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
-
@@ -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
+ }
@@ -1,18 +1,11 @@
1
- import {
2
- FETCH_TRUNCATION_MARKER,
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: string,
25
- content: string,
26
- maxChars: number,
27
- ): FormattedFetchContent {
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
  }
@@ -0,0 +1,4 @@
1
+ /** Shared plain-object guard: core modules and the installer's value helpers agree on one. */
2
+ export function isPlainObject(value: unknown): value is Record<string, unknown> {
3
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4
+ }
@@ -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
- input?.retryBaseDelayMs,
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
  }