deepseek-web-retrieval 0.6.0 → 0.8.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/README.md +113 -53
  3. package/dist/cli.js +2427 -1813
  4. package/docs/adr/0002-shared-config-base-layer.md +43 -0
  5. package/docs/agents/claude-code.md +273 -0
  6. package/docs/agents/codex.md +50 -27
  7. package/docs/agents/opencode-v2.md +59 -33
  8. package/docs/agents/pi.md +50 -27
  9. package/docs/configuration.md +109 -0
  10. package/docs/design/config-layering-design.md +180 -0
  11. package/docs/design/installer-design.md +24 -13
  12. package/docs/hosts.jsonc +5 -1
  13. package/docs/integration.md +15 -9
  14. package/docs/research/control-matrix.md +49 -11
  15. package/docs/research/mcp-compatibility.md +6 -3
  16. package/docs/research/ori-question-sourcing.md +336 -0
  17. package/docs/research/result-quality-passthrough.md +159 -0
  18. package/docs/research/search-api-defaults.md +18 -2
  19. package/docs/research/search-inline-content.md +244 -0
  20. package/examples/opencode.global.jsonc +3 -2
  21. package/package.json +2 -1
  22. package/src/core/auth.ts +3 -0
  23. package/src/core/client.ts +26 -4
  24. package/src/core/config-file.ts +168 -0
  25. package/src/core/config-summary.ts +82 -0
  26. package/src/core/constants.ts +15 -1
  27. package/src/core/descriptions.ts +6 -2
  28. package/src/core/env.ts +102 -32
  29. package/src/core/format.ts +19 -1
  30. package/src/core/options.ts +22 -2
  31. package/src/core/search.ts +6 -0
  32. package/src/core/types.ts +11 -0
  33. package/src/installer/cli.ts +50 -16
  34. package/src/installer/config-command.ts +116 -0
  35. package/src/installer/hosts/claude-code.ts +325 -0
  36. package/src/installer/hosts/codex.ts +9 -4
  37. package/src/installer/hosts/index.ts +3 -0
  38. package/src/installer/hosts/opencode.ts +9 -5
  39. package/src/installer/hosts/pi.ts +34 -9
  40. package/src/installer/ownership.ts +54 -1
  41. package/src/installer/paths.ts +7 -1
  42. package/src/installer/state.ts +73 -20
  43. package/src/installer/types.ts +11 -1
  44. package/src/installer/uninstall.ts +4 -1
  45. package/src/mcp/auth.ts +2 -3
  46. package/src/mcp/cli.ts +4 -3
  47. package/src/mcp/serve.ts +10 -1
  48. package/src/mcp/server.ts +7 -2
  49. package/src/opencode/plugin.ts +22 -2
  50. package/src/pi/extension.ts +78 -9
package/docs/agents/pi.md CHANGED
@@ -22,6 +22,8 @@ When loaded, it adds:
22
22
  Pi 1.3.0 ships no built-in web tool, so the registered tools are the only web path. For
23
23
  shared search/fetch limits, retries, and privacy behavior, see
