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
@@ -79,14 +79,19 @@ paths imply the available scopes; keys locked to a single scope are enumerated i
79
79
 
80
80
  - **Positioning** — VSCode-extension agent with an experimental lightweight CLI (`clite`,
81
81
  built on the Cline SDKs). Apache-2.0. MCP plus CLI/SDK custom tools; neither replaces the
82
- built-in `fetch_web_content`. Current docs.cline.bot describes the newer 3.x `cline`
83
- binary — the lock is 0.0.13 `clite`, and everything here is verified against that binary.
82
+ built-in `fetch_web_content`. `@cline/cli` is frozen at 0.0.13 (last publish 2026-05-11);
83
+ the next day the unscoped `cline` package superseded it (3.0.0 on 2026-05-12; 3.0.73 at
84
+ 2026-10-21; binary renamed `clite` → `cline`), and docs.cline.bot now describes only the
85
+ `cline` binary (re-verified 2026-10-21). The lock stays 0.0.13 `clite` and everything
86
+ here is verified against that binary; adopting 3.x means re-verifying this whole row.
84
87
  - **Built-in web tools** — `fetch_web_content` (fetch a URL and analyze it with a prompt);
85
88
  no built-in web search (the binary's `web_search` strings are Bedrock provider tool
86
89
  versions). Kill switch: `"disabledTools": ["fetch_web_content"]` in
87
90
  `~/.cline/data/settings/global-settings.json` (path overridable via
88
91
  `CLINE_GLOBAL_SETTINGS_PATH`); a disabled tool is dropped from the available set.
89
- `--yolo` mode also deactivates web fetch.
92
+ `--yolo` mode also deactivates web fetch. Current docs.cline.bot documents the SDK's
93
+ `toolPolicies: { enabled: false }` as the disable mechanism and no longer mentions the
94
+ `disabledTools` key, which still exists in the settings file (re-verified 2026-10-21).
90
95
  - **MCP registration** — `~/.cline/settings/cline_mcp_settings.json`
91
96
  (`CLINE_MCP_SETTINGS_PATH`), written by the `clite mcp` wizard in a nested form:
92
97
  `mcpServers.<name>.transport` with `type` (`stdio` | `sse` | `streamableHttp`) plus
@@ -108,11 +113,25 @@ paths imply the available scopes; keys locked to a single scope are enumerated i
108
113
  <bool>` form parses but discards the value at 0.163.0 — a silent no-op
109
114
  ([config source at rust-v0.163.0](https://github.com/openai/codex/blob/rust-v0.163.0/codex-rs/config/src/config_toml.rs)).
110
115
  - **MCP registration** — `~/.codex/config.toml` `[mcp_servers.<name>]`: stdio
111
- `command`/`args`/`env`/`env_vars`; `startup_timeout_sec` (10 s), `tool_timeout_sec`
112
- (60 s); `enabled_tools`/`disabled_tools`, per-tool approval modes, and output limits.
116
+ `command`/`args`/`env`/`env_vars`; `startup_timeout_sec` (30 s) and `tool_timeout_sec`
117
+ (300 s) defaults — the `DEFAULT_STARTUP_TIMEOUT`/`DEFAULT_TOOL_TIMEOUT` constants
118
+ ([rmcp_client.rs at rust-v0.163.0](https://github.com/openai/codex/blob/rust-v0.163.0/codex-rs/codex-mcp/src/rmcp_client.rs));
119
+ `enabled_tools`/`disabled_tools` (verbatim raw tool-name match, filtered before
120
+ registration so the model never sees a disabled tool; an empty `enabled_tools` allow-list
121
+ exposes nothing), per-tool approval modes, and output limits.
113
122
  stdio children do not inherit the parent environment (only `HOME`, `PATH`, and explicit
114
123
  `env`), so `DEEPSEEK_API_KEY` must be passed explicitly. When a tool result has
115
124
  `structuredContent`, Codex replaces the content blocks with the serialized JSON. No SSE.
125
+ Codex identifies itself in the initialize handshake as clientInfo name `codex-mcp-client`
126
+ with title `Codex`
127
+ ([rmcp_client.rs at rust-v0.163.0](https://github.com/openai/codex/blob/rust-v0.163.0/codex-rs/codex-mcp/src/rmcp_client.rs)).
128
+ The official docs moved to learn.chatgpt.com
129
+ ([MCP](https://learn.chatgpt.com/docs/extend/mcp),
130
+ [config reference](https://learn.chatgpt.com/docs/config-file/config-reference); the old
131
+ developers.openai.com URLs 308-redirect there) and now document every key above — but
132
+ state 10 s/60 s timeout defaults, which describe an older release line; the 30 s/300 s
133
+ constants above are the facts at the locked 0.163.0 (docs discrepancy re-verified
134
+ 2026-10-21).
116
135
  - **Rules** — `~/.codex/AGENTS.md` (with `AGENTS.override.md` preferred) at user scope;
117
136
  every `AGENTS.md` from the project root down to the cwd, concatenated root→cwd.
118
137
  Extensible via `project_doc_fallback_filenames` and `developer_instructions`
@@ -136,12 +155,25 @@ paths imply the available scopes; keys locked to a single scope are enumerated i
136
155
 
137
156
  ### Pi
138
157
 
139
- - **Positioning** — minimal, fully customizable agent (earendil-works). MIT. MCP server
140
- plus a TypeScript extension API. See [Pi](../agents/pi.md).
158
+ - **Positioning** — minimal, fully customizable agent (earendil-works). MIT. A TypeScript
159
+ extension API plus MCP. See [Pi](../agents/pi.md); the shipped adapter is the native
160
+ extension (`src/pi/`), with the MCP server as the fallback path.
141
161
  - **Built-in web tools** — none at 1.3.0. The built-in set is exactly `bash, edit, find,
