deepseek-web-retrieval 0.5.0 → 0.6.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.
@@ -27,7 +27,8 @@ For shared search/fetch limits, retries, and privacy behavior, see
27
27
 
28
28
  ## Requirements
29
29
 
30
- - Codex 0.163.0 or newer
30
+ - Codex at the tested version in [`docs/hosts.jsonc`](../hosts.jsonc); init reports
31
+ version drift, and newer versions may require rechecking routing behavior
31
32
  - Node.js 20 or newer (`npx` runs the server)
32
33
  - A DeepSeek account with access to `/v1/tools/search` and `/v1/tools/fetch`
33
34
  - A funded DeepSeek account; Search and Fetch are billed per successful request
@@ -43,9 +44,9 @@ npx deepseek-web-retrieval init --host codex --scope global \
43
44
  ```
44
45
 
45
46
  It prints the full plan — every file, old value → new value, and notes about existing
46
- MCP servers — and writes nothing until you pass `--apply`. `uninstall` restores exactly
47
- what the plan applied (snapshots, merged entries, created files). Codex's config lives in
48
- `~/.codex/config.toml` at the user level only, so the server registration and kill switch
47
+ MCP servers — and writes nothing until you pass `--apply`. `uninstall` removes recorded
48
+ additions and restores snapshotted values that have not since been edited. Codex's config
49
+ lives in `~/.codex/config.toml` at the user level only, so the server registration and kill switch
49
50
  are written there even for `--scope project`; the rules file follows the scope. An `off`
50
51
  capability is written as the tool's name in the server entry's `disabled_tools` array —
51
52
  Codex's own mechanism — merging with any entries you put there yourself. When `codex` is
@@ -54,6 +55,10 @@ release differs from the tested lock (see
54
55
  [hosts.jsonc](../hosts.jsonc)) — version-anchored behavior such as the kill switch may
55
56
  have drifted between the two.
56
57
 
58
+ For an existing installer-managed setup, follow the
59
+ [upgrade instructions](../integration.md#upgrading-existing-installs) from the original
60
+ project directory and scope.
61
+
57
62
  ### Manual install
58
63
 
59
64
  Add the server to `~/.codex/config.toml`:
@@ -149,9 +154,9 @@ defaults and clamping match the other adapters. `DEEPSEEK_API_KEY` goes in `env_
149
154
  | `DEEPSEEK_ENABLE_SEARCH` | `true` | Register the search tool. Only an explicit `false`/`0`/`no`/`off` disables it; at least one tool must stay enabled. |
150
155
  | `DEEPSEEK_SEARCH_COUNT` | `10` | Results requested from DeepSeek, 1–50. Billing is per request, not per result. |
151
156
  | `DEEPSEEK_SEARCH_MAX_RESULTS` | `10` | Results returned to the model after filtering, 0–50. `0` returns no results. |
152
- | `DEEPSEEK_SEARCH_MIN_RERANK` | `0` | Drop results below this relevance score. DeepSeek scores are 0–3. |
157
+ | `DEEPSEEK_SEARCH_MIN_RERANK` | `0` | Drop results below this relevance score (0–3); results without a finite score pass at zero and are dropped at a positive threshold. |
153
158
  | `DEEPSEEK_SEARCH_MIN_AUTHORITY` | `0` | Drop known authorities below this tier. Authority is 0–4. |
154
- | `DEEPSEEK_SEARCH_ALLOW_UNKNOWN_AUTHORITY` | `true` | Keep results whose authority is missing or unknown. Set `false`/`0` to drop them. |
159
+ | `DEEPSEEK_SEARCH_ALLOW_UNKNOWN_AUTHORITY` | `true` | Keep results whose authority is missing or unknown. Set `false`/`0`/`no`/`off` to drop them. |
155
160
  | `DEEPSEEK_SEARCH_BLOCKED_DOMAINS` | unset | Comma-separated; adds `-site:` operators to the query. |
156
161
  | `DEEPSEEK_SEARCH_INCLUDE_DOMAINS` | unset | Comma-separated; restricts results with `site:` operators. |
157
162
  | `DEEPSEEK_SEARCH_LANGUAGE` | unset | Adds a `lang:` operator, for example `zh,en`. |
@@ -208,7 +213,9 @@ Tested with Codex 0.163.0 on Linux over stdio.
208
213
  npx deepseek-web-retrieval uninstall --host codex --scope global
