deepseek-web-retrieval 0.6.0 → 0.7.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 (44) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/README.md +79 -10
  3. package/dist/cli.js +2203 -1826
  4. package/docs/adr/0002-shared-config-base-layer.md +43 -0
  5. package/docs/agents/codex.md +48 -27
  6. package/docs/agents/opencode-v2.md +57 -33
  7. package/docs/agents/pi.md +48 -27
  8. package/docs/design/config-layering-design.md +180 -0
  9. package/docs/design/installer-design.md +8 -0
  10. package/docs/integration.md +6 -3
  11. package/docs/research/control-matrix.md +15 -2
  12. package/docs/research/mcp-compatibility.md +4 -1
  13. package/docs/research/ori-question-sourcing.md +336 -0
  14. package/docs/research/result-quality-passthrough.md +159 -0
  15. package/docs/research/search-api-defaults.md +18 -2
  16. package/docs/research/search-inline-content.md +244 -0
  17. package/examples/opencode.global.jsonc +3 -2
  18. package/package.json +2 -1
  19. package/src/core/auth.ts +3 -0
  20. package/src/core/client.ts +26 -4
  21. package/src/core/config-file.ts +168 -0
  22. package/src/core/config-summary.ts +82 -0
  23. package/src/core/constants.ts +15 -1
  24. package/src/core/descriptions.ts +6 -2
  25. package/src/core/env.ts +99 -32
  26. package/src/core/format.ts +19 -1
  27. package/src/core/options.ts +22 -2
  28. package/src/core/search.ts +6 -0
  29. package/src/core/types.ts +11 -0
  30. package/src/installer/cli.ts +50 -16
  31. package/src/installer/config-command.ts +116 -0
  32. package/src/installer/hosts/codex.ts +5 -3
  33. package/src/installer/hosts/opencode.ts +9 -5
  34. package/src/installer/hosts/pi.ts +10 -6
  35. package/src/installer/ownership.ts +54 -1
  36. package/src/installer/paths.ts +7 -1
  37. package/src/installer/state.ts +72 -19
  38. package/src/installer/types.ts +10 -0
  39. package/src/mcp/auth.ts +2 -3
  40. package/src/mcp/cli.ts +4 -3
  41. package/src/mcp/serve.ts +10 -1
  42. package/src/mcp/server.ts +7 -2
  43. package/src/opencode/plugin.ts +22 -2
  44. package/src/pi/extension.ts +78 -9
