deepseek-web-retrieval 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +93 -244
- package/README.md +16 -11
- package/dist/cli.js +1366 -615
- package/docs/agents/codex.md +28 -7
- package/docs/agents/opencode-v2.md +1 -1
- package/docs/agents/pi.md +88 -86
- package/docs/design/installer-design.md +72 -9
- package/docs/hosts.jsonc +2 -2
- package/docs/integration.md +14 -9
- package/docs/research/control-matrix.md +48 -13
- package/docs/research/mcp-compatibility.md +9 -4
- package/examples/pi.settings.jsonc +17 -0
- package/index.ts +2 -2
- package/package.json +35 -4
- package/src/core/auth.ts +4 -6
- package/src/core/client.ts +65 -53
- package/src/core/constants.ts +24 -24
- package/src/core/descriptions.ts +30 -6
- package/src/core/domains.ts +56 -67
- package/src/core/env.ts +112 -0
- package/src/core/format.ts +53 -50
- package/src/core/index.ts +9 -9
- package/src/core/jsonc.ts +6 -6
- package/src/core/object.ts +4 -0
- package/src/core/options.ts +20 -25
- package/src/core/search.ts +45 -44
- package/src/core/transport.ts +109 -113
- package/src/core/types.ts +47 -47
- package/src/core/url.ts +20 -20
- package/src/installer/apply.ts +256 -81
- package/src/installer/cli.ts +135 -104
- package/src/installer/detect.ts +46 -31
- package/src/installer/formats/index.ts +26 -28
- package/src/installer/formats/json.ts +81 -77
- package/src/installer/formats/rules.ts +55 -28
- package/src/installer/formats/toml.ts +88 -81
- package/src/installer/formats/value.ts +34 -30
- package/src/installer/hosts/codex.ts +100 -54
- package/src/installer/hosts/index.ts +126 -41
- package/src/installer/hosts/opencode.ts +70 -51
- package/src/installer/hosts/pi.ts +269 -75
- package/src/installer/hosts/shared.ts +19 -14
- package/src/installer/operation-lock.ts +54 -0
- package/src/installer/ownership.ts +129 -0
- package/src/installer/paths.ts +7 -5
- package/src/installer/persistence.ts +66 -0
- package/src/installer/render.ts +29 -29
- package/src/installer/revert.ts +33 -29
- package/src/installer/rulesContent.ts +24 -13
- package/src/installer/state.ts +116 -31
- package/src/installer/types.ts +74 -56
- package/src/installer/uninstall.ts +40 -17
- package/src/mcp/auth.ts +7 -8
- package/src/mcp/cli.ts +38 -20
- package/src/mcp/client.ts +35 -0
- package/src/mcp/server.ts +68 -97
- package/src/opencode/auth.ts +24 -26
- package/src/opencode/plugin.ts +99 -87
- package/src/pi/extension.ts +164 -0
- package/src/pi/settings.ts +49 -0
- package/src/mcp/config.ts +0 -112
|
@@ -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`.
|
|
83
|
-
|
|
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` (
|
|
112
|
-
(
|
|
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.
|
|
140
|
-
|
|
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`
|
|
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
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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`
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
2
|
-
export
|
|
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
|
+
"version": "0.5.0",
|
|
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
|
|
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
|
-
|
|
5
|
-
|
|
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
|
}
|
package/src/core/client.ts
CHANGED
|
@@ -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
|
-
|
|
36
|
-
readonly
|
|
37
|
-
readonly
|
|
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
|
-
{
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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:
|
|
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();
|
package/src/core/constants.ts
CHANGED
|
@@ -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";
|
package/src/core/descriptions.ts
CHANGED
|
@@ -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.";
|