209
214
  ```
210
215
 
211
- This restores the previous `web_search` value, removes the `mcp_servers.deepseek` table
212
- and the rules block, and deletes files the installer created. Manually, remove the
216
+ This restores snapshotted `web_search` and `mcp_servers.deepseek` values only when their
217
+ current values still match what the installer wrote, removes the managed rules block,
218
+ and deletes installer-created config files only when left empty. Later user edits to
219
+ configuration values are kept and reported. For a manual installation, remove the
213
220
  `[mcp_servers.deepseek]` table, any `deepseek-web-retrieval` rules block in your
214
221
  `AGENTS.md`, and the top-level `web_search` key if it was only set for the forced tier.
@@ -2,7 +2,7 @@
2
2
 
3
3
  OpenCode v2 adapter for [DeepSeek Web Retrieval](../../README.md).
4
4
 
5
- Integration mechanism: OpenCode's native plugin API (`@opencode/plugin` 2.0.32+), not MCP.
5
+ Integration mechanism: OpenCode's native plugin API (`@opencode/plugin`), not MCP.
6
6
 
7
7
  When loaded, it adds:
8
8
 
@@ -18,7 +18,9 @@ For shared search/fetch limits, retries, and privacy behavior, see
18
18
 
19
19
  ## Requirements
20
20
 
21
- - OpenCode v2 with `@opencode/plugin` 2.0.32 or newer
21
+ - OpenCode v2 with `@opencode/plugin` in the `^2.0.32` peer range; the tested version
22
+ is recorded in [`docs/hosts.jsonc`](../hosts.jsonc)
23
+ - Node.js 20+ and npm when using the installer CLI
22
24
  - A DeepSeek account with access to `/v1/tools/search` and `/v1/tools/fetch`
23
25
  - A funded DeepSeek account; Search and Fetch are billed per successful request
24
26
  - A DeepSeek credential stored in OpenCode, or `DEEPSEEK_API_KEY`
@@ -38,8 +40,9 @@ npx deepseek-web-retrieval init --host opencode --scope <project|global> \
38
40
  It prints the full plan — every file, old value → new value — and writes nothing until
39
41
  you pass `--apply`. It snapshots the current `websearch.provider` before the forced tier
40
42
  replaces it, merges `permissions` without touching yours, preserves comments in
41
- `opencode.jsonc`, and `uninstall` restores everything. Search has no `preferred` tier on
42
- OpenCode (the provider is host-level config, never a model choice); the CLI rejects it.
43
+ `opencode.jsonc`, and `uninstall` restores recorded values that have not since been edited.
44
+ Search has no `preferred` tier on OpenCode (the provider is host-level config, never a
45
+ model choice); the CLI rejects it.
43
46
 
44
47
  ### OpenCode CLI install
45
48
 
@@ -100,9 +103,12 @@ A complete global config:
100
103
  Use the bare package name (optionally pinned) as the `package` value:
101
104
 
102
105
  ```jsonc
103
- "plugins": [{ "package": "deepseek-web-retrieval@0.4.0" }]
106
+ "plugins": [{ "package": "deepseek-web-retrieval" }]
104
107
  ```
105
108
 
109
+ Append `@X.Y.Z` to pin a published version. For an existing installer-managed setup,
110
+ follow the [upgrade instructions](../integration.md#upgrading-existing-installs).
111
+
106
112
  **Global checkout**
107
113
 
108
114
  ```bash
@@ -234,7 +240,7 @@ using both an OpenCode-stored DeepSeek credential and `DEEPSEEK_API_KEY`.
234
240
  ## Uninstall
235
241
 
236
242
  With the installer CLI (restores the snapshotted `websearch.provider`, removes only the
237
- appended permissions, and deletes files it created):
243
+ appended permissions, and deletes installer-created files only when they are left empty):
238
244
 