@@ -0,0 +1,43 @@
1
+ # Share one config base layer across hosts
2
+
3
+ The option pipeline was single-sourced, but the channels feeding it were per
4
+ host: `DEEPSEEK_*` shell variables on Pi, only `ctx.options` on OpenCode, and
5
+ on Codex only the names the installer's `env_vars` entry forwards — just
6
+ `DEEPSEEK_API_KEY`, so every other knob set in the shell silently never
7
+ reached the server. One intent ("20 results, block pinterest.com") took up to
8
+ three configuration acts in two vocabularies. We decided that a shared config
9
+ file becomes the base layer under every existing channel, with the precedence
10
+ `built-in defaults < shared config file < DEEPSEEK_* environment < host-native
11
+ config`, a three-class parameter taxonomy that isolates host integration in
12
+ the host-native layer and rejects it from the file, and no API key in the
13
+ file — auth keeps its existing channels.
14
+
15
+ ## Considered Options
16
+
17
+ - **Unify on the file alone** (retire the environment and host-option
18
+ vocabularies so there is one configuration act). But host integration —
19
+ `searchEnabled`/`fetchEnabled`, `setDefaultWebsearch`,
20
+ `fetchOverrideWebfetch`, `fetchRegisterTool`, `apiKeySource` — describes the
21
+ host's tool landscape, not the user↔DeepSeek relationship, so the host layer
22
+ must keep owning it, and retiring `DEEPSEEK_*` would break every existing
23
+ shell profile.
24
+ - **Keep the per-host channels as the only surface** (previous status quo).
25
+ The Codex `env_vars` trap stays — knobs set in the shell reach the server
26
+ only by hand-editing `config.toml` — and the same knob keeps two
27
+ vocabularies across hosts.
28
+
29
+ ## Consequences
30
+
31
+ One file — `$XDG_CONFIG_HOME/deepseek-web-retrieval/config.json`, default
32
+ `~/.config/deepseek-web-retrieval/config.json`, camelCase keys — configures
33
+ every host, and host layers keep winning: installer-written intent and
34
+ explicit per-host overrides always outrank the base, bounding the blast
35
+ radius of a bad shared value. Unknown keys and host-integration keys in the
36
+ file draw a one-line stderr warning and are otherwise ignored. Because a bad
37
+ shared value now fails everywhere at once, every adapter prints a
38
+ deviations-from-defaults summary of its effective configuration.
39
+
40
+ The installer's Codex `env_vars` entry forwards every `DEEPSEEK_*` name, so
41
+ environment overrides work on Codex without hand-editing `config.toml`.
42
+ Design detail (per-adapter composition, validation, visibility, rollout)
43
+ lives in the [layering design note](../design/config-layering-design.md).
@@ -44,7 +44,9 @@ npx deepseek-web-retrieval init --host codex --scope global \
44
44
  ```
45
45
 
46
46
  It prints the full plan — every file, old value → new value, and notes about existing
47
- MCP servers — and writes nothing until you pass `--apply`. `uninstall` removes recorded
47
+ MCP servers — and writes nothing until you pass `--apply`. Add `--pin` to register the
48
+ exact version of the CLI you are running instead of the always-latest package name.
49
+ `uninstall` removes recorded
48
50
  additions and restores snapshotted values that have not since been edited. Codex's config
49
51
  lives in `~/.codex/config.toml` at the user level only, so the server registration and kill switch
50
52
  are written there even for `--scope project`; the rules file follows the scope. An `off`
@@ -142,45 +144,64 @@ Fetch https://learn.chatgpt.com/docs/extend/mcp and summarize the config keys.
142
144
 
143
145
  The model calls `mcp__deepseek__deepseek_web_search` or `mcp__deepseek__deepseek_fetch`.
144
146
 
147
+ The search tool accepts an optional `original_question` argument — the user's question in
148
+ their own words — forwarded as DeepSeek's `ori_question` to sharpen search intent; omit it
149
+ when the query already stands alone.
150
+
145
151
  ## Configuration
146
152
 
147
153
  Set these in the server's `env` table. Values map onto the shared option pipeline, so
148
154
  defaults and clamping match the other adapters. `DEEPSEEK_API_KEY` goes in `env_vars`.
149
155
 
156
+ > [!NOTE]
157
+ > A shared config file — `~/.config/deepseek-web-retrieval/config.json`
158
+ > (Windows: `%APPDATA%\deepseek-web-retrieval\config.json`), camelCase keys —
159
+ > sits under the environment layer and works with no env forwarding at all: the server
160
+ > reads the file itself, making it the way to set knobs without editing `env_vars` (the
161
+ > installer's `env_vars` entry also forwards every `DEEPSEEK_*` name). The file carries
162
+ > neither the API key nor the host-integration switches. Run
163
+ > `npx deepseek-web-retrieval config` to see every knob's effective value and source
164
+ > (it replays file warnings visibly and exits 1 on file problems); `config --search "<query>"`
165
+ > verifies key, config, and network with one billed search.
166
+
150
167
  ### Search
151
168
 
152
- | Env variable | Default | Description |
153
- | --- | --- | --- |
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. |
155
- | `DEEPSEEK_SEARCH_COUNT` | `10` | Results requested from DeepSeek, 1–50. Billing is per request, not per result. |
156
- | `DEEPSEEK_SEARCH_MAX_RESULTS` | `10` | Results returned to the model after filtering, 0–50. `0` returns no results. |
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. |
158
- | `DEEPSEEK_SEARCH_MIN_AUTHORITY` | `0` | Drop known authorities below this tier. Authority is 0–4. |
159
- | `DEEPSEEK_SEARCH_ALLOW_UNKNOWN_AUTHORITY` | `true` | Keep results whose authority is missing or unknown. Set `false`/`0`/`no`/`off` to drop them. |
160
- | `DEEPSEEK_SEARCH_BLOCKED_DOMAINS` | unset | Comma-separated; adds `-site:` operators to the query. |
161
- | `DEEPSEEK_SEARCH_INCLUDE_DOMAINS` | unset | Comma-separated; restricts results with `site:` operators. |
162
- | `DEEPSEEK_SEARCH_LANGUAGE` | unset | Adds a `lang:` operator, for example `zh,en`. |
163
- | `DEEPSEEK_SEARCH_MAX_SUMMARY_CHARS` | `1200` | Per-result summary cap, 0–1,000,000. |
164
- | `DEEPSEEK_SEARCH_TIMEOUT_MS` | `30000` | Search timeout, 1,000–120,000 ms. |
169
+ The "Config file" column names each key's camelCase twin in the shared config file —
170
+ one vocabulary pair, one pipeline. `—` marks host-integration entries the file rejects.
171
+
172
+ | Env variable | Config file | Default | Description |
173
+ | --- | --- | --- | --- |
174
+ | `DEEPSEEK_ENABLE_SEARCH` | — | `true` | Register the search tool. Only an explicit `false`/`0`/`no`/`off` disables it; at least one tool must stay enabled. |
175
+ | `DEEPSEEK_SEARCH_COUNT` | `count` | `10` | Results requested from DeepSeek, 1–50. Billing is per request, not per result. |
176
+ | `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. |
177
+ | `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. |
178
+ | `DEEPSEEK_SEARCH_MIN_AUTHORITY` | `minAuthority` | `0` | Drop known authorities below this tier. Authority is 0–4. |
179
+ | `DEEPSEEK_SEARCH_ALLOW_UNKNOWN_AUTHORITY` | `allowUnknownAuthority` | `true` | Keep results whose authority is missing or unknown. Set `false`/`0`/`no`/`off` to drop them. |
180
+ | `DEEPSEEK_SEARCH_BLOCKED_DOMAINS` | `blockedDomains` | unset | Comma-separated; adds `-site:` operators to the query. |
181
+ | `DEEPSEEK_SEARCH_INCLUDE_DOMAINS` | `includeDomains` | unset | Comma-separated; restricts results with `site:` operators. |
182
+ | `DEEPSEEK_SEARCH_LANGUAGE` | `language` | unset | Adds a `lang:` operator, for example `zh,en`. |
183
+ | `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. |
184
+ | `DEEPSEEK_SEARCH_MAX_SUMMARY_CHARS` | `maxSummaryChars` | `1200` | Per-result summary cap, 0–1,000,000. |
185
+ | `DEEPSEEK_SEARCH_TIMEOUT_MS` | `searchTimeoutMs` | `30000` | Search timeout, 1,000–120,000 ms. |
165
186
 