24
24
  [General behavior](../../README.md#general-behavior).
25
+ For configuration layers, the full knob mapping, and defaults, see the
26
+ [configuration guide](../configuration.md).
25
27
 
26
28
  ## Requirements
27
29
 
@@ -42,7 +44,9 @@ npx deepseek-web-retrieval init --host pi --scope <project|global> \
42
44
  ```
43
45
 
44
46
  It prints the full plan — every file, old value → new value, and notes about existing
45
- packages and MCP servers — and writes nothing until you pass `--apply`. The default path is
47
+ packages and MCP servers — and writes nothing until you pass `--apply`. Add `--pin` to
48
+ register the exact version of the CLI you are running instead of the always-latest
49
+ package name. The default path is
46
50
  the extension: the plan appends `npm:deepseek-web-retrieval` to the `packages` array in
47
51
  `~/.pi/agent/settings.json` (global) or `.pi/settings.json` (project), and Pi resolves the
48
52
  source at the next session start (the first launch fetches the package from npm;
@@ -102,6 +106,12 @@ Fetch https://pi.dev/docs/latest/extensions and summarize the extension lifecycl
102
106
  The model calls `deepseek_web_search` or `deepseek_fetch` — the same names over both paths;
103
107
  only the MCP fallback prefixes them with `mcp__deepseek__`.
104
108
 
109
+ The search tool accepts an optional `original_question` input — the user's question in their
110
+ own words — forwarded as DeepSeek's `ori_question` to sharpen search intent. When the model
111
+ leaves it blank, the extension falls back to the most recent user message in the session
112
+ (best-effort; truncated to the API's 128-character limit). The MCP fallback path has no
113
+ session access, so there a blank input simply omits the parameter.
114
+
105
115
  ## Configuration
106
116
 
107
117
  The runtime config surface is the same `DEEPSEEK_*` environment the MCP server reads
@@ -109,40 +119,53 @@ The runtime config surface is the same `DEEPSEEK_*` environment the MCP server r
109
119
  `deepseekWebRetrieval` settings namespace for the off switches (written by the installer;
110
120
  it outranks the environment and is preserved by Pi's own settings rewrites).
111
121
 
122
+ The shared config file (`~/.config/deepseek-web-retrieval/config.json`, camelCase keys —
123
+ on Windows, `%APPDATA%\deepseek-web-retrieval\config.json`)
124
+ also sits in this surface, layered under the environment, and needs no forwarding on any
125
+ path — the extension and the MCP server both read the file themselves. One file therefore
126
+ configures every host running this package, with this host's environment and settings
127
+ still outranking it. `npx deepseek-web-retrieval config` prints what actually resolves —
128
+ each knob's value and source, with file warnings surfaced and `--search "<query>"` running
129
+ one billed search through it all.
130
+
112
131
  ### Search
113
132
 
114
- | Env variable | Default | Description |
115
- | --- | --- | --- |
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. |
117
- | `DEEPSEEK_SEARCH_COUNT` | `10` | Results requested from DeepSeek, 1–50. Billing is per request, not per result. |
118
- | `DEEPSEEK_SEARCH_MAX_RESULTS` | `10` | Results returned to the model after filtering, 0–50. `0` returns no results. |
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. |
120
- | `DEEPSEEK_SEARCH_MIN_AUTHORITY` | `0` | Drop known authorities below this tier. Authority is 0–4. |
121
- | `DEEPSEEK_SEARCH_ALLOW_UNKNOWN_AUTHORITY` | `true` | Keep results whose authority is missing or unknown. Set `false`/`0`/`no`/`off` to drop them. |
122
- | `DEEPSEEK_SEARCH_BLOCKED_DOMAINS` | unset | Comma-separated; adds `-site:` operators to the query. |
123
- | `DEEPSEEK_SEARCH_INCLUDE_DOMAINS` | unset | Comma-separated; restricts results with `site:` operators. |
124
- | `DEEPSEEK_SEARCH_LANGUAGE` | unset | Adds a `lang:` operator, for example `zh,en`. |
125
- | `DEEPSEEK_SEARCH_MAX_SUMMARY_CHARS` | `1200` | Per-result summary cap, 0–1,000,000. |
126
- | `DEEPSEEK_SEARCH_TIMEOUT_MS` | `30000` | Search timeout, 1,000–120,000 ms. No host-side cap applies on the extension path. |
133
+ The "Config file" column names each key's camelCase twin in the shared config file —
134
+ one vocabulary pair, one pipeline. `—` marks host-integration entries the file rejects.
135
+
136
+ | Env variable | Config file | Default | Description |
137
+ | --- | --- | --- | --- |
138
+ | `DEEPSEEK_ENABLE_SEARCH` | — | `true` | Register the search tool. Only an explicit `false`/`0`/`no`/`off` disables it; at least one tool must stay enabled. |
139
+ | `DEEPSEEK_SEARCH_COUNT` | `count` | `10` | Results requested from DeepSeek, 1–50. Billing is per request, not per result. |
140
+ | `DEEPSEEK_SEARCH_MAX_RESULTS` | `maxResults` | `10` | Results returned to the model after filtering, 0–50; defaults to `DEEPSEEK_SEARCH_COUNT` when unset. `0` returns no results. |
141
+ | `DEEPSEEK_SEARCH_MIN_RERANK` | `minRerank` | `0` | Drop results below this relevance score (0–3); results without a finite score pass at zero and are dropped at a positive threshold. |
142
+ | `DEEPSEEK_SEARCH_MIN_AUTHORITY` | `minAuthority` | `0` | Drop known authorities below this tier. Authority is 0–4. |
143
+ | `DEEPSEEK_SEARCH_ALLOW_UNKNOWN_AUTHORITY` | `allowUnknownAuthority` | `true` | Keep results whose authority is missing or unknown. Set `false`/`0`/`no`/`off` to drop them. |
144
+ | `DEEPSEEK_SEARCH_BLOCKED_DOMAINS` | `blockedDomains` | unset | Comma-separated; adds `-site:` operators to the query. |
145
+ | `DEEPSEEK_SEARCH_INCLUDE_DOMAINS` | `includeDomains` | unset | Comma-separated; restricts results with `site:` operators. |
146
+ | `DEEPSEEK_SEARCH_LANGUAGE` | `language` | unset | Adds a `lang:` operator, for example `zh,en`. |
147
+ | `DEEPSEEK_SEARCH_LOCALIZATION` | `searchLocalization` | unset | Valid IPv4 or IPv6 address sent as DeepSeek's `localization` to prefer geo-localized results; invalid values are ignored. Deployment-level only, never a per-call parameter. |
148
+ | `DEEPSEEK_SEARCH_MAX_SUMMARY_CHARS` | `maxSummaryChars` | `1200` | Per-result summary cap, 0–1,000,000. |
149
+ | `DEEPSEEK_SEARCH_TIMEOUT_MS` | `searchTimeoutMs` | `30000` | Search timeout, 1,000–120,000 ms. No host-side cap applies on the extension path. |
127
150
 
128
151
  ### Fetch
129
152
 
130
- | Env variable | Default | Description |
131
- | --- | --- | --- |
132
- | `DEEPSEEK_ENABLE_FETCH` | `true` | Register the fetch tool. Same false-only semantics as `DEEPSEEK_ENABLE_SEARCH`. |
133
- | `DEEPSEEK_FETCH_MAX_CHARS` | `16000` | Maximum characters returned, including the title, 0–1,000,000. The only output cap on this path. |
134
- | `DEEPSEEK_FETCH_TIMEOUT_MS` | `30000` | Fetch timeout, 1,000–120,000 ms. No host-side cap applies on the extension path. |
135
- | `DEEPSEEK_FETCH_ALLOWED_DOMAINS` | unset | If set, only these hosts and subdomains may be fetched. |
136
- | `DEEPSEEK_FETCH_BLOCKED_DOMAINS` | unset | Always block these hosts and subdomains. Blocked wins over allowed. |
137
- | `DEEPSEEK_FETCH_BLOCK_PRIVATE_HOSTS` | `true` | Reject localhost, private IPs, `.local`, `.internal`, and similar hosts. Set `false`/`0` to disable. |
153
+ | Env variable | Config file | Default | Description |
154
+ | --- | --- | --- | --- |
155
+ | `DEEPSEEK_ENABLE_FETCH` | — | `true` | Register the fetch tool. Same false-only semantics as `DEEPSEEK_ENABLE_SEARCH`. |
156
+ | `DEEPSEEK_FETCH_MAX_CHARS` | `fetchMaxChars` | `16000` | Maximum characters returned, including the title, 0–1,000,000. The only output cap on this path. |
157
+ | `DEEPSEEK_FETCH_TIMEOUT_MS` | `fetchTimeoutMs` | `30000` | Fetch timeout, 1,000–120,000 ms. No host-side cap applies on the extension path. |
158
+ | `DEEPSEEK_FETCH_ALLOWED_DOMAINS` | `fetchAllowedDomains` | unset | If set, only these hosts and subdomains may be fetched. |
159
+ | `DEEPSEEK_FETCH_BLOCKED_DOMAINS` | `fetchBlockedDomains` | unset | Always block these hosts and subdomains. Blocked wins over allowed. |
160
+ | `DEEPSEEK_FETCH_BLOCK_PRIVATE_HOSTS` | `fetchBlockPrivateHosts` | `true` | Reject localhost, private IPs, `.local`, `.internal`, and similar hosts. Set `false`/`0` to disable. |
138
161
 
139
162
  ### Credentials and retries
140
163
 
141
- | Env variable | Default | Description |
142
- | --- | --- | --- |
143
- | `DEEPSEEK_API_KEY` | required | DeepSeek API key, read from the Pi process environment. |
144
- | `DEEPSEEK_MAX_RETRIES` | `0` | Retries for 408, 425, 429, and 5xx responses, 0–5. |
145
- | `DEEPSEEK_RETRY_BASE_DELAY_MS` | `250` | Base delay for exponential backoff. `Retry-After` is honored up to 10 seconds. |
164
+ | Env variable | Config file | Default | Description |
165
+ | --- | --- | --- | --- |
166
+ | `DEEPSEEK_API_KEY` | — | required | DeepSeek API key, read from the Pi process environment. |
167
+ | `DEEPSEEK_MAX_RETRIES` | `maxRetries` | `2` | Retries for 408, 425, 429, and 5xx responses, 0–5. |
168
+ | `DEEPSEEK_RETRY_BASE_DELAY_MS` | `retryBaseDelayMs` | `250` | Base delay for exponential backoff. `Retry-After` is honored up to 10 seconds. |
146
169
 
147
170
  Invalid values fall back to the shared default.
148
171
 
@@ -0,0 +1,109 @@
1
+ # Configuration guide
2
+
3
+ The one reference for how every knob resolves on every host: the layering
4
+ chain, the shared config file, the full knob vocabulary, and which layer to set.
5
+ Behavior (what search and fetch do) lives in the README's
6
+ [General behavior](../README.md#general-behavior); per-host mechanics (output
7
+ caps, deadlines, how the environment reaches a stdio server) live in the host
8
+ guides: [Codex](agents/codex.md), [Claude Code](agents/claude-code.md),
9
+ [Pi](agents/pi.md),
10
+ [OpenCode](agents/opencode-v2.md).
11
+
12
+ ## The resolution chain
13
+
14
+ Every knob resolves through one chain, low → high, identically on every host:
15
+
16
+ ```
17
+ built-in defaults < shared config file < DEEPSEEK_* environment < host-native config
18
+ ```
19
+
20
+ A higher layer always wins, so nothing already configured loses effect. The
21
+ host-native layer means OpenCode's plugin options (`opencode.jsonc`), Pi's
22
+ `deepseekWebRetrieval` settings namespace, and — for MCP hosts — the per-server
23
+ `env` block in the host's registration (that block is how an environment
24
+ variable arrives, not a fifth layer).
25
+
26
+ ## The shared config file
27
+
28
+ `$XDG_CONFIG_HOME/deepseek-web-retrieval/config.json` when `XDG_CONFIG_HOME` is
29
+ set (this wins on every platform, Windows included); otherwise
30
+ `~/.config/deepseek-web-retrieval/config.json` on Linux and macOS and
31
+ `%APPDATA%\deepseek-web-retrieval\config.json` on Windows. camelCase keys:
32
+
33
+ ```json
34
+ {
35
+ "count": 20,
36
+ "blockedDomains": ["pinterest.com"],
37
+ "language": "zh",
38
+ "searchLocalization": "203.0.113.7"
39
+ }
40
+ ```
41
+
42
+ Unknown keys and host-integration keys (below) draw a one-line stderr warning
43
+ and are otherwise ignored — a bad file never blocks startup. The API key never
44
+ goes in the file; it keeps its existing channels (`DEEPSEEK_API_KEY` in the
45
+ environment, or OpenCode's stored credential).
46
+
47
+ ## Knob classes
48
+
49
+ | Knob class | Shared file | Examples |
50
+ | --- | --- | --- |
51
+ | API behavior — the user↔DeepSeek relationship | yes — that is the point of the file | `count`, `blockedDomains`, `language`, `maxRetries` |
52
+ | Host-sensitive — interacts with host output caps and deadlines | carries the safe defaults; a host layer is the recommended override point | `searchTimeoutMs`, `fetchTimeoutMs`, `fetchMaxChars`, `maxSummaryChars` |
53
+ | Host integration — the host's tool landscape | rejected — warned about and ignored | `searchEnabled`, `setDefaultWebsearch`, `apiKeySource` |
54
+
55
+ Keep host-independent intent in the shared file; prefer a host layer when a
56
+ host-sensitive knob would cross its host's cap or deadline.
57
+
58
+ ## The knob vocabulary
59
+
60
+ One knob, two names: the camelCase key for the shared file and OpenCode's
61
+ options, the `DEEPSEEK_*` variable for the environment. The table generates
62
+ from the same config vocabulary the adapters resolve through, so it cannot
63
+ drift from the code (`bun run docs:matrix` regenerates it; CI checks it).
64
+
65
+ <!-- generated: knob-table — edit src/core/config-summary.ts or env.ts, then run `bun run docs:matrix` -->
66
+
67
+ | File / OpenCode key | Environment | Default |
68
+ | --- | --- | --- |
69
+ | `count` | `DEEPSEEK_SEARCH_COUNT` | 10 |
70
+ | `maxResults` | `DEEPSEEK_SEARCH_MAX_RESULTS` | follows `count` (10) |
71
+ | `minRerank` | `DEEPSEEK_SEARCH_MIN_RERANK` | 0 |
72
+ | `minAuthority` | `DEEPSEEK_SEARCH_MIN_AUTHORITY` | 0 |
73
+ | `allowUnknownAuthority` | `DEEPSEEK_SEARCH_ALLOW_UNKNOWN_AUTHORITY` | true |
74
+ | `blockedDomains` | `DEEPSEEK_SEARCH_BLOCKED_DOMAINS` | unset |
75
+ | `includeDomains` | `DEEPSEEK_SEARCH_INCLUDE_DOMAINS` | unset |
76
+ | `language` | `DEEPSEEK_SEARCH_LANGUAGE` | unset |
77
+ | `searchLocalization` | `DEEPSEEK_SEARCH_LOCALIZATION` | unset |
78
+ | `maxSummaryChars` | `DEEPSEEK_SEARCH_MAX_SUMMARY_CHARS` | 1200 |
79
+ | `searchTimeoutMs` | `DEEPSEEK_SEARCH_TIMEOUT_MS` | 30000 |
80
+ | `fetchMaxChars` | `DEEPSEEK_FETCH_MAX_CHARS` | 16000 |
81
+ | `fetchAllowedDomains` | `DEEPSEEK_FETCH_ALLOWED_DOMAINS` | unset |
82
+ | `fetchBlockedDomains` | `DEEPSEEK_FETCH_BLOCKED_DOMAINS` | unset |
83
+ | `fetchTimeoutMs` | `DEEPSEEK_FETCH_TIMEOUT_MS` | 30000 |
84
+ | `fetchBlockPrivateHosts` | `DEEPSEEK_FETCH_BLOCK_PRIVATE_HOSTS` | true |
85
+ | `maxRetries` | `DEEPSEEK_MAX_RETRIES` | 2 |
86
+ | `retryBaseDelayMs` | `DEEPSEEK_RETRY_BASE_DELAY_MS` | 250 |
87
+
88
+ <!-- /generated: knob-table -->
89
+
90
+ ## Special semantics
91
+
92
+ - **`maxResults` follows `count`** when left unset — configure only `count` and
93
+ the cap moves with it. An explicit `maxResults` still caps; `count` never
94
+ follows `maxResults`.
95
+ - **Some switches are false-only.** `allowUnknownAuthority`,
96
+ `fetchBlockPrivateHosts`, `searchEnabled`, and `fetchEnabled` are on by
97
+ default; the environment can only turn them off (`false`, `0`, `no`, or
98
+ `off`). The last two have no file key — they are host-integration keys — so
99
+ they exist only as `DEEPSEEK_ENABLE_SEARCH` and `DEEPSEEK_ENABLE_FETCH`. A
100
+ value like `=true` changes nothing.
101
+ - **Invalid environment values are dropped** at startup and the default
102
+ applies. `npx deepseek-web-retrieval config` reports every dropped or
103
+ unknown `DEEPSEEK_*` variable as a warning and exits 1, alongside file
104
+ problems.
105
+ - **`searchLocalization` must be a valid IPv4 or IPv6 address**; anything else
106
+ is dropped at option resolution. It is deployment-level configuration, never
107
+ a per-call tool parameter.
108
+ - **Domains** in the environment are comma-separated; entries that are not
109
+ domains are dropped (and reported by `config`).
@@ -0,0 +1,180 @@
1
+ # Configuration layering design
2
+
3
+ Design note for the shared configuration base layer across the three shipped adapters (MCP
4
+ server, OpenCode plugin, Pi extension). It records the parameter taxonomy, the precedence
5
+ model, and the per-adapter composition so the design survives between conversations. Host
6
+ capability facts live in [control-matrix.md](../research/control-matrix.md); the config
7
+ surface inventory was verified against the code on 2026-10-23.
8
+
9
+ ## Problem
10
+
11
+ The state this design replaced — recorded here as history, not as the present. The option
12
+ pipeline was already single-sourced (names, defaults, clamping in
13
+ `src/core/constants.ts` → `src/core/env.ts` → `src/core/options.ts`), but the *channels*
14
+ that fed it were fragmented per host, and a multi-host user configured the same intent in
15
+ up to three ways:
16
+
17
+ - **Pi** — extension runs in-process; `DEEPSEEK_*` shell variables just worked.
18
+ - **Codex** — stdio children do not inherit the parent environment, and the installer's
19
+ `env_vars` entry forwarded **only** `DEEPSEEK_API_KEY`; every other knob set in the
20
+ shell never reached the server unless the user hand-edited `config.toml` to add each
21
+ variable name. (The entry now forwards every `DEEPSEEK_*` name — see the Codex
22
+ consequence in [ADR-0002](../adr/0002-shared-config-base-layer.md).)
23
+ - **OpenCode** — the plugin resolved **only** `ctx.options` (opencode.json plugin
24
+ options, camelCase); `DEEPSEEK_*` variables configured nothing there, so the same
25
+ knob had a second, differently-named vocabulary. (The plugin now composes
26
+ file < environment < `ctx.options` — see the per-adapter composition below.)
27
+
28
+ One intent ("20 results, block pinterest.com"), three configuration acts, two
29
+ vocabularies, one silent-failure trap. The isolation was accidental, not chosen.
30
+
31
+ ## Parameter taxonomy
32
+
33
+ Isolation versus unification is decided per class, not globally:
34
+
35
+ | Class | Parameters | Policy |
36
+ | --- | --- | --- |
37
+ | **API behavior** — describes the user↔DeepSeek relationship, host-independent | `count`, `maxResults`, `minRerank`, `minAuthority`, `allowUnknownAuthority`, `blockedDomains`, `includeDomains`, `language`, `searchLocalization`, `maxRetries`, `retryBaseDelayMs` | Shared base; identical everywhere unless a host layer overrides |
38
+ | **Host-sensitive** — interacts with host output caps / deadlines (OpenCode 2,000 lines / 50 KiB; Pi MCP 20 KB + 60 s; Pi extension uncapped; Claude Code 25k tokens) | `searchTimeoutMs`, `fetchTimeoutMs`, `fetchMaxChars`, `maxSummaryChars` | Shared base gives the safe default; **per-host override in the host-native layer is the recommended place** to raise them |
39
+ | **Host integration** — describes the host's tool landscape, not the API | `searchEnabled`, `fetchEnabled`, `setDefaultWebsearch`, `fetchOverrideWebfetch`, `fetchRegisterTool`, `apiKeySource` | **Deliberately isolated.** Installer-written, host-native config only; the shared file rejects them |
40
+
41
+ Security-shaped knobs (`fetchAllowedDomains`, `fetchBlockedDomains`, `fetchBlockPrivateHosts`)
42
+ are classed as API behavior: they are deployment policy about the DeepSeek call itself, and a
43
+ shared base strengthens rather than weakens them (one policy instead of three drifting ones).
44
+
45
+ ## The layering model
46
+
47
+ Precedence, low → high, identical semantics in every adapter:
48
+
49
+ ```
50
+ built-in defaults < shared config file < DEEPSEEK_* environment < host-native config
51
+ ```
52
+
53
+ - **Host-native config** means: OpenCode `ctx.options` (opencode.json plugin entry), Pi's
54
+ `deepseekWebRetrieval` settings namespace (the two switches), and — for MCP hosts — the
55
+ per-server `env` block in the host's MCP registration (that block is *how* an environment
56
+ variable arrives, not a fifth layer).
57
+ - Host-native layers keep outranking the base: installer-written intent (capability tiers)
58
+ and explicit per-host overrides always win, so the blast radius of a bad shared value is
59
+ bounded by whatever the host layer pins.
60
+ - The two vocabularies stay as they are — `DEEPSEEK_*` snake-case names for the environment,
61
+ camelCase `PluginOptions` keys for the file and OpenCode options — both generated from the
62
+ same single source and documented side by side.
63
+
64
+ ## The shared config file
65
+
66
+ - **Path:** `$XDG_CONFIG_HOME/deepseek-web-retrieval/config.json`, defaulting to
67
+ `~/.config/deepseek-web-retrieval/config.json`. On win32 an explicit, non-empty
68
+ `XDG_CONFIG_HOME` still wins; otherwise the conventional Roaming root applies —
69
+ `%APPDATA%\deepseek-web-retrieval\config.json`, falling back to
70
+ `<home>\AppData\Roaming\deepseek-web-retrieval\config.json` when `%APPDATA%` is
71
+ unset. User scope, one file. A project-scope file
72
+ is a confirmed non-goal for this iteration (open question below).
73
+ - **Format:** JSON object; keys are the camelCase `PluginOptions` vocabulary (class 1 and 2
74
+ only). Example:
75
+
76
+ ```json
77
+ {
78
+ "count": 20,
79
+ "blockedDomains": ["pinterest.com"],
80
+ "language": "zh",
81
+ "searchLocalization": "203.0.113.7"
82
+ }
83
+ ```
84
+
85
+ - **Validation:** the file's parsed object feeds the existing `resolvePluginOptions` —
86
+ clamps, IP validation, and domain normalization all come along for free. Unknown keys and
87
+ class-3 (host-integration) keys are **rejected with a one-line stderr warning** and
88
+ otherwise ignored; a missing or unparseable file contributes nothing (a warning line for
89
+ the unparseable case), mirroring the Pi settings reader's conventions.
90
+ - **The API key is deliberately absent.** Auth keeps its existing channels
91
+ (`DEEPSEEK_API_KEY`, OpenCode's auth store, Pi's env) — a config file is liable to end up
92
+ synced through dotfiles, and widening key storage is a security-surface decision this
93
+ design does not make.
94
+
95
+ ## Per-adapter composition
96
+
97
+ - **MCP server** (`src/mcp/serve.ts`): `resolveEnvOptions(env)` becomes
98
+ `resolveEnvOptions(env, readFileOptions())` — file keys under environment keys. The server
99
+ reads the file itself, so **the Codex `env_vars` trap disappears** regardless of what the
100
+ host forwards.
101
+ - **Pi extension** (`src/pi/extension.ts`): `resolveExtensionOptions` inserts the file
102
+ between defaults and the environment; the settings namespace stays on top, unchanged.
103
+ - **OpenCode plugin** (`src/opencode/plugin.ts`): `resolvePluginOptions(ctx.options)` becomes
104
+ `resolvePluginOptions({ ...fileOptions(), ...pluginOptionsFromEnv(process.env), ...ctx.options })`
105
+ — file, then environment, then the host layer. Additive only: existing opencode.json
106
+ options keep winning exactly as before; the change is that the file and environment now
107
+ work on this host too.
108
+
109
+ ## Installer changes
110
+
111
+ - **Codex `env_vars`:** enumerate **every** `DEEPSEEK_*` name (generated from
112
+ `src/core/constants.ts`, the single source) instead of only `DEEPSEEK_API_KEY`, so
113
+ environment overrides work on Codex without hand-editing `config.toml`. The note the
114
+ installer prints is updated to match.
115
+ - No other host artifacts change: the installer never writes the shared file (it is a user
116
+ authoring surface, like a shell profile), and capability tiers stay where they are.
117
+
118
+ ## Visibility
119
+
120
+ A bad shared value now fails everywhere at once, so effective configuration must be visible:
121
+
122
+ - The MCP server's startup stderr line (already printing the client identity and enabled
123
+ tools) additionally prints **deviations from defaults only** — e.g.
124
+ `config: count=20 blockedDomains=1 language=zh` — keeping the line short while showing
125
+ every non-default knob a multi-layer resolution produced.
126
+ - The OpenCode plugin and Pi extension print the same deviation summary once, at
127
+ registration time, through their host's logging channel (implementer picks the least
128
+ noisy channel per host; the content contract is "non-default effective values").
129
+ - `deepseek-web-retrieval config` is the user-facing arm: it prints every knob with its
130
+ value and source, replays the file's warnings where a human actually looks, and its
131
+ `--search` variant verifies the whole chain (key, config, network) with one billed
132
+ request.
133
+
134
+ ## What this design does not change
135
+
136
+ - `need_content` stays hardcoded `false` ([search-inline-content.md](../research/search-inline-content.md)).
137
+ - `ori_question` remains per-call-only (`original_question` input / Pi session fallback); it
138
+ is not configurable.
139
+ - Auth surfaces, capability tiers, and the installer's plan/confirm flow.
140
+
141
+ ## Testing
142
+
143
+ - Unit: file reader (missing / unparseable / unknown-key / class-3-key rejection with
144
+ warning), precedence matrix per adapter (defaults < file < env < host) asserted through
145
+ each adapter's existing resolution entry points.
146
+ - The installer's Codex plan snapshot gains the full `env_vars` list.
147
+ - Existing suites stay green; e2e scenarios unaffected (the file is absent in CI, which is
148
+ itself the empty-layer case).
149
+
150
+ ## Rollout
151
+
152
+ Additive and non-breaking: no existing configuration loses effect, no precedence between
153
+ existing layers changes. Documentation updates ship with the code: README gains a
154
+ "Configuration" section with the precedence table and the two-vocabulary table; the host
155
+ guides' option tables gain a shared-file column; the control-matrix note's Pi sentence
156
+ ("There is no per-extension options channel: the runtime config surface is the
157
+ `DEEPSEEK_*` environment") is updated to name the file as part of that surface.
158
+
159
+ **Amendment, 2026-10-23** — two changes approved after this design was confirmed alter
160
+ the effective behavior of *untouched* configurations, superseding the "no existing
161
+ configuration loses effect" sentence for those two knobs:
162
+
163
+ 1. `maxResults` now follows the effective `count` when unset, so `count`-only
164
+ configurations return `count` results instead of silently truncating to the old
165
+ default of 10 (the one-directional asymmetry is documented at the resolution site in
166
+ `src/core/options.ts`).
167
+ 2. Transient failures (408/425/429/5xx) retry twice by default instead of zero
168
+ (rationale and the dated vendor-survey correction live in
169
+ `docs/research/search-api-defaults.md`).
170
+
171
+ ## Decisions (confirmed 2026-10-23)
172
+
173
+ 1. **Project-scope file:** not in this iteration — hosts already own project-scope
174
+ semantics, and the extra precedence row is not yet earned by demand.
175
+ 2. **Invalid-key warnings:** one stderr line per bad key at first resolution — the noise
176
+ buys typo detection (`"retries"` vs `"maxRetries"`) in a hand-edited file.
177
+ 3. **File naming:** `config.json` inside the `~/.config/deepseek-web-retrieval/`
178
+ directory (win32: `%APPDATA%\deepseek-web-retrieval\`) — room for future siblings.
179
+ 4. **ADR:** the layering decision is recorded as
180
+ [ADR 0002](../adr/0002-shared-config-base-layer.md) alongside this design note.
@@ -1,13 +1,13 @@
1
1
  # Installer design
2
2
 
3
3
  Design note for the install-time flow, shipped as the `deepseek-web-retrieval init`,
4
- `uninstall`, and `list` subcommands for **OpenCode, Pi, and Codex**. It records how the
5
- [control tiers](../research/control-matrix.md) map to the artifacts the installer emits, so
6
- the design survives between conversations. Research backing lives in
4
+ `uninstall`, and `list` subcommands for **OpenCode, Pi, Codex, and Claude Code**. It records how the
5
+ [control tiers](../research/control-matrix.md) map to the artifacts the installer emits, so the design
6
+ 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
9
  Examples involving other hosts describe design requirements for future modules;
10
- the current CLI accepts only the three shipped hosts above.
10
+ the current CLI accepts only the four shipped hosts above.
11
11
 
12
12
  ## Principles
13
13
 
@@ -44,9 +44,9 @@ already exists, and the installer uses three phases:
44
44
  Report the conflict surface: existing rules steering toward another web tool; other
45
45
  web-capable MCP servers already on the field; array-valued keys the plan must merge
46
46
  into rather than replace (`disabledTools`, `permissions.deny`, permissions
47
- `exclude`, `disabled_tools`); and higher layers (Claude Code managed policy, Devin
48
- enterprise team settings) that would outrank what the
49
- installer writes.
47
+ `exclude`, `disabled_tools`); and higher layers (Claude Code managed policy — its
48
+ existence is checked by the claude-code module; Devin enterprise team settings) that
49
+ would outrank what the installer writes.
50
50
  2. **Merge or snapshot.** Array keys merge with existing entries, recording exactly
51
51
  which entries the installer appended; single-value keys (`websearch.provider`, Pi's
52
52
  `exposure`) snapshot the prior value before overwriting; files the installer creates
@@ -71,7 +71,9 @@ already exists, and the installer uses three phases:
71
71
  competitor, not third-party web MCP servers — those must be excluded too (the
72
72
  per-host keys cover them), or routing is not actually forced. Concretely: Pi's
73
73
  per-server `enabled` key lets the plan disable detected web-capable competitors
74
- (each prior value snapshotted for uninstall); Codex has no per-server kill switch,
74
+ (each prior value snapshotted for uninstall); Claude Code excludes them the same way
75
+ via server-level `mcp__<server>` deny rules appended to `permissions.deny`; Codex has
76
+ no per-server kill switch,
75
77
  so a detected web-capable competitor makes the plan refuse the tier instead of
76
78
  promising non-exclusive routing; OpenCode's forced is replacement, which no
77
79
  third-party server overrides. Detection classifies a server web-capable from its
@@ -156,9 +158,9 @@ the artifacts below name the path where they differ.
156
158
  | Optional | Host config registering the integration: Pi's extension path appends an `npm:<package>` source to `packages` in settings.json; the MCP paths register the server (Pi's MCP entry needs `"exposure": "direct"`; the extension path needs nothing — extension tools are `direct` by Pi's own default) |
157
159
  | Preferred | The above plus a rules file for the host (file names per host in the [per-host reference](../research/control-matrix.md#per-host-reference)); the rules name the path's tool ids (`deepseek_web_search` for the extension, `mcp__deepseek__deepseek_web_search` for MCP) |
158
160
  | Forced (replacement) | OpenCode plugin options (`setDefaultWebsearch` / `fetchOverrideWebfetch`) |
159
- | Forced (exclusion) | The built-in competitor's kill switch (per-host keys in the per-host reference), plus the exclusion of any other web-capable MCP servers the detect phase found — Pi sets their `enabled` key to `false`; Codex, lacking a per-server kill switch, refuses the tier instead; Pi's extension path additionally refuses when a web-capable `packages`/`extensions` source is listed in settings.json (no per-package kill switch exists) |
161
+ | Forced (exclusion) | The built-in competitor's kill switch (per-host keys in the per-host reference), plus the exclusion of any other web-capable MCP servers the detect phase found — Pi sets their `enabled` key to `false`; Claude Code appends server-level `mcp__<server>` deny rules to `permissions.deny` (merging with the user's own entries); Codex, lacking a per-server kill switch, refuses the tier instead; Pi's extension path additionally refuses when a web-capable `packages`/`extensions` source is listed in settings.json (no per-package kill switch exists) |
160
162
  | Forced (de-facto) | Nothing beyond the Optional artifacts — no competitor exists |
161
- | Off | Codex writes the capability's raw tool name into the server entry's `disabled_tools` (host-native: the tool leaves the model's context; the user's own entries in that array are kept); the other MCP entry writes the capability's false-only server-side switch instead (`DEEPSEEK_ENABLE_SEARCH` / `DEEPSEEK_ENABLE_FETCH`); Pi's extension path writes the same switches into the `deepseekWebRetrieval` settings namespace the extension reads at load; both capabilities off refuses to install |
163
+ | Off | Codex writes the capability's raw tool name into the server entry's `disabled_tools` (host-native: the tool leaves the model's context; the user's own entries in that array are kept); Claude Code appends the capability's full tool id (`mcp__deepseek__*`) to `permissions.deny`, the same host-native removal; the other MCP entry writes the capability's false-only server-side switch instead (`DEEPSEEK_ENABLE_SEARCH` / `DEEPSEEK_ENABLE_FETCH`); Pi's extension path writes the same switches into the `deepseekWebRetrieval` settings namespace the extension reads at load; both capabilities off refuses to install |
162
164
 
163
165
  OpenCode additionally appends `permissions` allow entries for exactly the tools it
164
166
  registers (`websearch`, `deepseek_fetch`, and `webfetch` when fetch is forced), merged
@@ -194,10 +196,19 @@ into any existing `permissions` array rather than replacing it.
194
196
  reverts cannot re-enable a still-excluded competitor.
195
197
  - Verify is file-level: after apply it re-reads each written file and asserts every op
196
198
  holds (set values in place, appended entries present, rules block present). Higher
197
- layers would be a plan-time detect concern, and none of the three shipped hosts
198
- documents a layer above user config, so there is nothing to check there yet.
199
+ layers are a plan-time detect concern; Claude Code documents one (managed policy), and
200
+ the claude-code module reports an existing managed-settings.json instead of failing
201
+ silently. The other shipped hosts document no layer above user config.
199
202
  - Only a bare `--apply` enables writes; assigning a value such as `--apply=false`
200
203
  is a usage error and writes nothing.
204
+ - `--pin` is opt-in version pinning: the ref written into host config gets the
205
+ running CLI's version appended (`name` becomes `name@<version>`; a ref already
206
+ ending in a version keeps it), so a host resolves that exact release instead of
207
+ the latest publish. Without the flag the ref stays unpinned, byte-identical to
208
+ before; re-running `init --pin` from a newer CLI moves the pin. Ref identity —
209
+ orphan, upgrade, and unexcludable-competitor detection — compares package names
210
+ with the `@version` suffix stripped (`samePackageName` in ownership.ts), so a
211
+ pinned ref still matches the lineage an earlier init registered.
201
212
  - File verification precedes the install-record commit. A write or verification
202
213
  failure restores the pre-apply files and record and checks the restored values.
203
214
  `ApplyError.status` is `rolled_back` only after recovery and new-claim cleanup
@@ -216,7 +227,7 @@ host-side tool filtering.
216
227
 
217
228
  ## Hosts without an installer story
218
229
 
219
- Claude Code, Cursor, Devin, GitHub Copilot, and Cline still have no
230
+ Cursor, Devin, GitHub Copilot, and Cline still have no
220
231
  host module; their artifacts (from the tier-to-artifact mapping) remain hand-edited
221
232
  config until a module lands here. OpenCode's `opencode plugin add` remains available but
222
233
  cannot pass options or manage snapshots; `init` supersedes it for tiered installs.
package/docs/hosts.jsonc CHANGED
@@ -102,11 +102,15 @@
102
102
  "nativeSearchProvider": false,
103
103
  "nativeFetchOverride": false,
104
104
  "paths": ["mcp"],
105
- "status": "no-adapter-yet",
105
+ "status": "supported",
106
106
  "control": {
107
107
  "search": { "optional": "yes", "preferred": "yes", "forced": "exclusion", "key": "`permissions.deny: [\"WebSearch\"]` removes the tool from context" },
108
108
  "fetch": { "optional": "yes", "preferred": "yes", "forced": "exclusion", "key": "`permissions.deny: [\"WebFetch\"]`, or `CLAUDE_CODE_DISABLE_WEB_FETCH=1`" }
109
109
  },