239
245
  ```bash
240
246
  npx deepseek-web-retrieval uninstall --host opencode --scope <project|global>
package/docs/agents/pi.md CHANGED
@@ -25,7 +25,9 @@ shared search/fetch limits, retries, and privacy behavior, see
25
25
 
26
26
  ## Requirements
27
27
 
28
- - Pi 1.3.0 or newer
28
+ - Pi with host SDKs in the `^1.3.0` peer range (`>=1.3.0 <2.0.0`); the tested
29
+ version is recorded in [`docs/hosts.jsonc`](../hosts.jsonc)
30
+ - Node.js 20+ and npm when using the installer CLI or MCP fallback
29
31
  - A DeepSeek account with access to `/v1/tools/search` and `/v1/tools/fetch`
30
32
  - A funded DeepSeek account; Search and Fetch are billed per successful request
31
33
  - `DEEPSEEK_API_KEY` set in the shell that launches Pi
@@ -46,8 +48,12 @@ the extension: the plan appends `npm:deepseek-web-retrieval` to the `packages` a
46
48
  source at the next session start (the first launch fetches the package from npm;
47
49
  `pi update --extensions` refreshes it later). An off capability is written into the
48
50
  `deepseekWebRetrieval` settings namespace the extension reads at load. Pass `--path mcp`
49
- to install the [MCP fallback path](#the-mcp-fallback-path) instead. `uninstall` restores
50
- exactly what the plan applied.
51
+ to install the [MCP fallback path](#the-mcp-fallback-path) instead. `uninstall` removes
52
+ recorded additions and restores unchanged configuration values from their snapshots.
53
+
54
+ For upgrades from 0.4.0, select `--path mcp` explicitly to retain the old path, or use
55
+ the extension default to migrate; follow the shared
56
+ [upgrade instructions](../integration.md#upgrading-existing-installs).
51
57
 
52
58
  ### Manual install
53
59
 
@@ -110,9 +116,9 @@ it outranks the environment and is preserved by Pi's own settings rewrites).
110
116
  | `DEEPSEEK_ENABLE_SEARCH` | `true` | Register the search tool. Only an explicit `false`/`0`/`no`/`off` disables it; at least one tool must stay enabled. |
111
117
  | `DEEPSEEK_SEARCH_COUNT` | `10` | Results requested from DeepSeek, 1–50. Billing is per request, not per result. |
112
118
  | `DEEPSEEK_SEARCH_MAX_RESULTS` | `10` | Results returned to the model after filtering, 0–50. `0` returns no results. |
113
- | `DEEPSEEK_SEARCH_MIN_RERANK` | `0` | Drop results below this relevance score. DeepSeek scores are 0–3. |
119
+ | `DEEPSEEK_SEARCH_MIN_RERANK` | `0` | Drop results below this relevance score (0–3); results without a finite score pass at zero and are dropped at a positive threshold. |
114
120
  | `DEEPSEEK_SEARCH_MIN_AUTHORITY` | `0` | Drop known authorities below this tier. Authority is 0–4. |
115
- | `DEEPSEEK_SEARCH_ALLOW_UNKNOWN_AUTHORITY` | `true` | Keep results whose authority is missing or unknown. Set `false`/`0` to drop them. |
121
+ | `DEEPSEEK_SEARCH_ALLOW_UNKNOWN_AUTHORITY` | `true` | Keep results whose authority is missing or unknown. Set `false`/`0`/`no`/`off` to drop them. |
116
122
  | `DEEPSEEK_SEARCH_BLOCKED_DOMAINS` | unset | Comma-separated; adds `-site:` operators to the query. |
117
123
  | `DEEPSEEK_SEARCH_INCLUDE_DOMAINS` | unset | Comma-separated; restricts results with `site:` operators. |
118
124
  | `DEEPSEEK_SEARCH_LANGUAGE` | unset | Adds a `lang:` operator, for example `zh,en`. |
@@ -175,7 +181,9 @@ With the installer CLI:
175
181
  npx deepseek-web-retrieval uninstall --host pi --scope <project|global>
176
182
  ```
177
183
 
178
- Or remove the source with Pi itself:
184
+ For a manual installation, remove the source with Pi itself; use `--local` for a
185
+ project-local install. Installer-managed installs should use `uninstall` above so
186
+ rules, snapshots, and install records are cleaned up as well:
179
187
 
180
188
  ```bash
181
189
  pi remove npm:deepseek-web-retrieval
@@ -6,6 +6,8 @@ Design note for the install-time flow, shipped as the `deepseek-web-retrieval in
6
6
  the design survives between conversations. Research backing lives in
7
7
  [control-matrix.md](../research/control-matrix.md); host facts in [hosts.jsonc](../hosts.jsonc).
8
8
  Remaining hosts get host modules here as their guides ship.
9
+ Examples involving other hosts describe design requirements for future modules;
10
+ the current CLI accepts only the three shipped hosts above.
9
11
 
10
12
  ## Principles
11
13
 
@@ -27,17 +29,16 @@ of the whole install.
27
29
  - **Optional** — registered, the model picks freely.
28
30
  - **Preferred** — registered plus rules steering the model toward them.
29
31
  - **Forced** — deterministic routing (replacement, exclusion, or de-facto).
30
- - **Off** — the capability's tool is not registered at all. When the other capability
31
- stays on, the MCP entry still lands and carries that capability's server-side
32
- false-only switch (`DEEPSEEK_ENABLE_SEARCH` / `DEEPSEEK_ENABLE_FETCH`); both off
33
- refuses to install anything.
32
+ - **Off** — the DeepSeek capability is hidden from the model through the adapter's
33
+ registration switch or Codex's host-side `disabled_tools` filter; the mapping below
34
+ describes each path. Both off refuses to install anything.
34
35
  - **Scope** — project-level (this checkout, shareable via VCS) or global (every project
35
36
  for that host on the machine).
36
37
 
37
38
  ## Stock configuration
38
39
 
39
40
  The tier definitions assume a clean host. Real installs land on configuration that
40
- already exists, and the installer runs three phases before anything is written:
41
+ already exists, and the installer uses three phases:
41
42
 
42
43
  1. **Detect.** Read the host config, the rules files, and the registered MCP servers.
43
44
  Report the conflict surface: existing rules steering toward another web tool; other