166
187
  ### Fetch
167
188
 
168
- | Env variable | Default | Description |
169
- | --- | --- | --- |
170
- | `DEEPSEEK_ENABLE_FETCH` | `true` | Register the fetch tool. Same false-only semantics as `DEEPSEEK_ENABLE_SEARCH`. |
171
- | `DEEPSEEK_FETCH_MAX_CHARS` | `16000` | Maximum characters returned, including the title, 0–1,000,000. |
172
- | `DEEPSEEK_FETCH_TIMEOUT_MS` | `30000` | Fetch timeout, 1,000–120,000 ms. |
173
- | `DEEPSEEK_FETCH_ALLOWED_DOMAINS` | unset | If set, only these hosts and subdomains may be fetched. |
174
- | `DEEPSEEK_FETCH_BLOCKED_DOMAINS` | unset | Always block these hosts and subdomains. Blocked wins over allowed. |
175
- | `DEEPSEEK_FETCH_BLOCK_PRIVATE_HOSTS` | `true` | Reject localhost, private IPs, `.local`, `.internal`, and similar hosts. |
189
+ | Env variable | Config file | Default | Description |
190
+ | --- | --- | --- | --- |
191
+ | `DEEPSEEK_ENABLE_FETCH` | — | `true` | Register the fetch tool. Same false-only semantics as `DEEPSEEK_ENABLE_SEARCH`. |
192
+ | `DEEPSEEK_FETCH_MAX_CHARS` | `fetchMaxChars` | `16000` | Maximum characters returned, including the title, 0–1,000,000. |
193
+ | `DEEPSEEK_FETCH_TIMEOUT_MS` | `fetchTimeoutMs` | `30000` | Fetch timeout, 1,000–120,000 ms. |
194
+ | `DEEPSEEK_FETCH_ALLOWED_DOMAINS` | `fetchAllowedDomains` | unset | If set, only these hosts and subdomains may be fetched. |
195
+ | `DEEPSEEK_FETCH_BLOCKED_DOMAINS` | `fetchBlockedDomains` | unset | Always block these hosts and subdomains. Blocked wins over allowed. |
196
+ | `DEEPSEEK_FETCH_BLOCK_PRIVATE_HOSTS` | `fetchBlockPrivateHosts` | `true` | Reject localhost, private IPs, `.local`, `.internal`, and similar hosts. |
176
197
 