142
162
  grep, ls, powershell, read, write` (default-active: `read, bash, edit, write`); web
143
163
  search/fetch arrive via MCP or extensions
144
164
  ([tools index at v1.3.0](https://github.com/earendil-works/pi/blob/v1.3.0/packages/coding-agent/src/core/tools/index.ts)).
165
+ - **Extension registration** — `packages` in settings.json (`~/.pi/agent/settings.json` or
166
+ `.pi/settings.json`): source strings (`npm:<pkg>`, `git:…`, local paths) resolved at
167
+ session start; `pi install <source>` appends them, and a package's `pi` manifest field
168
+ names its entry files (loaded via jiti, TypeScript directly). Extension tools default to
169
+ `exposure: "direct"` — no codemode trap — and run in-process: the key is read from
170
+ `process.env` (no forwarding), and results bypass the MCP layer entirely, so neither the
171
+ 20 KB truncation nor the 60 s request timeout applies. There is no per-extension options
172
+ channel: the runtime config surface is the `DEEPSEEK_*` environment (the same pipeline as
173
+ the MCP server) plus a `deepseekWebRetrieval` settings namespace the installer writes for
174
+ the off tier — Pi preserves unknown settings keys on read and on its own rewrites
175
+ (`pi install`), verified at 1.3.0. Action methods such as `getSettings()` cannot be
176
+ called during extension loading, so the adapter reads the settings files itself.
145
177
  - **MCP registration** — `~/.pi/agent/mcp.json` or `.pi/mcp.json`: stdio
146
178
  `command`/`args`/`env`/`cwd`, HTTP `url`/`headers`, plus `timeout`, `enabled`,
147
179
  `exposure`. MCP tools are hidden from the model by default
@@ -237,7 +269,9 @@ paths imply the available scopes; keys locked to a single scope are enumerated i
237
269
  - **Rules** — `~/.copilot/copilot-instructions.md` (user),
238
270
  `.github/copilot-instructions.md` (repo) and `.github/instructions/**/*.instructions.md`
239
271
  (path-scoped via `applyTo` globs); `AGENTS.md`/`CLAUDE.md` discovery (changelog-dated
240
- ≤1.0.86; `GEMINI.md` could not be dated to the lock);
272
+ ≤1.0.86); `GEMINI.md` is never named in the changelog but the official docs have listed
273
+ it since the page's creation on 2026-02-16 (docs-history-dated, 0.0.41x era — comfortably
274
+ ≤1.0.98; re-verified 2026-10-21); `.claude/rules` support is changelog-dated 1.0.89;
241
275
  `COPILOT_CUSTOM_INSTRUCTIONS_DIRS` adds directories
242
276
  ([custom instructions](https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-custom-instructions)).
243
277
 
@@ -250,8 +284,9 @@ paths imply the available scopes; keys locked to a single scope are enumerated i
250
284
  - **Replacement is exclusive to native extension points** — only OpenCode. Exclusion is
251
285
  the general forced path on MCP hosts. The two structural limits: OpenCode search has no
252
286
  middle tier, and Cursor search cannot be forced at all.
253
- - **Version drift to re-verify on the next baseline pass:** Cline's docs describe the 3.x
254
- `cline` binary while the lock is 0.0.13 `clite`; Codex's config docs have moved and
255
- describe a newer release; Copilot's `GEMINI.md` support could not be changelog-dated to
256
- ≤1.0.98. Protocol-revision evidence for every host lives in
257
- [mcp-compatibility.md](mcp-compatibility.md).
287
+ - **Version drift to re-verify on the next baseline pass:** Cline's `cline` 3.x package
288
+ (3.0.73 at 2026-10-21) has superseded the locked 0.0.13 `clite`; adopting it means
289
+ re-verifying that host's whole row (the documented kill switch already differs: SDK
290
+ `toolPolicies`). Codex's docs location and Copilot's `GEMINI.md` dating were re-verified
291
+ 2026-10-21 and are settled in their sections above. Protocol-revision evidence for every
292
+ host lives in [mcp-compatibility.md](mcp-compatibility.md).
@@ -107,12 +107,17 @@ differences:
107
107
  capability; do not rely on resources, prompts, sampling, elicitation, or roots — the
108
108
  hosts' client capabilities are inconsistent (Codex has elicitation only; Pi serves
109
109
  roots). Always return readable
110
- text in `content` blocks, and mirror the same data in `structuredContent` for
111
- `2025-06-18`+ clients — the spec's own backwards-compat SHOULD. This split matters in
112
- practice: Codex *replaces* content with `structuredContent` when present; Cline reads
110
+ text in `content` blocks, and mirror the same data in `structuredContent` — the
111
+ spec's own backwards-compat SHOULD — except for clients that would replace the text
112
+ with its serialized JSON: Codex *replaces* content with `structuredContent` when
113
+ present, so the shipped server detects its `codex-mcp-client` identity (the
114
+ initialize `clientInfo` on handshake-era connections, the per-request envelope on
115
+ `2026-07-28`) and omits the mirror for it. Cline reads
113
116
  text content only; Pi is
114
117
  content-first, falling back to `structuredContent` only when a result has no content
115
- blocks (verified in `tools.ts` at v1.3.0).
118
+ blocks (verified in `tools.ts` at v1.3.0). No `outputSchema` is declared: the SDK
119
+ rejects text-only results for a tool with a declared schema, and no host in the
120
+ matrix validates the schema anyway.
116
121
  2. **stdio is the universal transport.** Every host in the matrix launches stdio servers
117
122
  with the same conceptual JSON (`mcpServers` → `command`/`args`/`env`), differing only in
118
123
  file location and cosmetic field names. stdio needs no OAuth and no URL config, and the
@@ -0,0 +1,17 @@
1
+ {
2
+ // Pi extension config: ~/.pi/agent/settings.json (user) or .pi/settings.json (project).
3
+ // "packages" sources resolve at session start; the first launch fetches
4
+ // npm:deepseek-web-retrieval from the registry (pi update --extensions refreshes it).
5
+ // The key never enters this file: the extension reads DEEPSEEK_API_KEY from the
6
+ // environment of the Pi process itself.
7
+ "packages": [
8
+ "npm:deepseek-web-retrieval"
9
+ ],
10
+ // Optional: the off switches the installer writes; they outrank the
11
+ // DEEPSEEK_ENABLE_* environment variables. Pi preserves unknown keys like
12
+ // this namespace on read and on its own rewrites.
13
+ "deepseekWebRetrieval": {
14
+ // "enableSearch": false,
15
+ // "enableFetch": false
16
+ }
17
+ }
package/index.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { default } from "./src/opencode/plugin"
2
- export * from "./src/core/index"
1
+ export * from "./src/core/index";
2
+ export { default } from "./src/opencode/plugin";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "deepseek-web-retrieval",
4
- "version": "0.4.0",
4
+ "version": "0.5.1",
5
5
  "description": "Token-efficient DeepSeek Search and Fetch for coding agents, starting with OpenCode v2.",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -14,6 +14,8 @@
14
14
  "keywords": [
15
15
  "opencode",
16
16
  "opencode-plugin",
17
+ "pi",
18
+ "pi-extension",
17
19
  "deepseek",
18
20
  "websearch",
19
21
  "webfetch",
@@ -23,7 +25,13 @@
23
25
  "exports": {
24
26
  ".": "./index.ts",
25
27
  "./core": "./src/core/index.ts",
26
- "./mcp": "./src/mcp/server.ts"
28
+ "./mcp": "./src/mcp/server.ts",
29
+ "./pi": "./src/pi/extension.ts"
30
+ },
31
+ "pi": {
32
+ "extensions": [
33
+ "./src/pi/extension.ts"
34
+ ]
27
35
  },
28
36
  "bin": {
29
37
  "deepseek-web-retrieval": "dist/cli.js"
@@ -39,19 +47,42 @@
39
47
  "examples"
40
48
  ],
41
49
  "scripts": {
50
+ "lint": "biome check --error-on-warnings .",
51
+ "lint:ci": "biome ci --error-on-warnings .",
52
+ "lint:fix": "biome check --write --error-on-warnings .",
53
+ "format": "biome format --write .",
42
54
  "typecheck": "tsc --noEmit",
43
55
  "build": "bun build src/mcp/cli.ts --target=node --format=esm --outdir=dist --external @modelcontextprotocol/server --external @modelcontextprotocol/server/stdio --external zod --external zod/v4",
44
- "prepublishOnly": "bun run typecheck && bun run build",
56
+ "prepublishOnly": "bun run check:release",
45
57
  "test": "bun test",
58
+ "test:package": "node scripts/test-package.mjs",
59
+ "test:e2e:keyless": "bun run test:e2e pi pi-extension installer reinit upgrade pkg capability-off",
60
+ "check:release": "bun run check && bun run test:package && bun run test:e2e:keyless",
46
61
  "test:e2e": "bun run test/e2e/run.ts",
47
62
  "docs:matrix": "bun run scripts/render-matrix.ts",
48
63
  "test:coverage": "bun test --coverage",
49
- "check": "bun run typecheck && bun run docs:matrix --check && bun run build && bun test"
64
+ "check": "bun run lint && bun run typecheck && bun run docs:matrix --check && bun run build && bun test"
50
65
  },
51
66
  "peerDependencies": {
67
+ "@earendil-works/pi-ai": "^1.3.0",
68
+ "@earendil-works/pi-coding-agent": "^1.3.0",
52
69
  "@opencode/plugin": "^2.0.32"
53
70
  },
71
+ "peerDependenciesMeta": {
72
+ "@earendil-works/pi-ai": {
73
+ "optional": true
74
+ },
75
+ "@earendil-works/pi-coding-agent": {
76
+ "optional": true
77
+ },
78
+ "@opencode/plugin": {
79
+ "optional": true
80
+ }
81
+ },
54
82
  "devDependencies": {
83
+ "@biomejs/biome": "2.5.15",
84
+ "@earendil-works/pi-ai": "1.3.0",
85
+ "@earendil-works/pi-coding-agent": "1.3.0",
55
86
  "@modelcontextprotocol/client": "^2.3.1",
56
87
  "@opencode/plugin": "2.0.32",
57
88
  "@types/bun": "1.4.0",
package/src/core/auth.ts CHANGED
@@ -1,8 +1,6 @@
1
- import { API_KEY_ENV } from "./constants"
1
+ import { API_KEY_ENV } from "./constants";
2
2
 
3
- export function apiKeyFromEnv(
4
- env: Readonly<Record<string, string | undefined>> = process.env,
5
- ): string | undefined {
6
- const key = env[API_KEY_ENV]?.trim()
7
- return key ? key : undefined
3
+ export function apiKeyFromEnv(env: Readonly<Record<string, string | undefined>> = process.env): string | undefined {
4
+ const key = env[API_KEY_ENV]?.trim();
5
+ return key ? key : undefined;
8
6
  }
@@ -1,9 +1,8 @@
1
- import { FETCH_ENDPOINT, MAX_QUERY_CHARS, SEARCH_ENDPOINT } from "./constants"
2
- import { assertFetchHostPolicy } from "./domains"
3
- import { formatFetchedContent } from "./format"
4
- import { buildQuery, processResults } from "./search"
5
- import { postJson } from "./transport"
6
- import { parseFetchUrl } from "./url"
1
+ import { FETCH_ENDPOINT, MAX_QUERY_CHARS, SEARCH_ENDPOINT } from "./constants";
2
+ import { assertFetchHostPolicy } from "./domains";
3
+ import { formatFetchedContent } from "./format";
4
+ import { buildQuery, processResults } from "./search";
5
+ import { postJson } from "./transport";
7
6
  import type {
8
7
  DeepSeekSearchResult,
9
8
  FetchedDocument,
@@ -13,30 +12,33 @@ import type {
13
12
  RetryOptions,
14
13
  SearchOptions,
15
14
  SearchResult,
16
- } from "./types"
15
+ } from "./types";
16
+ import { parseFetchUrl } from "./url";
17
17
 
18
- export { DeepSeekApiError } from "./transport"
18
+ export { DeepSeekApiError } from "./transport";
19
19
 
20
20
  /** The shorter of the configured and per-call timeout always wins. */
21
21
  function effectiveTimeout(configuredMs: number, perCallMs: number | undefined): number {
22
- return perCallMs === undefined ? configuredMs : Math.min(configuredMs, perCallMs)
22
+ return perCallMs === undefined ? configuredMs : Math.min(configuredMs, perCallMs);
23
23
  }
24
24
 
25
25
  interface SearchResponse {
26
- readonly results?: unknown
26
+ readonly results?: unknown;
27
27
  }
28
28
 
29
29
  interface FetchResponse {
30
- readonly title?: unknown
31
- readonly content?: unknown
30
+ readonly title?: unknown;
31
+ readonly content?: unknown;
32
32
  }
33
33
 
34
34
  export interface RequestOptions {
35
- readonly signal: AbortSignal
36
- readonly auth: RequestAuth
37
- readonly retry: RetryOptions
35
+ /** Optional per-request HTTP dependency; factories bind this once for an adapter. */
36
+ readonly fetchImpl?: typeof fetch;
37
+ readonly signal: AbortSignal;
38
+ readonly auth: RequestAuth;
39
+ readonly retry: RetryOptions;
38
40
  /** Per-call cap in milliseconds; the shorter of this and the configured timeout wins. */
39
- readonly timeoutMs?: number
41
+ readonly timeoutMs?: number;
40
42
  }
41
43
 
42
44
  export async function searchWeb(
@@ -44,23 +46,23 @@ export async function searchWeb(
44
46
  options: SearchOptions,
45
47
  request: RequestOptions,
46
48
  ): Promise<SearchResult[]> {
47
- const trimmed = query.trim()
48
- if (!trimmed) throw new Error("DeepSeek search query must not be empty")
49
+ const trimmed = query.trim();
50
+ if (!trimmed) throw new Error("DeepSeek search query must not be empty");
49
51
  if (Array.from(trimmed).length > MAX_QUERY_CHARS) {
50
- throw new Error(`DeepSeek search query must be ${MAX_QUERY_CHARS} characters or fewer`)
52
+ throw new Error(`DeepSeek search query must be ${MAX_QUERY_CHARS} characters or fewer`);
51
53
  }
52
54
 
53
- const builtQuery = buildQuery(trimmed, options)
54
- const builtQueryLength = Array.from(builtQuery).length
55
+ const builtQuery = buildQuery(trimmed, options);
56
+ const builtQueryLength = Array.from(builtQuery).length;
55
57
  if (builtQueryLength > MAX_QUERY_CHARS) {
56
58
  throw new Error(
57
59
  `DeepSeek search query must be ${MAX_QUERY_CHARS} characters or fewer after applying ` +
58
60
  `blockedDomains, includeDomains, and language ` +
59
61
  `(built query is ${builtQueryLength} characters)`,
60
- )
62
+ );
61
63
  }
62
64
 
63
- const timeout = effectiveTimeout(options.timeoutMs, request.timeoutMs)
65
+ const timeout = effectiveTimeout(options.timeoutMs, request.timeoutMs);
64
66
  const payload = await postJson<SearchResponse>(
65
67
  SEARCH_ENDPOINT,
66
68
  {
@@ -69,43 +71,38 @@ export async function searchWeb(
69
71
  need_content: false,
70
72
  },
71
73
  "DeepSeek Search",
72
- { signal: request.signal, auth: request.auth, retry: request.retry, timeoutMs: timeout },
73
- )
74
+ { ...request, timeoutMs: timeout },
75
+ );
74
76
 
75
77
  if (!Array.isArray(payload.results)) {
76
- throw new Error("DeepSeek Search returned a response without a results array")
78
+ throw new Error("DeepSeek Search returned a response without a results array");
77
79
  }
78
- return processResults(payload.results as DeepSeekSearchResult[], options)
80
+ return processResults(payload.results as DeepSeekSearchResult[], options);
79
81
  }
80
82
 
81
- export async function fetchPage(
82
- url: URL,
83
- options: FetchOptions,
84
- request: RequestOptions,
85
- ): Promise<FetchedPage> {
86
- // Enforced here so direct core consumers cannot bypass the policy. Adapters
87
- // may also pre-check to reject before a permission prompt.
88
- assertFetchHostPolicy(url, options)
89
- const timeout = effectiveTimeout(options.timeoutMs, request.timeoutMs)
90
- const payload = await postJson<FetchResponse>(
91
- FETCH_ENDPOINT,
92
- { url: url.href },
93
- "DeepSeek Fetch",
94
- { signal: request.signal, auth: request.auth, retry: request.retry, timeoutMs: timeout },
95
- )
96
- const title = typeof payload.title === "string" ? payload.title.trim() : ""
97
- const content = typeof payload.content === "string" ? payload.content : ""
83
+ export async function fetchPage(url: URL, options: FetchOptions, request: RequestOptions): Promise<FetchedPage> {
84
+ // A URL object is parsed, not necessarily safe: direct core callers must pass
85
+ // the same protocol, credentials, length and host checks as string callers.
86
+ const target = parseFetchUrl(url.href);
87
+ assertFetchHostPolicy(target, options);
88
+ const timeout = effectiveTimeout(options.timeoutMs, request.timeoutMs);
89
+ const payload = await postJson<FetchResponse>(FETCH_ENDPOINT, { url: target.href }, "DeepSeek Fetch", {
90
+ ...request,
91
+ timeoutMs: timeout,
92
+ });
93
+ const title = typeof payload.title === "string" ? payload.title.trim() : "";
94
+ const content = typeof payload.content === "string" ? payload.content : "";
98
95
  if (!content.trim()) {
99
- throw new Error("DeepSeek Fetch returned no page content (the page may be empty or unavailable)")
96
+ throw new Error("DeepSeek Fetch returned no page content (the page may be empty or unavailable)");
100
97
  }
101
- return { url: url.href, title, content }
98
+ return { url: target.href, title, content };
102
99
  }
103
100
 
104
101
  /** Parses the URL and enforces the host policy in one call, returning the fetch target. */
105
102
  export function inspectFetchTarget(url: string, options: FetchOptions): URL {
106
- const target = parseFetchUrl(url)
107
- assertFetchHostPolicy(target, options)
108
- return target
103
+ const target = parseFetchUrl(url);
104
+ assertFetchHostPolicy(target, options);
105
+ return target;
109
106
  }
110
107
 
111
108
  /**
@@ -117,13 +114,28 @@ export async function fetchDocument(
117
114
  options: FetchOptions,
118
115
  request: RequestOptions,
119
116
  ): Promise<FetchedDocument> {
120
- if (typeof url !== "string" && !(url instanceof URL)) throw new Error('"url" must be a string or URL')
121
- const page = await fetchPage(url instanceof URL ? url : parseFetchUrl(url), options, request)
122
- const formatted = formatFetchedContent(page.title, page.content, options.maxChars)
117
+ if (typeof url !== "string" && !(url instanceof URL)) throw new Error('"url" must be a string or URL');
118
+ const page = await fetchPage(url instanceof URL ? url : parseFetchUrl(url), options, request);
119
+ const formatted = formatFetchedContent(page.title, page.content, options.maxChars);
123
120
  return {
124
121
  url: page.url,
125
122
  title: page.title,
126
123
  content: formatted.content,
127
124
  truncated: formatted.truncated,
128
- }
125
+ };
129
126
  }
127
+
128
+ /** One client per adapter/test; transport overrides never mutate process globals. */
129
+ export function createRetrievalClient(dependencies: { readonly fetchImpl?: typeof fetch } = {}) {
130
+ const bind = (request: RequestOptions): RequestOptions => ({ ...request, ...dependencies });
131
+ return {
132
+ searchWeb: (query: string, options: SearchOptions, request: RequestOptions) =>
133
+ searchWeb(query, options, bind(request)),
134
+ fetchPage: (url: URL, options: FetchOptions, request: RequestOptions) => fetchPage(url, options, bind(request)),
135
+ fetchDocument: (url: string | URL, options: FetchOptions, request: RequestOptions) =>
136
+ fetchDocument(url, options, bind(request)),
137
+ };
138
+ }
139
+
140
+ export type RetrievalClient = ReturnType<typeof createRetrievalClient>;
141
+ export const retrievalClient = createRetrievalClient();
@@ -1,5 +1,5 @@
1
- export const SEARCH_ENDPOINT = "https://api.deepseek.com/v1/tools/search"
2
- export const FETCH_ENDPOINT = "https://api.deepseek.com/v1/tools/fetch"
1
+ export const SEARCH_ENDPOINT = "https://api.deepseek.com/v1/tools/search";
2
+ export const FETCH_ENDPOINT = "https://api.deepseek.com/v1/tools/fetch";
3
3
 
4
4
  /**
5
5
  * Search defaults are benchmarked against surveyed AI-native search APIs and
@@ -7,30 +7,30 @@ export const FETCH_ENDPOINT = "https://api.deepseek.com/v1/tools/fetch"
7
7
  * Result caps cluster at 10 across vendors, and no surveyed vendor filters
8
8
  * results client-side by score or authority — thresholds stay opt-in.
9
9
  */
10
- export const DEFAULT_SEARCH_COUNT = 10
11
- export const DEFAULT_MAX_RESULTS = 10
12
- export const DEFAULT_MIN_RERANK = 0
13
- export const DEFAULT_MIN_AUTHORITY = 0
14
- export const DEFAULT_MAX_SUMMARY_CHARS = 1200
10
+ export const DEFAULT_SEARCH_COUNT = 10;
11
+ export const DEFAULT_MAX_RESULTS = 10;
12
+ export const DEFAULT_MIN_RERANK = 0;
13
+ export const DEFAULT_MIN_AUTHORITY = 0;
14
+ export const DEFAULT_MAX_SUMMARY_CHARS = 1200;
15
15
 
16
- export const DEFAULT_FETCH_MAX_CHARS = 16000
17
- export const DEFAULT_TIMEOUT_MS = 30000
18
- export const MIN_TIMEOUT_MS = 1000
19
- export const MAX_TIMEOUT_MS = 120000
16
+ export const DEFAULT_FETCH_MAX_CHARS = 16000;
17
+ export const DEFAULT_TIMEOUT_MS = 30000;
18
+ export const MIN_TIMEOUT_MS = 1000;
19
+ export const MAX_TIMEOUT_MS = 120000;
20
20
 
21
- export const MAX_QUERY_CHARS = 128
22
- export const MAX_URL_CHARS = 8192
23
- export const MAX_ERROR_BODY_CHARS = 2000
24
- export const MAX_RESPONSE_BYTES = 5 * 1024 * 1024
25
- export const FETCH_TRUNCATION_MARKER = "\n\n[content truncated]"
21
+ export const MAX_QUERY_CHARS = 128;
22
+ export const MAX_URL_CHARS = 8192;
23
+ export const MAX_ERROR_BODY_CHARS = 2000;
24
+ export const MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
25
+ export const FETCH_TRUNCATION_MARKER = "\n\n[content truncated]";
26
26
 
27
- export const DEFAULT_API_KEY_SOURCE = "auto"
28
- export const DEFAULT_MAX_RETRIES = 0
29
- export const MAX_RETRIES = 5
30
- export const DEFAULT_RETRY_BASE_DELAY_MS = 250
31
- export const MAX_RETRY_DELAY_MS = 10000
27
+ export const DEFAULT_API_KEY_SOURCE = "auto";
28
+ export const DEFAULT_MAX_RETRIES = 0;
29
+ export const MAX_RETRIES = 5;
30
+ export const DEFAULT_RETRY_BASE_DELAY_MS = 250;
31
+ export const MAX_RETRY_DELAY_MS = 10000;
32
32
 
33
33
  /** The single source for every DEEPSEEK_* environment variable name. */
34
- export const API_KEY_ENV = "DEEPSEEK_API_KEY"
35
- export const ENABLE_SEARCH_ENV = "DEEPSEEK_ENABLE_SEARCH"
36
- export const ENABLE_FETCH_ENV = "DEEPSEEK_ENABLE_FETCH"
34
+ export const API_KEY_ENV = "DEEPSEEK_API_KEY";
35
+ export const ENABLE_SEARCH_ENV = "DEEPSEEK_ENABLE_SEARCH";
36
+ export const ENABLE_FETCH_ENV = "DEEPSEEK_ENABLE_FETCH";
@@ -4,16 +4,40 @@
4
4
  */
5
5
 
6
6
  export const FETCH_SERVICE_LIMITS =
7
- "DeepSeek does not render JavaScript and cannot reach private networks; PDFs, images, and other binary files are unsupported."
7
+ "DeepSeek does not render JavaScript and cannot reach private networks; PDFs, images, and other binary files are unsupported.";
8
8
 
9
- export const FETCH_OUTPUT_TRUNCATED = "Output is truncated to the configured size."
9
+ export const FETCH_OUTPUT_TRUNCATED = "Output is truncated to the configured size.";
10
10
 
11
- export const FETCH_MARKDOWN_ONLY = 'Only format "markdown" is supported.'
11
+ export const FETCH_MARKDOWN_ONLY = 'Only format "markdown" is supported.';
12
12
 
13
13
  export const SEARCH_RANKED_FILTERED =
14
- "Results are ranked by relevance and source authority, deduplicated by URL, and capped at the configured maximum."
14
+ "Results are ranked by relevance and source authority, deduplicated by URL, and capped at the configured maximum.";
15
15
 
16
- export const SEARCH_EMPTY_IS_NORMAL = "An empty result list is normal, not an error."
16
+ export const SEARCH_EMPTY_IS_NORMAL = "An empty result list is normal, not an error.";
17
17
 
18
18
  export const SEARCH_QUERY_OPERATORS =
19
- "Use site:/-site: operators in the query to include or exclude domains; the full query must stay within 128 characters."
19
+ "Use site:/-site: operators in the query to include or exclude domains; the full query must stay within 128 characters.";
20
+
21
+ /**
22
+ * The full tool descriptions, composed from the fact fragments. Every host
23
+ * adapter registers its tools with these verbatim — the MCP server and the Pi
24
+ * extension must never drift on what a tool does.
25
+ */
26
+ export const SEARCH_TOOL_DESCRIPTION =
27
+ "Search the web with DeepSeek's in-house search engine and return a short, filtered result " +
28
+ "list (title, URL, summary). " +
29
+ `${SEARCH_RANKED_FILTERED} ` +
30
+ `${SEARCH_EMPTY_IS_NORMAL} ` +
31
+ `${SEARCH_QUERY_OPERATORS} ` +
32
+ "To read a full page, pass a result URL to the deepseek_fetch tool.";
33
+
34
+ export const FETCH_TOOL_DESCRIPTION =
35
+ "Fetch one web page as clean Markdown through DeepSeek's Fetch API. " +
36
+ `${FETCH_SERVICE_LIMITS} ` +
37
+ `${FETCH_OUTPUT_TRUNCATED}`;
38
+
39
+ /** Parameter descriptions shared by every adapter's input schema for the two tools. */
40
+ export const SEARCH_QUERY_PARAMETER =
41
+ "Search query, 128 characters or fewer including operators (site:, -site:, quoted phrases, lang:).";
42
+
43
+ export const FETCH_URL_PARAMETER = "Absolute http:// or https:// URL to fetch. Credentials are rejected.";