@@ -50,14 +51,15 @@ already exists, and the installer runs three phases before anything is written:
50
51
  which entries the installer appended; single-value keys (`websearch.provider`, Pi's
51
52
  `exposure`) snapshot the prior value before overwriting; files the installer creates
52
53
  are tracked as created. Nothing is written until the user approves the full plan.
53
- 3. **Verify.** After applying, confirm the chosen tier actually holds: the deny rules
54
- take effect, no higher layer overrides them, and the model sees exactly the intended
55
- tools.
54
+ 3. **Verify.** After applying, re-read the files and confirm the planned values and rules
55
+ were written. Runtime tool visibility and model routing are checked by host-specific
56
+ end-to-end scenarios, not by the installer itself.
56
57
 
57
58
  ## Tier semantics against stock configuration
58
59
 
59
- - **Optional is inherently stock-safe**: purely additive — it appends new entries
60
- without altering existing keys or values, and removes nothing. It is the safe default.
60
+ - **Optional keeps built-in routing unchanged**: it registers the DeepSeek capability
61
+ without preference rules or competitor exclusion. An existing DeepSeek registration
62
+ may still need replacement, which the plan shows and snapshots; inspect the preview.
61
63
  - **Preferred** can conflict without any config being touched: existing rules pointing
62
64
  at another web tool produce contradictory instructions whose outcome depends on each
63
65
  host's load order (Claude Code concatenates every level; Devin ranks team above
@@ -101,16 +103,17 @@ settings hierarchy, OpenCode's global versus project config) under three rules:
101
103
  alternative — the global fallback for a kill switch, or a different tier that works
102
104
  at project scope — instead of quietly writing to the other scope.
103
105
  - **Blast radius is part of the plan.** Global scope changes every project the user
104
- opens with that host; project scope is confined to one checkout but is often
105
- committed, which shares the config and rules with the whole team — including the
106
+ opens with that host; project scope normally writes into one checkout, while shared
107
+ config fallbacks such as Codex's retain user-wide effects. Project-local artifacts are
108
+ often committed, which shares the config and rules with the whole team — including the
106
109
  `${DEEPSEEK_API_KEY}` expansion that relies on each member's environment. The impact
107
110
  report states which applies.
108
111
  - **Layers above the user still outrank the choice.** Managed, enterprise, and system
109
112
  layers override anything the installer writes at either scope; the detect phase
110
113
  reports them instead of failing silently.
111
114
 
112
- Uninstall is scope-aware: removing a project-scope install never touches user-scope
113
- entries and vice versa, and a list mode shows what is installed where. The invariant is
115
+ Uninstall follows the files recorded for the chosen scope, including Codex's user-level
116
+ config even for a project-scope install; list shows those actual paths. The invariant is
114
117
  guarded, not just assumed: two records for one host never claim the same file. When a
115
118
  scope choice would overlap the other scope's record — Codex's project-scope fallback
116
119
  onto `~/.codex/config.toml` beside a global install — the plan refuses with guidance,
@@ -133,10 +136,13 @@ Do not delete an ownership sidecar while its install is still active.
133
136
 
134
137
  ## Rollback
135
138
 
136
- Uninstall reverses exactly what the plan applied: overwritten values restore their
137
- snapshots, merged arrays remove only the entries the installer appended, appended rules
138
- content is removed, and files the installer created are deleted — nothing more, nothing
139
- less. The [OpenCode v2 guide](../agents/opencode-v2.md) documents the failure mode this
139
+ Uninstall restores overwritten values from their snapshots only while the current value
140
+ still matches what the installer wrote; later user edits are kept and reported. Merged
141
+ arrays lose only matching entries the installer appended, and managed rules blocks are
142
+ removed while surrounding user content is retained. Installer-created config files are
143
+ deleted only when left empty; an empty rules file is also removed. TOML comments are not
144
+ restored because the codec reserializes the document. The
145
+ [OpenCode v2 guide](../agents/opencode-v2.md) documents the failure mode this
140
146
  avoids: `plugin remove` deletes the plugin entry but leaves `websearch.provider`
141
147
  pointing at the removed plugin, and the user must clean up by hand.
142
148
 
@@ -215,8 +221,7 @@ host module; their artifacts (from the tier-to-artifact mapping) remain hand-edi
215
221
  config until a module lands here. OpenCode's `opencode plugin add` remains available but
216
222
  cannot pass options or manage snapshots; `init` supersedes it for tiered installs.
217
223
 
218
-
219
- ### Persistence and concurrent operations
224
+ ## Persistence and concurrent operations
220
225
 
221
226
  Install records distinguish absent files from invalid or unreadable files. The
222
227
  reader validates version, host/scope identity, tiers, file metadata, and recorded
@@ -48,11 +48,11 @@ Codex are Apache-2.0; OpenCode and Pi are MIT. Claude Code and
48
48
  Cursor are proprietary. GitHub Copilot CLI and Devin CLI publish installers and docs, not