177
198
  ### Credentials and retries
178
199
 
179
- | Env variable | Default | Description |
180
- | --- | --- | --- |
181
- | `DEEPSEEK_API_KEY` | required | DeepSeek API key, forwarded via `env_vars`. The server refuses to start without it. |
182
- | `DEEPSEEK_MAX_RETRIES` | `0` | Retries for 408, 425, 429, and 5xx responses, 0–5. |
183
- | `DEEPSEEK_RETRY_BASE_DELAY_MS` | `250` | Base delay for exponential backoff. `Retry-After` is honored up to 10 seconds. |
200
+ | Env variable | Config file | Default | Description |
201
+ | --- | --- | --- | --- |
202
+ | `DEEPSEEK_API_KEY` | — | required | DeepSeek API key, forwarded via `env_vars`. The server refuses to start without it. |
203
+ | `DEEPSEEK_MAX_RETRIES` | `maxRetries` | `2` | Retries for 408, 425, 429, and 5xx responses, 0–5. |
204
+ | `DEEPSEEK_RETRY_BASE_DELAY_MS` | `retryBaseDelayMs` | `250` | Base delay for exponential backoff. `Retry-After` is honored up to 10 seconds. |
184
205
 
185
206
  ## Troubleshooting
186
207
 
@@ -38,7 +38,8 @@ npx deepseek-web-retrieval init --host opencode --scope <project|global> \
38
38
  ```
39
39
 
40
40
  It prints the full plan — every file, old value → new value — and writes nothing until
41
- you pass `--apply`. It snapshots the current `websearch.provider` before the forced tier
41
+ you pass `--apply`. Add `--pin` to register the exact version of the CLI you are running
42
+ instead of the always-latest package name. It snapshots the current `websearch.provider` before the forced tier
42
43
  replaces it, merges `permissions` without touching yours, preserves comments in
43
44
  `opencode.jsonc`, and `uninstall` restores recorded values that have not since been edited.
44
45
  Search has no `preferred` tier on OpenCode (the provider is host-level config, never a
@@ -112,11 +113,14 @@ follow the [upgrade instructions](../integration.md#upgrading-existing-installs)
112
113
  **Global checkout**
113
114
 
114
115
  ```bash
115
- git clone https://github.com/gwokhou/deepseek-web-retrieval.git \
116
+ git clone https://github.com/guokhou/deepseek-web-retrieval.git \
116
117
  ~/.config/opencode/vendor/deepseek-web-retrieval