110
+ "adapter": "src/mcp",
111
+ "installer": "src/installer/hosts/claude-code",
112
+ "guide": "docs/agents/claude-code.md",
113
+ "e2eScenario": "claude-code"
110
114
  },
111
115
  {
112
116
  "name": "cursor",
@@ -17,7 +17,7 @@ ship built-in web tools. MCP tools can add equivalent tools, but do not replace
17
17
  | Native tool / fetch override | No | Yes | Host tool API | OpenCode v2 adapter |
18
18
  | Custom tool plugin | Yes | Yes | Host plugin API | Cline CLI/SDK; no adapter yet |
19
19
  | Extension API | Yes | Yes | Host extension API | Pi adapter (`src/pi/`) |
20
- | MCP server | Yes | Yes | Host MCP client | Shipped (`src/mcp/`; Pi and Codex guides) |
20
+ | MCP server | Yes | Yes | Host MCP client | Shipped (`src/mcp/`; Pi, Codex, and Claude Code guides) |
21
21
  | CLI/HTTP wrapper | Yes | Yes | Host can call external tools | Not shipped |
22
22
 
23
23
  MCP is a protocol, not an agent. It is one possible transport for hosts that support MCP
@@ -35,7 +35,7 @@ Rows are sorted with open-source hosts first, then by host name alphabetically.
35
35
  | Codex | 0.163.0 | Yes (Apache-2.0) | No | No | MCP server | Supported |
36
36
  | OpenCode | 2.0.32 | Yes (MIT) | Yes | Yes | native websearch provider or native tool/fetch override | Supported |
37
37
  | Pi | 1.3.0 | Yes (MIT) | No | No | extension API or MCP server | Supported |
38
- | Claude Code | 2.1.303 | No | No | No | MCP server | No adapter yet |
38
+ | Claude Code | 2.1.303 | No | No | No | MCP server | Supported |
39
39
  | Cursor | 2026.10.08 | No | No | No | MCP server | No adapter yet |
40
40
  | Devin | 3000.11.92 | No | No | No | MCP server | No adapter yet |
41
41
  | GitHub Copilot | 1.0.98 | No | No | No | MCP server | No adapter yet |
@@ -48,7 +48,7 @@ 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
- OpenCode, Pi, and Codex have supported setup guides and installer host modules. The
51
+ OpenCode, Pi, Codex, and Claude Code have supported setup guides and installer host modules. The
52
52
  MCP server in `src/mcp/` provides a possible stdio path for the remaining hosts, but
53
53
  protocol compatibility alone does not establish a tested host setup. For those rows,
54
54
  `No adapter yet` means no supported host-specific adapter or setup guide is shipped.
@@ -66,8 +66,10 @@ the details: built-in web tools and their kill switches, MCP registration, and r
66
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
- - **Claude Code** — Ships built-in `WebSearch` and `WebFetch` tools. See
70
- [Claude Code](research/control-matrix.md#claude-code).
69
+ - **Claude Code** — Ships built-in `WebSearch` and `WebFetch` tools. Supported through
70
+ the MCP server; see
71
+ [Claude Code](agents/claude-code.md) and
72
+ [Claude Code in the per-host reference](research/control-matrix.md#claude-code).
71
73
  - **Codex** — Ships a built-in `web_search` tool. Supported through the MCP server; see
72
74
  [Codex](agents/codex.md) and [Codex in the per-host reference](research/control-matrix.md#codex).
73
75
  - **Cursor** — Uses the same `mcp.json` servers as the editor; ships built-in web search
@@ -94,9 +96,12 @@ For an installation managed by `init`:
94
96
  needed for uninstall. A global install can still claim shared configuration from a
95
97
  particular project, so keep the original working directory for upgrades.
96
98
  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.
99
+ guide. For Pi's extension path, use `pi update --extensions`. To pin the host's
100
+ package to the exact version being run, pass `--pin` to init (it writes
101
+ `deepseek-web-retrieval@X.Y.Z` into the registration); upgrading a pinned install
102
+ means re-running init with the newer CLI, since updating the CLI alone does not
103
+ move an independently pinned host package. `--package` still overrides the ref
104
+ entirely.
100
105
 
101
106
  When upgrading from 0.4.0, Pi's default changes from MCP to the native extension.
102
107
  Pass `--path mcp` explicitly to retain MCP, or `--path extension` to migrate; the plan
@@ -116,7 +121,8 @@ Code importing the core's removed `formatFetchContent` helper must migrate to
116
121
  ## Available paths and future hosts
117
122
 
118
123
  - **MCP server** — shipped in `src/mcp/` (the fallback path in the [Pi](agents/pi.md) guide
119
- and the path in the [Codex](agents/codex.md) guide); the broadest route for hosts that
124
+ and the path in the [Codex](agents/codex.md) and
125
+ [Claude Code](agents/claude-code.md) guides); the broadest route for hosts that
120
126
  support MCP tools. Protocol compatibility across these hosts is researched in
121
127
  [mcp-compatibility.md](research/mcp-compatibility.md).
122
128
  - **Native extension adapters** — the Pi extension adapter ships in `src/pi/`; a host whose