49
49
  source code, under proprietary terms.
50
50
 
51
- Support for the other popular CLI agents is in progress through the MCP server, which is
52
- shipped in `src/mcp/` and works with any host that supports MCP tools over stdio. A host is
53
- listed as supported only after an adapter and guide exist; for the remaining hosts,
54
- `No adapter yet` means the shipped MCP server already provides the path, but no guide for
55
- that host exists yet. All version locks live in [hosts.jsonc](hosts.jsonc), the single
51
+ OpenCode, Pi, and Codex have supported setup guides and installer host modules. The
52
+ MCP server in `src/mcp/` provides a possible stdio path for the remaining hosts, but
53
+ protocol compatibility alone does not establish a tested host setup. For those rows,
54
+ `No adapter yet` means no supported host-specific adapter or setup guide is shipped.
55
+ All version locks live in [hosts.jsonc](hosts.jsonc), the single
56
56
  source of truth for this table; OpenCode is pinned to the adapter's tested target, not
57
57
  necessarily the latest OpenCode release. Control tiers per host and capability — optional,
58
58
  preferred, or forced routing — are researched in [control-matrix.md](research/control-matrix.md),
@@ -63,7 +63,7 @@ whose per-host reference also carries MCP registration and rules files.
63
63
  One line per host. The [per-host reference](research/control-matrix.md#per-host-reference) carries
64
64
  the details: built-in web tools and their kill switches, MCP registration, and rules files.
65
65
 
66
- - **OpenCode v2** — The only shipped adapter. It uses OpenCode's native plugin API and
66
+ - **OpenCode v2** — Uses OpenCode's native plugin API and
67
67
  exposes both a websearch provider and a fetch tool/override. See
68
68
  [OpenCode v2](agents/opencode-v2.md).
69
69
  - **Claude Code** — Ships built-in `WebSearch` and `WebFetch` tools. See
@@ -82,7 +82,38 @@ the details: built-in web tools and their kill switches, MCP registration, and r
82
82
  the native extension adapter (the recommended path), with the MCP server as the fallback;
83
83
  see [Pi](agents/pi.md) and [Pi in the per-host reference](research/control-matrix.md#pi).
84
84
 
85
- ## Roadmap
85
+ ## Upgrading existing installs
86
+
87
+ For an installation managed by `init`:
88
+
89
+ 1. Run `npx deepseek-web-retrieval@latest list` from the original project directory,
90
+ using the same home and `XDG_CONFIG_HOME` as the original installation.
91
+ 2. Run `npx deepseek-web-retrieval@latest init` with the original `--host`, `--scope`,
92
+ `--search`, and `--fetch` values, changing only the settings you intend to change.
93
+ Review the plan and repeat with `--apply`; re-init preserves the original snapshots
94
+ needed for uninstall. A global install can still claim shared configuration from a
95
+ particular project, so keep the original working directory for upgrades.
96
+ 3. Refresh the package in the host and start or reload a session as described in its
97
+ guide. For Pi's extension path, use `pi update --extensions`. If you pin the host's
98
+ package version, pass the desired `--package deepseek-web-retrieval@X.Y.Z` to init;
99
+ updating the CLI alone does not update an independently pinned host package.
100
+
101
+ When upgrading from 0.4.0, Pi's default changes from MCP to the native extension.
102
+ Pass `--path mcp` explicitly to retain MCP, or `--path extension` to migrate; the plan
103
+ shows the old path's removals and retains the original uninstall snapshots. OpenCode's
104
+ native plugin and Codex's MCP path remain the same. Standalone MCP installs no longer
105
+ require OpenCode or Pi SDKs; native adapters use the SDKs provided by their hosts.
106
+
107
+ Keep install records and ownership sidecars: deleting them loses recovery information
108
+ and can cause a later init to refuse an existing registration. If an operation was
109
+ interrupted, follow the [recovery guidance](design/installer-design.md#persistence-and-concurrent-operations)
110
+ before retrying. For manual installations, update the package reference or checkout and
111
+ follow the host guide rather than adopting the config through init.
112
+
113
+ Code importing the core's removed `formatFetchContent` helper must migrate to
114
+ `formatFetchedContent`, which returns both `content` and `truncated`.
115
+
116
+ ## Available paths and future hosts
86
117
 
87
118
  - **MCP server** — shipped in `src/mcp/` (the fallback path in the [Pi](agents/pi.md) guide
88
119
  and the path in the [Codex](agents/codex.md) guide); the broadest route for hosts that
@@ -181,8 +181,8 @@ paths imply the available scopes; keys locked to a single scope are enumerated i
181
181
  prerequisite for every tier. SSE is rejected. Text results over 20 KB are
182
182
  middle-truncated with the full text saved to a file, and the per-request default
183
183
  timeout is 60 s. Pi is content-first for the model, with a `structuredContent`
184
- fallback used only when a result has no content blocks — the one open-source host that
185
- touches `structuredContent` at all — and a re-declared (not validated) tool
184
+ fallback used only when a result has no content blocks, unlike Codex's preference for
185
+ `structuredContent`, and a re-declared (not validated) tool
186
186
  `outputSchema`.
187
187
  - **Rules** — `AGENTS.md`/`CLAUDE.md` context files from the agent dir, the working
188
188
  directory, and its parents; `SYSTEM.md` replaces the system prompt and `APPEND_SYSTEM.md`
@@ -129,7 +129,7 @@ differences:
129
129
  1.0.81+). A v1.x-built server still works today — modern clients fall back to the legacy
130
130
  handshake — but it can never offer the stateless era; v2 serves both from one entry
131
131
  point.
132
- 4. **Streamable HTTP only for a hosted deployment.** All nine hosts support it (with
132
+ 4. **Streamable HTTP only for a hosted deployment.** All seven MCP hosts surveyed here support it (with
133
133
  different `type` strings: `http`, `streamable-http`, `streamableHttp`), and OAuth 2.1
134
134
  resource-server behavior (RFC 9728 metadata) then applies; prefer pre-registered clients
135
135
  over the now-deprecated Dynamic Client Registration.
@@ -139,6 +139,10 @@ differences:
139
139
  truncation, environment passing, output caps, and per-host approval surfaces; consult
140
140
  it per host rather than this summary.
141
141
 
142
+ These protocol findings describe compatibility at the research baseline, not completed
143
+ end-to-end support for every host. The shipped server exposes stdio; supported setup
144
+ guides and installer coverage are listed in the [integration model](../integration.md).
145
+
142
146
  ## Sources
143
147
 
144
148
  ### Specification and SDKs
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.5.0",
4
+ "version": "0.6.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",
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync, rmSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
+ import { TOOL_VERSION } from "../version";
3
4
  import { codecFor, writeConfig } from "./formats";
4
5
  import { deepEqual, valueAt } from "./formats/value";
5
6
  import { withOperationLock } from "./operation-lock";
@@ -8,9 +9,9 @@ import { pruneEmptyDir, revertRecordedFile } from "./revert";
8
9
  import { deleteRecord, readRecord, statePathFor, writeRecord } from "./state";
9
10
  import type { ConfigOp, FileEdit, InstallerInput, InstallPlan, InstallRecord, RecordedFile } from "./types";
10
11
 
12
+ /** What a successful apply reports; every failure mode throws instead. */
11
13
  export interface ApplyResult {
12
- readonly lines: string[];
13
- readonly ok: boolean;
14
+ readonly lines: readonly string[];
14
15
  }
15
16
 
16
17
  /** A failed apply tells callers whether retrying is safe or recovery is still needed. */
@@ -94,6 +95,7 @@ function applyLocked(plan: InstallPlan, input: InstallerInput): ApplyResult {
94
95
  lines.push(...verification.lines);
95
96
  const record: InstallRecord = {
96
97
  version: 1,
98
+ installerVersion: TOOL_VERSION,
97
99
  host: plan.host,
98
100
  scope: plan.scope,
99
101
  tiers: plan.tiers,
@@ -135,7 +137,7 @@ function applyLocked(plan: InstallPlan, input: InstallerInput): ApplyResult {
135
137
  error,
136
138
  );
137
139
  }
138
- return { lines, ok: true };
140
+ return { lines };
139
141
  }
140
142
 
141
143
  function errorMessage(error: unknown): string {
@@ -1,6 +1,7 @@
1
1
  import { homedir } from "node:os";
2
2
  import { resolve } from "node:path";
3
- import { applyPlan } from "./apply";
3
+ import { TOOL_VERSION } from "../version";
4
+ import { ApplyError, applyPlan } from "./apply";
4
5
  import { HOST_PLANNERS, HOST_VERSION_PROBES, planForHost } from "./hosts";
5
6
  import { renderPlan } from "./render";
6
7
  import { listRecords } from "./state";
@@ -12,12 +13,17 @@ const HOST_CHOICES = `<${HOSTS.join("|")}>`;
12
13
  const SCOPES: readonly Scope[] = ["project", "global"];
13
14
  const TIERS: readonly Tier[] = ["optional", "preferred", "forced", "off"];
14
15
 
16
+ const EXIT_FAILURE = 1;
17
+ const EXIT_USAGE = 2;
18
+ const EXIT_RECOVERY = 3;
19
+
15
20
  const USAGE = `Usage:
16
21
  deepseek-web-retrieval init --host ${HOST_CHOICES} --scope <project|global> \
17
22
  --search <optional|preferred|forced|off> --fetch <optional|preferred|forced|off> \
18
23
  [--path <extension|mcp>] [--package <npm ref>] [--cwd <dir>] [--apply]
19
24
  deepseek-web-retrieval uninstall --host ${HOST_CHOICES} --scope <project|global> [--cwd <dir>]
20
25
  deepseek-web-retrieval list [--cwd <dir>]
26
+ deepseek-web-retrieval version
21
27
 
22
28
  init prints the full plan (every file, old value → new value) and writes nothing
23
29
  until --apply is passed. uninstall restores what the plan applied. --path picks
@@ -25,7 +31,12 @@ the integration mechanism for hosts with more than one (Pi: extension by
25
31
  default, mcp for the stdio MCP server). --package overrides the package ref
26
32
  written into host config; the Pi extension path turns it into the packages
27
33
  source (a bare name is prefixed with npm:, a ref that already starts with npm:
28
- is written as-is).`;
34
+ is written as-is).
35
+
36
+ Exit codes: 0 success; 1 refused or failed with nothing written (a failed apply
37
+ is fully rolled back); 2 invalid command line; 3 recovery required — a failed
38
+ apply could not roll back completely, follow the reported guidance before
39
+ re-running.`;
29
40
 
30
41
  interface Flags {
31
42
  readonly values: Map<string, string>;
@@ -54,6 +65,10 @@ function parseFlags(args: readonly string[]): Flags {
54
65
  problems.push(`--${name} does not accept a value; use --${name} to enable it or omit it`);
55
66
  continue;
56
67
  }
68
+ if (booleans.has(name)) {
69
+ problems.push(`--${name} was given more than once`);
70
+ continue;
71
+ }
57
72
  booleans.add(name);
58
73
  continue;
59
74
  }
@@ -61,6 +76,10 @@ function parseFlags(args: readonly string[]): Flags {
61
76
  problems.push(`unknown flag --${name}`);
62
77
  continue;
63
78
  }
79
+ if (values.has(name)) {
80
+ problems.push(`--${name} was given more than once`);
81
+ continue;
82
+ }
64
83
  if (eq !== -1) {
65
84
  values.set(name, arg.slice(eq + 1));
66
85
  continue;
@@ -87,15 +106,23 @@ export async function runInstallerCli(argv: readonly string[], io: CliIo = {}):
87
106
  const out = io.out ?? ((line: string) => console.log(line));
88
107
  const err = io.err ?? ((line: string) => console.error(line));
89
108
  const [command, ...rest] = argv;
90
- if (command === undefined || command === "help" || command === "--help") {
109
+ if (command === "version" || command === "--version") {
110
+ out(TOOL_VERSION);
111
+ return 0;
112
+ }
113
+ if (command === undefined) {
114
+ err(USAGE);
115
+ return EXIT_USAGE;
116
+ }
117
+ if (command === "help" || command === "--help") {
91
118
  out(USAGE);
92
- return command === undefined ? 1 : 0;
119
+ return 0;
93
120
  }
94
121
  const flags = parseFlags(rest);
95
122
  if (flags.problems.length > 0) {
96
123
  for (const problem of flags.problems) err(`error: ${problem}`);
97
124
  err(`\n${USAGE}`);
98
- return 1;
125
+ return EXIT_USAGE;
99
126
  }
100
127
  const cwd = resolve(flags.values.get("cwd") ?? process.cwd());
101
128
  const home = process.env.HOME ?? homedir();
@@ -114,8 +141,9 @@ export async function runInstallerCli(argv: readonly string[], io: CliIo = {}):
114
141
  return 0;
115
142
  }
116
143
  for (const record of records) {
144
+ const by = record.installerVersion ? ` by v${record.installerVersion}` : "";
117
145
  out(
118
- `${record.host} (${record.scope}) — search=${record.tiers.search}, fetch=${record.tiers.fetch}, installed ${record.createdAt}`,
146
+ `${record.host} (${record.scope}) — search=${record.tiers.search}, fetch=${record.tiers.fetch}, installed ${record.createdAt}${by}`,
119
147
  );
120
148
  for (const file of record.files) out(` ${file.created ? "created" : "modified"} ${file.file}`);
121
149
  }
@@ -136,7 +164,7 @@ export async function runInstallerCli(argv: readonly string[], io: CliIo = {}):
136
164
 
137
165
  if (command !== "init") {
138
166
  err(`Unknown command "${command}".\n\n${USAGE}`);
139
- return 1;
167
+ return EXIT_USAGE;
140
168
  }
141
169
 
142
170
  const required = requireInput(flags, cwd, home, configHome, true);
@@ -158,17 +186,22 @@ export async function runInstallerCli(argv: readonly string[], io: CliIo = {}):
158
186
  try {
159
187
  const result = applyPlan(plan, input);
160
188
  for (const line of result.lines) out(line);
161
- return result.ok ? 0 : 1;
189
+ return 0;
162
190
  } catch (error) {
163
191
  err(error instanceof Error ? error.message : String(error));
164
- return 1;
192
+ return exitCodeForError(error);
165
193
  }
166
194
  }
167
195
 
196
+ /** Maps a thrown error to its exit code; recovery is the case a script must not retry blindly. */
197
+ export function exitCodeForError(error: unknown): number {
198
+ return error instanceof ApplyError && error.status === "recovery_required" ? EXIT_RECOVERY : EXIT_FAILURE;
199
+ }
200
+
168
201
  function usageError(problems: readonly string[], err: (line: string) => void): number {
169
202
  for (const problem of problems) err(`error: ${problem}`);
170
203
  err(`\n${USAGE}`);
171
- return 1;
204
+ return EXIT_USAGE;
172
205
  }
173
206
 
174
207
  /**
@@ -22,6 +22,7 @@ const operation = z.discriminatedUnion("op", [
22
22
  ]);
23
23
  const recordSchema = z.object({
24
24
  version: z.literal(1),
25
+ installerVersion: z.string().optional(),
25
26
  host: z.enum(["opencode", "pi", "codex"]),
26
27
  scope: z.enum(["global", "project"]),
27
28
  tiers: z.object({ search: tier, fetch: tier }),
@@ -136,6 +136,8 @@ export interface RecordedFile {
136
136
  /** Persisted install state; one record per host and scope. */
137
137
  export interface InstallRecord {
138
138
  readonly version: 1;
139
+ /** Version of the installer that wrote this record; absent on records from older releases. */
140
+ readonly installerVersion?: string;
139
141
  readonly host: HostName;
140
142
  readonly scope: Scope;
141
143
  readonly tiers: { readonly search: Tier; readonly fetch: Tier };
package/src/mcp/cli.ts CHANGED
@@ -1,57 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { serveStdio } from "@modelcontextprotocol/server/stdio";
3
- import { apiKeyFromEnv } from "../core/auth";
4
- import { resolveEnvOptions } from "../core/env";
5
- import { MISSING_API_KEY_MESSAGE } from "./auth";
6
- import { isJsonReplacingClient } from "./client";
7
- import { createDeepSeekServer, servePrecheck } from "./server";
8
-
9
- // Installer subcommands run before the server path; MCP clients spawn this bin
10
- // with no arguments, so the default stdio behavior is unchanged.
11
- const subcommand = process.argv[2];
12
- if (subcommand === "init" || subcommand === "uninstall" || subcommand === "list") {
2
+ // Any argument means a human ran the CLI; MCP clients spawn this bin with no
3
+ // arguments for the stdio server. The installer is imported statically and the
4
+ // MCP server dynamically, so init/uninstall never load — or depend on — the
5
+ // server's dependency graph, including when recovering from a bad server setup.
6
+ if (process.argv.length > 2) {
13
7
  const { runInstallerCli } = await import("../installer/cli");
14
8
  process.exit(await runInstallerCli(process.argv.slice(2)));
15
9
  }
16
10
 
17
- // stdout is the JSON-RPC channel; all diagnostics must go to stderr.
18
- const env = process.env;
19
- if (!apiKeyFromEnv(env)) {
20
- console.error(MISSING_API_KEY_MESSAGE);
21
- process.exit(1);
22
- }
23
-
24
- const config = resolveEnvOptions(env);
25
- const precheck = servePrecheck(config);
26
- if (precheck) {
27
- console.error(precheck);
28
- process.exit(1);
29
- }
30
- const handle = serveStdio(() => {
31
- const server = createDeepSeekServer(config, env);
32
- // Fires after the handshake-era initialize exchange (the stateless era opens
33
- // differently and simply never logs). One stderr line so a human run shows
34
- // which client connected and whether text-only shaping kicked in; if a host
35
- // renames its clientInfo, this line is where the regression becomes visible.
36
- server.server.oninitialized = () => {
37
- const info = server.server.getClientVersion();
38
- if (info === undefined) return;
39
- console.error(
40
- `mcp client: ${info.name} ${info.version ?? "unknown"} — ` +
41
- (isJsonReplacingClient(info.name)
42
- ? "text-only results (this client replaces content with serialized structuredContent)"
43
- : "text + structuredContent results"),
44
- );
45
- };
46
- return server;
47
- });
48
- const tools = [
49
- config.search.enabled ? `search (max ${config.search.maxResults} results)` : null,
50
- config.fetch.enabled ? `fetch (max ${config.fetch.maxChars} chars)` : null,
51
- ]
52
- .filter(Boolean)
53
- .join(", ");
54
- console.error(`deepseek-web-retrieval MCP server on stdio (${tools})`);
11
+ const { serveMcp } = await import("./serve");
12
+ serveMcp();
55
13
 
56
- process.on("SIGINT", () => void handle.close());
57
- process.on("SIGTERM", () => void handle.close());
14
+ export {};