117
118
  ```
118
119
 
119
- Use `"package": "./vendor/deepseek-web-retrieval"`.
120
+ Use `"package": "./vendor/deepseek-web-retrieval"`. On Windows the global config root is
121
+ `%APPDATA%\opencode\` (unless `XDG_CONFIG_HOME` is explicitly set), so clone into
122
+ `%APPDATA%\opencode\vendor\deepseek-web-retrieval` there — the same root the installer
123
+ writes to.
120
124
 
121
125
  **Project checkout**
122
126
 
@@ -131,7 +135,8 @@ Use `"package": "./.opencode/vendor/deepseek-web-retrieval"`.
131
135
  **Auto-discovery**
132
136
 
133
137
  Clone or copy the repository into `~/.config/opencode/plugins/deepseek-web-retrieval` or
134
- `.opencode/plugins/deepseek-web-retrieval`. Auto-discovered plugins do not receive options,
138
+ `.opencode/plugins/deepseek-web-retrieval` (on Windows, `%APPDATA%\opencode\plugins\…`).
139
+ Auto-discovered plugins do not receive options,
135
140
  so `webfetch` stays in place; the `deepseek` search provider and `deepseek_fetch` are still
136
141
  added.
137
142
 
@@ -169,47 +174,66 @@ Fetch https://opencode.ai/v2/docs/build/plugins and summarize the plugin lifecyc
169
174
  The model calls `websearch` (provider `deepseek`) or `deepseek_fetch`. When
170
175
  `fetchOverrideWebfetch` is enabled, Markdown fetches go through `webfetch` instead.
171
176
 
177
+ DeepSeek's optional `ori_question` is not sent on this path: OpenCode's `websearch` tool
178
+ input is fixed to `query` at 2.0.32, so the provider has no channel for it (see the
179
+ [research note](../research/ori-question-sourcing.md)).
180
+
172
181
  ## Configuration
173
182
 
174
183
  Set these in the plugin's `options` object.
175
184
 
185
+ > [!NOTE]
186
+ > These options use the same camelCase vocabulary as the shared config file
187
+ > (`~/.config/deepseek-web-retrieval/config.json`; on Windows,
188
+ > `%APPDATA%\deepseek-web-retrieval\config.json`), with the `DEEPSEEK_*` environment in
189
+ > between: `shared file < environment < options`. Configure once in the file — it works on
190
+ > every host — and override per host here in `options`; existing `options` keep winning
191
+ > exactly as before. `npx deepseek-web-retrieval config` shows the file/env/defaults view
192
+ > of every knob (these `options` sit above it, so check them here when it differs).
193
+
176
194
  ### Search
177
195
 
178
- | Option | Default | Description |
179
- | --- | --- | --- |
180
- | `searchEnabled` | `true` | Register the DeepSeek search provider at all. Only an explicit `false` disables it. |
181
- | `count` | `10` | Results requested from DeepSeek, 1–50. Billing is per request, not per result. |
182
- | `maxResults` | `10` | Results returned to the model after filtering, 0–50. `0` returns no results. |
183
- | `minRerank` | `0` | Drop results below this relevance score. DeepSeek scores are 0–3. A result without a finite score passes while the threshold is `0` and is dropped once it is positive. |
184
- | `minAuthority` | `0` | Drop known authorities below this tier. Authority is 0–4. |
185
- | `allowUnknownAuthority` | `true` | Keep results whose authority is missing or unknown. Only an explicit `false` disables it. |
186
- | `blockedDomains` | `[]` | Add `-site:` operators to the search query. |
187
- | `includeDomains` | `[]` | Restrict results with `site:` operators. |
188
- | `language` | unset | Add a `lang:` operator, for example `zh,en`. |
189
- | `maxSummaryChars` | `1200` | Per-result summary cap, 0–1,000,000. |
190
- | `searchTimeoutMs` | `30000` | Search timeout, 1,000–120,000 ms. |
191
- | `setDefaultWebsearch` | `false` | Make `deepseek` the default websearch provider when the plugin loads. |
196
+ The "Environment" column names each option's `DEEPSEEK_*` twin — the plugin reads the
197
+ environment as the layer beneath these options, and the same camelCase keys also work
198
+ in the shared config file beneath both. `—` marks host-integration options with no
199
+ environment or file channel.
200
+
201
+ | Option | Environment | Default | Description |
202
+ | --- | --- | --- | --- |
203
+ | `searchEnabled` | `DEEPSEEK_ENABLE_SEARCH` | `true` | Register the DeepSeek search provider at all. Only an explicit `false` disables it. |
204
+ | `count` | `DEEPSEEK_SEARCH_COUNT` | `10` | Results requested from DeepSeek, 1–50. Billing is per request, not per result. |
205
+ | `maxResults` | `DEEPSEEK_SEARCH_MAX_RESULTS` | `10` | Results returned to the model after filtering, 0–50; defaults to `count` when unset. `0` returns no results. |
206
+ | `minRerank` | `DEEPSEEK_SEARCH_MIN_RERANK` | `0` | Drop results below this relevance score. DeepSeek scores are 0–3. A result without a finite score passes while the threshold is `0` and is dropped once it is positive. |
207
+ | `minAuthority` | `DEEPSEEK_SEARCH_MIN_AUTHORITY` | `0` | Drop known authorities below this tier. Authority is 0–4. |
208
+ | `allowUnknownAuthority` | `DEEPSEEK_SEARCH_ALLOW_UNKNOWN_AUTHORITY` | `true` | Keep results whose authority is missing or unknown. Only an explicit `false` disables it. |
209
+ | `blockedDomains` | `DEEPSEEK_SEARCH_BLOCKED_DOMAINS` | `[]` | Add `-site:` operators to the search query. |
210
+ | `includeDomains` | `DEEPSEEK_SEARCH_INCLUDE_DOMAINS` | `[]` | Restrict results with `site:` operators. |
211
+ | `language` | `DEEPSEEK_SEARCH_LANGUAGE` | unset | Add a `lang:` operator, for example `zh,en`. |
212
+ | `searchLocalization` | `DEEPSEEK_SEARCH_LOCALIZATION` | 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. |
213
+ | `maxSummaryChars` | `DEEPSEEK_SEARCH_MAX_SUMMARY_CHARS` | `1200` | Per-result summary cap, 0–1,000,000. |
214
+ | `searchTimeoutMs` | `DEEPSEEK_SEARCH_TIMEOUT_MS` | `30000` | Search timeout, 1,000–120,000 ms. |
215
+ | `setDefaultWebsearch` | — | `false` | Make `deepseek` the default websearch provider when the plugin loads. |
192
216
 
193
217
  ### Fetch
194
218
 
195
- | Option | Default | Description |
196
- | --- | --- | --- |
197
- | `fetchEnabled` | `true` | Register any DeepSeek fetch tooling (the separate tool and the webfetch override). |
198
- | `fetchMaxChars` | `16000` | Maximum characters returned, including the title, 0–1,000,000. |
199
- | `fetchTimeoutMs` | `30000` | Fetch timeout, 1,000–120,000 ms. |
200
- | `fetchOverrideWebfetch` | `false` | Replace the built-in `webfetch`. |
201
- | `fetchRegisterTool` | `true` | Register the separate `deepseek_fetch` tool. |
202
- | `fetchAllowedDomains` | `[]` | If non-empty, only these hosts and subdomains may be fetched. |
203
- | `fetchBlockedDomains` | `[]` | Always block these hosts and subdomains. Blocked wins over allowed. |
204
- | `fetchBlockPrivateHosts` | `true` | Reject localhost, private IPs, `.local`, `.internal`, and similar hosts. |
219
+ | Option | Environment | Default | Description |
220
+ | --- | --- | --- | --- |
221
+ | `fetchEnabled` | `DEEPSEEK_ENABLE_FETCH` | `true` | Register any DeepSeek fetch tooling (the separate tool and the webfetch override). |
222
+ | `fetchMaxChars` | `DEEPSEEK_FETCH_MAX_CHARS` | `16000` | Maximum characters returned, including the title, 0–1,000,000. |
223
+ | `fetchTimeoutMs` | `DEEPSEEK_FETCH_TIMEOUT_MS` | `30000` | Fetch timeout, 1,000–120,000 ms. |
224
+ | `fetchOverrideWebfetch` | — | `false` | Replace the built-in `webfetch`. |
225
+ | `fetchRegisterTool` | — | `true` | Register the separate `deepseek_fetch` tool. |
226
+ | `fetchAllowedDomains` | `DEEPSEEK_FETCH_ALLOWED_DOMAINS` | `[]` | If non-empty, only these hosts and subdomains may be fetched. |
227
+ | `fetchBlockedDomains` | `DEEPSEEK_FETCH_BLOCKED_DOMAINS` | `[]` | Always block these hosts and subdomains. Blocked wins over allowed. |
228
+ | `fetchBlockPrivateHosts` | `DEEPSEEK_FETCH_BLOCK_PRIVATE_HOSTS` | `true` | Reject localhost, private IPs, `.local`, `.internal`, and similar hosts. |
205
229
 
206
230
  ### Credentials and retries
207
231
 
208
- | Option | Default | Description |
209
- | --- | --- | --- |
210
- | `apiKeySource` | `"auto"` | Credential strategy: `"auto"`, `"opencode"`, or `"env"`. |
211
- | `maxRetries` | `0` | Retries for 408, 425, 429, and 5xx responses, 0–5. |
212
- | `retryBaseDelayMs` | `250` | Base delay for exponential backoff. `Retry-After` is honored up to 10 seconds. |
232
+ | Option | Environment | Default | Description |
233
+ | --- | --- | --- | --- |
234
+ | `apiKeySource` | — | `"auto"` | Credential strategy: `"auto"`, `"opencode"`, or `"env"`. |
235
+ | `maxRetries` | `DEEPSEEK_MAX_RETRIES` | `2` | Retries for 408, 425, 429, and 5xx responses, 0–5. |
236
+ | `retryBaseDelayMs` | `DEEPSEEK_RETRY_BASE_DELAY_MS` | `250` | Base delay for exponential backoff. `Retry-After` is honored up to 10 seconds. |
213
237
 
214
238
  ## Troubleshooting
215
239
 
package/docs/agents/pi.md CHANGED
@@ -42,7 +42,9 @@ npx deepseek-web-retrieval init --host pi --scope <project|global> \
42
42
  ```
43
43
 
44
44
  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
45
+ packages and MCP servers — and writes nothing until you pass `--apply`. Add `--pin` to
46
+ register the exact version of the CLI you are running instead of the always-latest
47
+ package name. The default path is
46
48
  the extension: the plan appends `npm:deepseek-web-retrieval` to the `packages` array in
47
49
  `~/.pi/agent/settings.json` (global) or `.pi/settings.json` (project), and Pi resolves the
48
50
  source at the next session start (the first launch fetches the package from npm;
@@ -102,6 +104,12 @@ Fetch https://pi.dev/docs/latest/extensions and summarize the extension lifecycl
102
104
  The model calls `deepseek_web_search` or `deepseek_fetch` — the same names over both paths;
103
105
  only the MCP fallback prefixes them with `mcp__deepseek__`.
104
106
 
107
+ The search tool accepts an optional `original_question` input — the user's question in their
108
+ own words — forwarded as DeepSeek's `ori_question` to sharpen search intent. When the model
109
+ leaves it blank, the extension falls back to the most recent user message in the session
110
+ (best-effort; truncated to the API's 128-character limit). The MCP fallback path has no
111
+ session access, so there a blank input simply omits the parameter.
112
+
105
113
  ## Configuration
106
114
 
107
115
  The runtime config surface is the same `DEEPSEEK_*` environment the MCP server reads
@@ -109,40 +117,53 @@ The runtime config surface is the same `DEEPSEEK_*` environment the MCP server r
109
117
  `deepseekWebRetrieval` settings namespace for the off switches (written by the installer;
110
118
  it outranks the environment and is preserved by Pi's own settings rewrites).
111
119
 
120
+ The shared config file (`~/.config/deepseek-web-retrieval/config.json`, camelCase keys —
121
+ on Windows, `%APPDATA%\deepseek-web-retrieval\config.json`)
122
+ also sits in this surface, layered under the environment, and needs no forwarding on any
123
+ path — the extension and the MCP server both read the file themselves. One file therefore
124
+ configures every host running this package, with this host's environment and settings
125
+ still outranking it. `npx deepseek-web-retrieval config` prints what actually resolves —
126
+ each knob's value and source, with file warnings surfaced and `--search "<query>"` running
127
+ one billed search through it all.
128
+
112
129
  ### Search
113
130
 
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. |
131
+ The "Config file" column names each key's camelCase twin in the shared config file —
132
+ one vocabulary pair, one pipeline. `—` marks host-integration entries the file rejects.
133
+
134
+ | Env variable | Config file | Default | Description |
135
+ | --- | --- | --- | --- |
136
+ | `DEEPSEEK_ENABLE_SEARCH` | — | `true` | Register the search tool. Only an explicit `false`/`0`/`no`/`off` disables it; at least one tool must stay enabled. |
137
+ | `DEEPSEEK_SEARCH_COUNT` | `count` | `10` | Results requested from DeepSeek, 1–50. Billing is per request, not per result. |
138
+ | `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. |
139
+ | `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. |
140
+ | `DEEPSEEK_SEARCH_MIN_AUTHORITY` | `minAuthority` | `0` | Drop known authorities below this tier. Authority is 0–4. |
141
+ | `DEEPSEEK_SEARCH_ALLOW_UNKNOWN_AUTHORITY` | `allowUnknownAuthority` | `true` | Keep results whose authority is missing or unknown. Set `false`/`0`/`no`/`off` to drop them. |
142
+ | `DEEPSEEK_SEARCH_BLOCKED_DOMAINS` | `blockedDomains` | unset | Comma-separated; adds `-site:` operators to the query. |
143
+ | `DEEPSEEK_SEARCH_INCLUDE_DOMAINS` | `includeDomains` | unset | Comma-separated; restricts results with `site:` operators. |
144
+ | `DEEPSEEK_SEARCH_LANGUAGE` | `language` | unset | Adds a `lang:` operator, for example `zh,en`. |
145
+ | `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. |
146
+ | `DEEPSEEK_SEARCH_MAX_SUMMARY_CHARS` | `maxSummaryChars` | `1200` | Per-result summary cap, 0–1,000,000. |
147
+ | `DEEPSEEK_SEARCH_TIMEOUT_MS` | `searchTimeoutMs` | `30000` | Search timeout, 1,000–120,000 ms. No host-side cap applies on the extension path. |
127
148
 
128
149
  ### Fetch
129
150
 
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. |
151
+ | Env variable | Config file | Default | Description |
152
+ | --- | --- | --- | --- |
153
+ | `DEEPSEEK_ENABLE_FETCH` | — | `true` | Register the fetch tool. Same false-only semantics as `DEEPSEEK_ENABLE_SEARCH`. |
154
+ | `DEEPSEEK_FETCH_MAX_CHARS` | `fetchMaxChars` | `16000` | Maximum characters returned, including the title, 0–1,000,000. The only output cap on this path. |
155
+ | `DEEPSEEK_FETCH_TIMEOUT_MS` | `fetchTimeoutMs` | `30000` | Fetch timeout, 1,000–120,000 ms. No host-side cap applies on the extension path. |
156
+ | `DEEPSEEK_FETCH_ALLOWED_DOMAINS` | `fetchAllowedDomains` | unset | If set, only these hosts and subdomains may be fetched. |
157
+ | `DEEPSEEK_FETCH_BLOCKED_DOMAINS` | `fetchBlockedDomains` | unset | Always block these hosts and subdomains. Blocked wins over allowed. |
158
+ | `DEEPSEEK_FETCH_BLOCK_PRIVATE_HOSTS` | `fetchBlockPrivateHosts` | `true` | Reject localhost, private IPs, `.local`, `.internal`, and similar hosts. Set `false`/`0` to disable. |
138
159
 
139
160
  ### Credentials and retries
140
161
 
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. |
162
+ | Env variable | Config file | Default | Description |
163
+ | --- | --- | --- | --- |
164
+ | `DEEPSEEK_API_KEY` | — | required | DeepSeek API key, read from the Pi process environment. |
165
+ | `DEEPSEEK_MAX_RETRIES` | `maxRetries` | `2` | Retries for 408, 425, 429, and 5xx responses, 0–5. |
166
+ | `DEEPSEEK_RETRY_BASE_DELAY_MS` | `retryBaseDelayMs` | `250` | Base delay for exponential backoff. `Retry-After` is honored up to 10 seconds. |
146
167
 
147
168
  Invalid values fall back to the shared default.
148
169
 
@@ -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.
@@ -198,6 +198,14 @@ into any existing `permissions` array rather than replacing it.
198
198
  documents a layer above user config, so there is nothing to check there yet.
199
199
  - Only a bare `--apply` enables writes; assigning a value such as `--apply=false`
200
200
  is a usage error and writes nothing.
201
+ - `--pin` is opt-in version pinning: the ref written into host config gets the
202
+ running CLI's version appended (`name` becomes `name@<version>`; a ref already
203
+ ending in a version keeps it), so a host resolves that exact release instead of
204
+ the latest publish. Without the flag the ref stays unpinned, byte-identical to
205
+ before; re-running `init --pin` from a newer CLI moves the pin. Ref identity —
206
+ orphan, upgrade, and unexcludable-competitor detection — compares package names
207
+ with the `@version` suffix stripped (`samePackageName` in ownership.ts), so a
208
+ pinned ref still matches the lineage an earlier init registered.
201
209
  - File verification precedes the install-record commit. A write or verification
202
210
  failure restores the pre-apply files and record and checks the restored values.
203
211
  `ApplyError.status` is `rolled_back` only after recovery and new-claim cleanup
@@ -94,9 +94,12 @@ For an installation managed by `init`:
94
94
  needed for uninstall. A global install can still claim shared configuration from a
95
95
  particular project, so keep the original working directory for upgrades.
96
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.
97
+ guide. For Pi's extension path, use `pi update --extensions`. To pin the host's
98
+ package to the exact version being run, pass `--pin` to init (it writes
99
+ `deepseek-web-retrieval@X.Y.Z` into the registration); upgrading a pinned install
100
+ means re-running init with the newer CLI, since updating the CLI alone does not
101
+ move an independently pinned host package. `--package` still overrides the ref
102
+ entirely.
100
103
 
101
104
  When upgrading from 0.4.0, Pi's default changes from MCP to the native extension.
102
105
  Pass `--path mcp` explicitly to retain MCP, or `--path extension` to migrate; the plan