deepseek-web-retrieval 0.8.0 → 0.10.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 (53) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/README.md +33 -16
  3. package/dist/cli.js +307 -174
  4. package/docs/adr/0001-refuse-drift-between-plan-and-apply.md +7 -3
  5. package/docs/adr/0002-shared-config-base-layer.md +5 -1
  6. package/docs/adr/0003-need-content-per-call-boolean.md +49 -0
  7. package/docs/agents/claude-code.md +16 -6
  8. package/docs/agents/codex.md +14 -8
  9. package/docs/agents/domain.md +2 -2
  10. package/docs/agents/issue-tracker.md +5 -5
  11. package/docs/agents/opencode-v2.md +29 -15
  12. package/docs/agents/pi.md +41 -12
  13. package/docs/configuration.md +24 -7
  14. package/docs/design/config-layering-design.md +10 -5
  15. package/docs/design/installer-design.md +57 -25
  16. package/docs/design/search-content-design.md +133 -0
  17. package/docs/integration.md +19 -16
  18. package/docs/research/control-matrix.md +39 -34
  19. package/docs/research/mcp-compatibility.md +3 -1
  20. package/docs/research/ori-question-sourcing.md +31 -20
  21. package/docs/research/result-quality-passthrough.md +9 -7
  22. package/docs/research/search-api-defaults.md +4 -2
  23. package/docs/research/search-inline-content.md +49 -9
  24. package/package.json +3 -2
  25. package/src/core/auth.ts +7 -0
  26. package/src/core/client.ts +98 -27
  27. package/src/core/config-summary.ts +3 -0
  28. package/src/core/constants.ts +30 -2
  29. package/src/core/descriptions.ts +17 -2
  30. package/src/core/env.ts +7 -1
  31. package/src/core/format.ts +35 -5
  32. package/src/core/options.ts +56 -0
  33. package/src/core/search.ts +106 -15
  34. package/src/core/types.ts +29 -0
  35. package/src/installer/apply.ts +4 -4
  36. package/src/installer/cli.ts +2 -2
  37. package/src/installer/config-command.ts +4 -3
  38. package/src/installer/hosts/claude-code.ts +3 -17
  39. package/src/installer/hosts/codex.ts +3 -17
  40. package/src/installer/hosts/index.ts +2 -2
  41. package/src/installer/hosts/opencode.ts +1 -1
  42. package/src/installer/hosts/pi.ts +14 -15
  43. package/src/installer/hosts/shared.ts +23 -0
  44. package/src/installer/ownership.ts +2 -2
  45. package/src/installer/paths.ts +2 -2
  46. package/src/installer/{state.ts → record.ts} +15 -15
  47. package/src/installer/types.ts +1 -1
  48. package/src/installer/uninstall.ts +4 -4
  49. package/src/mcp/serve.ts +1 -2
  50. package/src/mcp/server.ts +19 -19
  51. package/src/opencode/plugin.ts +15 -10
  52. package/src/pi/extension.ts +26 -17
  53. package/src/mcp/auth.ts +0 -11
@@ -1,8 +1,10 @@
1
1
  # Refuse drift between plan and apply
2
2
 
3
+ Status: Accepted — shipped in 0.3.0 (see [CHANGELOG](../../CHANGELOG.md)).
4
+
3
5
  The installer's plan previews the previous value each `set` op replaces, and the
4
6
  install record must snapshot exactly that value for uninstall to restore. We
5
- decided that apply re-verifies the current value at every `set` path still
7
+ decided that apply re-verifies that the current value at every `set` path still
6
8
  equals the plan-time `previous`, refuses with guidance when it does not
7
9
  ("config changed since the plan was rendered — re-run"), and records the
8
10
  plan-time snapshot — so the preview, the secret guard, and the record all
@@ -12,9 +14,11 @@ describe one snapshot taken once, at plan time.
12
14
 
13
15
  - **Inherit the plan-time snapshot silently.** Single source, but apply would
14
16
  overwrite an edit made between plan review and `--apply` without the user's
15
- knowledge — violating the installer's first principle.
17
+ knowledge — violating the [installer's first
18
+ principle](../design/installer-design.md#principles).
16
19
  - **Keep re-deriving `previous` at write time** (previous status quo). The
17
- preview and the record can disagree, and the secret guard, which checks the
20
+ preview and the record can disagree, and the secret guard (the
21
+ `withSecretGuard` wrapper in `src/installer/hosts/index.ts`), which checks the
18
22
  plan-time value, no longer covers what is actually recorded.
19
23
 
20
24
  ## Consequences
@@ -1,6 +1,10 @@
1
1
  # Share one config base layer across hosts
2
2
 
3
- The option pipeline was single-sourced, but the channels feeding it were per
3
+ Status: Accepted — shipped in 0.7.0 (see [CHANGELOG](../../CHANGELOG.md)).
4
+
5
+ The option pipeline was single-sourced — names, defaults, and clamping resolved through
6
+ one chain (`src/core/constants.ts` → `src/core/env.ts` → `src/core/options.ts`) — but the
7
+ channels feeding it were per
4
8
  host: `DEEPSEEK_*` shell variables on Pi, only `ctx.options` on OpenCode, and
5
9
  on Codex only the names the installer's `env_vars` entry forwards — just
6
10
  `DEEPSEEK_API_KEY`, so every other knob set in the shell silently never
@@ -0,0 +1,49 @@
1
+ # Expose need_content as the agent's per-call boolean
2
+
3
+ Status: Accepted — shipped in 0.10.0 (see [CHANGELOG](../../CHANGELOG.md)).
4
+
5
+ DeepSeek's Search API accepts a `need_content` boolean — return each result's page body —
6
+ that this repo had hardcoded `false` since the first release: the wire field arrived, the
7
+ pipeline dropped it, and no search result ever carried a page body
8
+ ([search-inline-content.md](../research/search-inline-content.md)). The same research
9
+ pinned why the value cannot travel alone: DeepSeek documents no size bound for the
10
+ returned content, host output caps run as tight as Pi's 20 KB MCP-path truncation, and
11
+ the 30 s default search timeout does not budget for a server-side page crawl. We decided
12
+ that the boolean itself becomes a per-call tool parameter on the model-facing surfaces
13
+ (the MCP server and the Pi extension), defaulting to `false`, while the deployment owns
14
+ two envelope-only knobs that apply whenever content flows: `searchContentMaxChars` (the
15
+ per-result body budget, default 1,500) and `searchContentTimeoutMs` (the timeout a
16
+ content call runs on, default 60 s). Neither knob is ever a tool parameter — the model
17
+ cannot see which host caps its output, so it cannot own a budget.
18
+
19
+ ## Considered Options
20
+
21
+ - **A tristate `searchContent: off | on | auto` config mode** gating whether the
22
+ parameter is exposed. The wire value is a boolean; `auto` is invented vocabulary, and
23
+ no per-call parameter in this repo (`count`, fetch `maxChars`) is mode-gated. The
24
+ `on` state's only real beneficiary is OpenCode, whose `websearch` provider input the
25
+ host fixes to `query` — the same upstream gap that already blocks per-call `count`
26
+ ([opencode-v2.md](../agents/opencode-v2.md)).
27
+ - **Default-on inline content** (Jina's architecture). DeepSeek documents no size bound
28
+ for `content`; a default `count=10` response at typical page sizes runs 30–110 KB
29
+ against host caps that truncate near 20 KB, and per-request billing makes the call
30
+ free, not the context bloat.
31
+ - **The previous status quo** (hardcoded `false`). An agent that needs page bodies must
32
+ guess from summaries which URLs to fetch; a wrong guess is a silent miss.
33
+
34
+ ## Consequences
35
+
36
+ The model decides per call whether a search carries page bodies — one round-trip
37
+ replaces search-plus-fetch when recall matters, and summaries stay the default when
38
+ they suffice. A page body replaces its result's digest, clamped to
39
+ `searchContentMaxChars` with an ellipsis marker, so a response's content volume is
40
+ bounded by budget × count and can never blow a host cap by an undocumented server-side
41
+ size. The text channel renders bodies as indented Markdown that keeps heading and list
42
+ structure; digests stay one-line. `searchContentMaxChars: 0` drops bodies entirely (the
43
+ digest renders) — a deployment kill switch that still pays the slower call, so leaving
44
+ the parameter false is the better tool for the model. A result whose content comes back
45
+ empty — the API documents per-result absence — falls back to its summary, so a content
46
+ call degrades to today's output rather than to nothing. OpenCode stays summary-only
47
+ until its provider input widens upstream. Design detail (budget arithmetic, rendering,
48
+ per-adapter composition) lives in the
49
+ [search content design note](../design/search-content-design.md).
@@ -38,20 +38,23 @@ The quickest option is the installer CLI:
38
38
 
39
39
  ```bash
40
40
  npx deepseek-web-retrieval init --host claude-code --scope project \
41
- --search <tier> --fetch <tier>
41
+ --search <optional|preferred|forced|off> --fetch <optional|preferred|forced|off>
42
42
  ```
43
43
 
44
44
  It prints the full plan — every file, old value → new value, and notes about existing MCP
45
45
  servers — and writes nothing until you pass `--apply`. Add `--pin` to register the exact
46
46
  version of the CLI you are running instead of the always-latest package name. `uninstall`
47
47
  removes recorded additions and restores snapshotted values that have not since been edited.
48
+
48
49
  `--scope project` registers the server in `./.mcp.json` (shareable via VCS) and writes tier
49
50
  artifacts to `./.claude/settings.json` and `./CLAUDE.md`; `--scope global` uses
50
51
  `~/.claude.json`, `~/.claude/settings.json`, and `~/.claude/CLAUDE.md` instead — the two
51
52
  scopes use disjoint files, so both can coexist. An `off` capability is written as the
52
53
  tool's full id in the `permissions.deny` array — Claude Code's own mechanism, which removes
53
54
  the tool from the model's context while the server keeps running — merging with any entries
54
- you put there yourself. When `claude` is on PATH, `init` also probes `claude --version` and
55
+ you put there yourself.
56
+
57
+ When `claude` is on PATH, `init` also probes `claude --version` and
55
58
  adds a plan note when the installed release differs from the tested lock (see
56
59
  [hosts.jsonc](../hosts.jsonc)) — version-anchored behavior such as the deny rules may have
57
60
  drifted between the two.
@@ -168,9 +171,16 @@ The model calls `mcp__deepseek__deepseek_web_search` or `mcp__deepseek__deepseek
168
171
 
169
172
  The search tool accepts an optional `original_question` argument — the user's question in
170
173
  their own words — forwarded as DeepSeek's `ori_question` to sharpen search intent; omit it
171
- when the query already stands alone.
172
-
173
- Tool output is capped at 25k tokens by default (`MAX_MCP_OUTPUT_TOKENS`), and successful
174
+ when the query already stands alone. It also takes an optional `count` (1–50) for the call,
175
+ and the fetch tool takes an optional `maxChars` (0–1,000,000) per-call output budget; each
176
+ overrides the configured default for that call, so the model can widen a truncated fetch or
177
+ ask for more results without rephrasing the query. When configured domain or language
178
+ operators change the query actually sent, the result text opens with
179
+ `Searched for: <effective query>` (plus any degradation applied to fit the API's
180
+ 128-character limit), so the model can attribute results to the configuration in effect.
181
+
182
+ Tool output is capped at 25k tokens by default (`MAX_MCP_OUTPUT_TOKENS`, a Claude Code
183
+ environment setting), and successful
174
184
  text results longer than 50,000 characters are saved to a file and referenced — not
175
185
  dropped silently.
176
186
 
@@ -249,7 +259,7 @@ one vocabulary pair, one pipeline. `—` marks host-integration entries the file
249
259
  | Claude Code | locked in [`docs/hosts.jsonc`](../hosts.jsonc) |
250
260
  | MCP revision | `2026-07-28` negotiated; server accepts `2024-11-05`–`2026-07-28` |
251
261
  | MCP SDK | `@modelcontextprotocol/server` 2.3.1 |
252
- | Runtime | Node.js 20+ (stdio) |
262
+ | Runtime | Node.js 20 or newer (stdio) |
253
263
  | DeepSeek API | `/v1/tools/search`, `/v1/tools/fetch` |
254
264
 
255
265
  Tested with Claude Code at the `docs/hosts.jsonc` lock on Linux over stdio: the keyless
@@ -17,10 +17,10 @@ The tools appear in Codex as `mcp__deepseek__deepseek_web_search` and
17
17
  [Control tiers](#control-tiers) for routing it away.
18
18
 
19
19
  The server recognizes Codex's MCP client identity (`codex-mcp-client`, declared in the
20
- initialize handshake) and returns text-only tool results: Codex replaces the content
21
- blocks with the serialized `structuredContent` JSON when it is present, so for Codex the
22
- structured mirror is omitted and the model reads clean Markdown. Other clients receive
23
- both the text and `structuredContent`.
20
+ initialize handshake) and returns text-only tool results for it. Codex replaces the
21
+ content blocks with the serialized `structuredContent` JSON when it is present, so for
22
+ Codex the structured mirror is omitted and the model reads clean Markdown. Other
23
+ clients receive both the text and `structuredContent`.
24
24
 
25
25
  For shared search/fetch limits, retries, and privacy behavior, see
26
26
  [General behavior](../../README.md#general-behavior).
@@ -42,7 +42,7 @@ The quickest option is the installer CLI:
42
42
 
43
43
  ```bash
44
44
  npx deepseek-web-retrieval init --host codex --scope global \
45
- --search <tier> --fetch <tier>
45
+ --search <optional|preferred|forced|off> --fetch <optional|preferred|forced|off>
46
46
  ```
47
47
 
48
48
  It prints the full plan — every file, old value → new value, and notes about existing
@@ -117,7 +117,7 @@ built-in fetch tool, so the tiers differ per capability:
117
117
 
118
118
  | Capability | Optional | Preferred | Forced |
119
119
  | --- | --- | --- | --- |
120
- | Search | Tools registered; the built-in `web_search` stays available | Plus an AGENTS.md rules block steering the model to `mcp__deepseek__deepseek_web_search` | Exclusion: `web_search = "disabled"` at the top level of config.toml |
120
+ | Search | Tools registered; the built-in `web_search` stays available | Plus an AGENTS.md rules block steering the model to `mcp__deepseek__deepseek_web_search` | Exclusion: `web_search = "disabled"` at the top level of `config.toml` |
121
121
  | Fetch | Tools registered | Plus the rules block | De-facto: no built-in fetch tool exists |
122
122
 
123
123
  The installer maps each tier to these artifacts. `AGENTS.md` rules land in
@@ -148,7 +148,13 @@ The model calls `mcp__deepseek__deepseek_web_search` or `mcp__deepseek__deepseek
148
148
 
149
149
  The search tool accepts an optional `original_question` argument — the user's question in
150
150
  their own words — forwarded as DeepSeek's `ori_question` to sharpen search intent; omit it
151
- when the query already stands alone.
151
+ when the query already stands alone. It also takes an optional `count` (1–50) for the call,
152
+ and the fetch tool takes an optional `maxChars` (0–1,000,000) per-call output budget; each
153
+ overrides the configured default for that call, so the model can widen a truncated fetch or
154
+ ask for more results without rephrasing the query. When configured domain or language
155
+ operators change the query actually sent, the result text opens with
156
+ `Searched for: <effective query>` (plus any degradation applied to fit the API's
157
+ 128-character limit), so the model can attribute results to the configuration in effect.
152
158
 
153
159
  ## Configuration
154
160
 
@@ -225,7 +231,7 @@ one vocabulary pair, one pipeline. `—` marks host-integration entries the file
225
231
  | Codex | locked in [`docs/hosts.jsonc`](../hosts.jsonc) |
226
232
  | MCP revision | `2025-06-18` negotiated (`mcp_2026_07_28` behind a flag); server accepts `2024-11-05`–`2026-07-28` |
227
233
  | MCP SDK | `@modelcontextprotocol/server` 2.3.1 |
228
- | Runtime | Node.js 20+ (stdio) |
234
+ | Runtime | Node.js 20 or newer (stdio) |
229
235
  | DeepSeek API | `/v1/tools/search`, `/v1/tools/fetch` |
230
236
 
231
237
  Tested with Codex 0.163.0 on Linux over stdio.
@@ -14,7 +14,7 @@ If any of these files don't exist, **proceed silently**. Don't flag their absenc
14
14
 
15
15
  Single-context repo (most repos):
16
16
 
17
- ```
17
+ ```text
18
18
  /
19
19
  ├── CONTEXT.md
20
20
  ├── docs/adr/
@@ -25,7 +25,7 @@ Single-context repo (most repos):
25
25
 
26
26
  Multi-context repo (presence of `CONTEXT-MAP.md` at the root):
27
27
 
28
- ```
28
+ ```text
29
29
  /
30
30
  ├── CONTEXT-MAP.md
31
31
  ├── docs/adr/ ← system-wide decisions
@@ -5,7 +5,7 @@ Issues and specs for this repo live as GitHub issues. Use the `gh` CLI for all o
5
5
  ## Conventions
6
6
 
7
7
  - **Create an issue**: `gh issue create --title "..." --body "..."`. Use a heredoc for multi-line bodies.
8
- - **Read an issue**: `gh issue view <number> --comments`, filtering comments by `jq` and also fetching labels.
8
+ - **Read an issue**: `gh issue view <number> --comments` for reading; add `--json body,labels,comments` (with `--jq` to project fields) when scripting.
9
9
  - **List issues**: `gh issue list --state open --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'` with appropriate `--label` and `--state` filters.
10
10
  - **Comment on an issue**: `gh issue comment <number> --body "..."`
11
11
  - **Apply / remove labels**: `gh issue edit <number> --add-label "..."` / `--remove-label "..."`
@@ -15,7 +15,7 @@ Infer the repo from `git remote -v`; `gh` does this automatically when run insid
15
15
 
16
16
  ## Pull requests as a triage surface
17
17
 
18
- **PRs as a request surface: no.** _(Set to `yes` if this repo treats external PRs as feature requests; `/triage` reads this flag.)_
18
+ **PRs as a request surface: no.** _(Maintainers only: set to `yes` if this repo treats external PRs as feature requests; the `/triage` skill reads this flag.)_
19
19
 
20
20
  When set to `yes`, PRs run through the same labels and states as issues, using the `gh pr` equivalents:
21
21
 
@@ -39,7 +39,7 @@ Used by `/wayfinder`. The **map** is a single issue with **child** issues as tic
39
39
 
40
40
  - **Map**: a single issue labelled `wayfinder:map`, holding the Notes / Decisions-so-far / Fog body. `gh issue create --label wayfinder:map`.
41
41
  - **Child ticket**: an issue linked to the map as a GitHub sub-issue (`gh api` on the sub-issues endpoint). Where sub-issues aren't enabled, add the child to a task list in the map body and put `Part of #<map>` at the top of the child body. Labels: `wayfinder:<type>` (`research`/`prototype`/`grilling`/`task`). Once claimed, the ticket is assigned to the driving dev.
42
- - **Blocking**: GitHub's **native issue dependencies**, the canonical, UI-visible representation. Add an edge with `gh api --method POST repos/<owner>/<repo>/issues/<child>/dependencies/blocked_by -F issue_id=<blocker-db-id>`, where `<blocker-db-id>` is the blocker's numeric **database id** (`gh api repos/<owner>/<repo>/issues/<n> --jq .id`, _not_ the `#number` or `node_id`). GitHub reports `issue_dependencies_summary.blocked_by` (open blockers only, the live gate). Where dependencies aren't available, fall back to a `Blocked by: #<n>, #<n>` line at the top of the child body. A ticket is unblocked when every blocker is closed.
43
- - **Frontier query**: list the map's open children (`gh issue list --state open`, scoped to the map's sub-issues / task list), drop any with an open blocker (`issue_dependencies_summary.blocked_by > 0`, or an open issue in the `Blocked by` line) or an assignee; first in map order wins.
42
+ - **Blocking**: GitHub's **native issue dependencies**, the canonical, UI-visible representation. Add an edge with `gh api --method POST repos/<owner>/<repo>/issues/<child>/dependencies/blocked_by -F issue_id=<blocker-db-id>`, where `<blocker-db-id>` is the blocker's numeric **database id** (`gh api repos/<owner>/<repo>/issues/<n> --jq .id`, _not_ the `#number` or `node_id`). Read the resulting block count from the issue's REST representation — `gh api repos/<owner>/<repo>/issues/<n> --jq .issue_dependencies_summary.blocked_by` (open blockers only, the live gate; `gh issue view --json` does not expose this field). Where dependencies aren't available, fall back to a `Blocked by: #<n>, #<n>` line at the top of the child body. A ticket is unblocked when every blocker is closed.
43
+ - **Frontier query**: list the map's open children (`gh issue list --state open`, scoped to the map's sub-issues / task list), drop any with an open blocker (`issue_dependencies_summary.blocked_by > 0` read as above, or an open issue in the `Blocked by` line) or an assignee; first in map order wins.
44
44
  - **Claim**: `gh issue edit <n> --add-assignee @me`, the session's first write.
45
- - **Resolve**: `gh issue comment <n> --body "<answer>"`, then `gh issue close <n>`, then append a context pointer (gist + link) to the map's Decisions-so-far.
45
+ - **Resolve**: `gh issue comment <n> --body "<answer>"`, then `gh issue close <n>`, then append a context pointer to the map's Decisions-so-far (link the resolved child; include the gist URL if the resolving session created one).
@@ -22,7 +22,7 @@ For configuration layers, the full knob mapping, and defaults, see the
22
22
 
23
23
  - OpenCode v2 with `@opencode/plugin` in the `^2.0.32` peer range; the tested version
24
24
  is recorded in [`docs/hosts.jsonc`](../hosts.jsonc)
25
- - Node.js 20+ and npm when using the installer CLI
25
+ - Node.js 20 or newer and npm when using the installer CLI
26
26
  - A DeepSeek account with access to `/v1/tools/search` and `/v1/tools/fetch`
27
27
  - A funded DeepSeek account; Search and Fetch are billed per successful request
28
28
  - A DeepSeek credential stored in OpenCode, or `DEEPSEEK_API_KEY`
@@ -41,11 +41,14 @@ npx deepseek-web-retrieval init --host opencode --scope <project|global> \
41
41
 
42
42
  It prints the full plan — every file, old value → new value — and writes nothing until
43
43
  you pass `--apply`. Add `--pin` to register the exact version of the CLI you are running
44
- instead of the always-latest package name. It snapshots the current `websearch.provider` before the forced tier
45
- replaces it, merges `permissions` without touching yours, preserves comments in
46
- `opencode.jsonc`, and `uninstall` restores recorded values that have not since been edited.
47
- Search has no `preferred` tier on OpenCode (the provider is host-level config, never a
48
- model choice); the CLI rejects it.
44
+ instead of the always-latest package name. It snapshots the current `websearch.provider`
45
+ before the forced tier replaces it, merges `permissions` without touching yours,
46
+ preserves comments in `opencode.jsonc`, and `uninstall` restores recorded values that
47
+ have not since been edited.
48
+
49
+ > [!NOTE]
50
+ > Search has no `preferred` tier on OpenCode: the provider is host-level config, never
51
+ > a model choice, so the CLI rejects it.
49
52
 
50
53
  ### OpenCode CLI install
51
54
 
@@ -176,9 +179,20 @@ Fetch https://opencode.ai/v2/docs/build/plugins and summarize the plugin lifecyc
176
179
  The model calls `websearch` (provider `deepseek`) or `deepseek_fetch`. When
177
180
  `fetchOverrideWebfetch` is enabled, Markdown fetches go through `webfetch` instead.
178
181
 
179
- DeepSeek's optional `ori_question` is not sent on this path: OpenCode's `websearch` tool
180
- input is fixed to `query` at 2.0.32, so the provider has no channel for it (see the
181
- [research note](../research/ori-question-sourcing.md)).
182
+ DeepSeek's optional `ori_question` is not sent on this path, and the model cannot request a
183
+ per-call result count or inline page content (`need_content`): OpenCode's `websearch` tool
184
+ input is fixed to `query` (verified unchanged at `@opencode/schema` 2.1.4, the drift check
185
+ above the 2.0.32 lock), so the provider has no channel for either — search stays
186
+ summary-only here while the MCP server and the Pi extension expose both parameters. Upstream requests:
187
+ [anomalyco/opencode#58635](https://github.com/anomalyco/opencode/issues/58635) (session linkage)
188
+ and [#58636](https://github.com/anomalyco/opencode/issues/58636) (per-call `count`); see the
189
+ [research note](../research/ori-question-sourcing.md). The fetch tools are not constrained:
190
+ they accept an optional per-call `maxChars` (0–1,000,000) that overrides the configured
191
+ `fetchMaxChars` for one call, so the model can widen the budget after seeing a
192
+ `[content truncated]` marker. The effective-query header and query degradation notes have no
193
+ channel on this path either — OpenCode's result shape is closed — but degradation still runs,
194
+ and a query that exceeds the length limit and cannot be degraded further surfaces as an
195
+ error.
182
196
 
183
197
  ## Configuration
184
198
 
@@ -202,14 +216,14 @@ environment or file channel.
202
216
 
203
217
  | Option | Environment | Default | Description |
204
218
  | --- | --- | --- | --- |
205
- | `searchEnabled` | `DEEPSEEK_ENABLE_SEARCH` | `true` | Register the DeepSeek search provider at all. Only an explicit `false` disables it. |
219
+ | `searchEnabled` | `DEEPSEEK_ENABLE_SEARCH` | `true` | Register the DeepSeek search provider at all. In `options`, only an explicit `false` disables it; the environment twin also accepts `0`/`no`/`off`. |
206
220
  | `count` | `DEEPSEEK_SEARCH_COUNT` | `10` | Results requested from DeepSeek, 1–50. Billing is per request, not per result. |
207
221
  | `maxResults` | `DEEPSEEK_SEARCH_MAX_RESULTS` | `10` | Results returned to the model after filtering, 0–50; defaults to `count` when unset. `0` returns no results. |
208
222
  | `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. |
209
223
  | `minAuthority` | `DEEPSEEK_SEARCH_MIN_AUTHORITY` | `0` | Drop known authorities below this tier. Authority is 0–4. |
210
- | `allowUnknownAuthority` | `DEEPSEEK_SEARCH_ALLOW_UNKNOWN_AUTHORITY` | `true` | Keep results whose authority is missing or unknown. Only an explicit `false` disables it. |
211
- | `blockedDomains` | `DEEPSEEK_SEARCH_BLOCKED_DOMAINS` | `[]` | Add `-site:` operators to the search query. |
212
- | `includeDomains` | `DEEPSEEK_SEARCH_INCLUDE_DOMAINS` | `[]` | Restrict results with `site:` operators. |
224
+ | `allowUnknownAuthority` | `DEEPSEEK_SEARCH_ALLOW_UNKNOWN_AUTHORITY` | `true` | Keep results whose authority is missing or unknown. In `options`, only an explicit `false` disables it; the environment twin also accepts `0`/`no`/`off`. |
225
+ | `blockedDomains` | `DEEPSEEK_SEARCH_BLOCKED_DOMAINS` | unset | Add `-site:` operators to the search query. |
226
+ | `includeDomains` | `DEEPSEEK_SEARCH_INCLUDE_DOMAINS` | unset | Restrict results with `site:` operators. |
213
227
  | `language` | `DEEPSEEK_SEARCH_LANGUAGE` | unset | Add a `lang:` operator, for example `zh,en`. |
214
228
  | `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. |
215
229
  | `maxSummaryChars` | `DEEPSEEK_SEARCH_MAX_SUMMARY_CHARS` | `1200` | Per-result summary cap, 0–1,000,000. |
@@ -225,8 +239,8 @@ environment or file channel.
225
239
  | `fetchTimeoutMs` | `DEEPSEEK_FETCH_TIMEOUT_MS` | `30000` | Fetch timeout, 1,000–120,000 ms. |
226
240
  | `fetchOverrideWebfetch` | — | `false` | Replace the built-in `webfetch`. |
227
241
  | `fetchRegisterTool` | — | `true` | Register the separate `deepseek_fetch` tool. |
228
- | `fetchAllowedDomains` | `DEEPSEEK_FETCH_ALLOWED_DOMAINS` | `[]` | If non-empty, only these hosts and subdomains may be fetched. |
229
- | `fetchBlockedDomains` | `DEEPSEEK_FETCH_BLOCKED_DOMAINS` | `[]` | Always block these hosts and subdomains. Blocked wins over allowed. |
242
+ | `fetchAllowedDomains` | `DEEPSEEK_FETCH_ALLOWED_DOMAINS` | unset | If non-empty, only these hosts and subdomains may be fetched. |
243
+ | `fetchBlockedDomains` | `DEEPSEEK_FETCH_BLOCKED_DOMAINS` | unset | Always block these hosts and subdomains. Blocked wins over allowed. |
230
244
  | `fetchBlockPrivateHosts` | `DEEPSEEK_FETCH_BLOCK_PRIVATE_HOSTS` | `true` | Reject localhost, private IPs, `.local`, `.internal`, and similar hosts. |
231
245
 
232
246
  ### Credentials and retries
package/docs/agents/pi.md CHANGED
@@ -8,7 +8,9 @@ start. The same build also ships an MCP server over stdio as the fallback path
8
8
  ([below](#the-mcp-fallback-path)); the extension is the recommended one:
9
9
 
10
10
  - The tools are declared to the model like built-ins (`exposure: "direct"` is Pi's own
11
- default for extension tools — the MCP path's codemode trap does not exist here).
11
+ default for extension tools — the MCP path defaults its tools to
12
+ `exposure: "codemode"`, which hides them from the model unless the entry opts into
13
+ `direct`; that trap does not exist here).
12
14
  - Results bypass Pi's MCP layer entirely: no 20 KB middle truncation and no 60 s
13
15
  per-request timeout; the adapter's own output cap is the only limit.
14
16
  - The key is read from the Pi process environment — no `env` forwarding, no `${VAR}`
@@ -19,7 +21,8 @@ When loaded, it adds:
19
21
  - A `deepseek_web_search` tool backed by DeepSeek Search
20
22
  - A `deepseek_fetch` tool backed by DeepSeek Fetch
21
23
 
22
- Pi 1.3.0 ships no built-in web tool, so the registered tools are the only web path. For
24
+ Pi 1.3.0 ships no built-in web tool, so the registered tools are the only web path (see
25
+ [Control tiers](#control-tiers)). For
23
26
  shared search/fetch limits, retries, and privacy behavior, see
24
27
  [General behavior](../../README.md#general-behavior).
25
28
  For configuration layers, the full knob mapping, and defaults, see the
@@ -29,7 +32,7 @@ For configuration layers, the full knob mapping, and defaults, see the
29
32
 
30
33
  - Pi with host SDKs in the `^1.3.0` peer range (`>=1.3.0 <2.0.0`); the tested
31
34
  version is recorded in [`docs/hosts.jsonc`](../hosts.jsonc)
32
- - Node.js 20+ and npm when using the installer CLI or MCP fallback
35
+ - Node.js 20 or newer and npm when using the installer CLI or MCP fallback
33
36
  - A DeepSeek account with access to `/v1/tools/search` and `/v1/tools/fetch`
34
37
  - A funded DeepSeek account; Search and Fetch are billed per successful request
35
38
  - `DEEPSEEK_API_KEY` set in the shell that launches Pi
@@ -55,7 +58,8 @@ source at the next session start (the first launch fetches the package from npm;
55
58
  to install the [MCP fallback path](#the-mcp-fallback-path) instead. `uninstall` removes
56
59
  recorded additions and restores unchanged configuration values from their snapshots.
57
60
 
58
- For upgrades from 0.4.0, select `--path mcp` explicitly to retain the old path, or use
61
+ For upgrades from a 0.4.x or earlier install (MCP was Pi's default path before 0.5.0),
62
+ select `--path mcp` explicitly to retain the old path, or use
59
63
  the extension default to migrate; follow the shared
60
64
  [upgrade instructions](../integration.md#upgrading-existing-installs).
61
65
 
@@ -86,6 +90,27 @@ pi install /absolute/path/to/deepseek-web-retrieval
86
90
  The extension entry imports only host-supplied modules (`@earendil-works/pi-coding-agent`,
87
91
  `@earendil-works/pi-ai`) and the package's own `src/core/`, so a plain clone works.
88
92
 
93
+ ## Control tiers
94
+
95
+ Pi 1.3.0 ships no built-in web tool, so the registered tools are the only web path at
96
+ every tier — forced is de-facto rather than exclusion:
97
+
98
+ | Capability | Optional | Preferred | Forced |
99
+ | --- | --- | --- | --- |
100
+ | Search | Tools registered — already the only web search | Plus an AGENTS.md rules block steering the model to the search tool | De-facto: no built-in competitor; web-capable MCP servers in `mcp.json` are disabled |
101
+ | Fetch | Tools registered — already the only web fetch | Plus the rules block | De-facto: same |
102
+
103
+ The installer maps each tier to these artifacts. The rules block lands in
104
+ `~/.pi/agent/AGENTS.md` (global) or `./AGENTS.md` (project), naming the path's tool
105
+ ids (`deepseek_web_search` for the extension, `mcp__deepseek__deepseek_web_search` for
106
+ the MCP fallback). Under a forced tier, web-capable MCP servers detected in
107
+ `~/.pi/agent/mcp.json` or `.pi/mcp.json` get `enabled: false` (each prior value
108
+ snapshotted for uninstall); a web-capable `packages` or `extensions` source in
109
+ settings.json cannot be excluded — Pi has no per-package kill switch — so `init`
110
+ refuses the tier instead of promising routing it cannot deliver. The `off` tier is the
111
+ `deepseekWebRetrieval` settings namespace described under
112
+ [Configuration](#configuration).
113
+
89
114
  ## Authentication
90
115
 
91
116
  `DEEPSEEK_API_KEY` is the only source: the extension runs inside the Pi process and reads
@@ -110,14 +135,20 @@ The search tool accepts an optional `original_question` input — the user's que
110
135
  own words — forwarded as DeepSeek's `ori_question` to sharpen search intent. When the model
111
136
  leaves it blank, the extension falls back to the most recent user message in the session
112
137
  (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.
138
+ session access, so there a blank input simply omits the parameter. The search tool also
139
+ takes an optional `count` (1–50) for the call, and the fetch tool an optional `maxChars`
140
+ (0–1,000,000) per-call output budget; each overrides the configured default for that call.
141
+ When configured domain or language operators change the query actually sent, the result text
142
+ opens with `Searched for: <effective query>` (plus any degradation applied to fit the API's
143
+ 128-character limit), so the model can attribute results to the configuration in effect.
114
144
 
115
145
  ## Configuration
116
146
 
117
147
  The runtime config surface is the same `DEEPSEEK_*` environment the MCP server reads
118
148
  (identical defaults and clamping), set in the shell that launches Pi, plus the
119
149
  `deepseekWebRetrieval` settings namespace for the off switches (written by the installer;
120
- it outranks the environment and is preserved by Pi's own settings rewrites).
150
+ it outranks the environment and is preserved by Pi's own settings rewrites). Invalid
151
+ `DEEPSEEK_*` values are dropped at startup and the shared default applies.
121
152
 
122
153
  The shared config file (`~/.config/deepseek-web-retrieval/config.json`, camelCase keys —
123
154
  on Windows, `%APPDATA%\deepseek-web-retrieval\config.json`)
@@ -157,7 +188,7 @@ one vocabulary pair, one pipeline. `—` marks host-integration entries the file
157
188
  | `DEEPSEEK_FETCH_TIMEOUT_MS` | `fetchTimeoutMs` | `30000` | Fetch timeout, 1,000–120,000 ms. No host-side cap applies on the extension path. |
158
189
  | `DEEPSEEK_FETCH_ALLOWED_DOMAINS` | `fetchAllowedDomains` | unset | If set, only these hosts and subdomains may be fetched. |
159
190
  | `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. |
191
+ | `DEEPSEEK_FETCH_BLOCK_PRIVATE_HOSTS` | `fetchBlockPrivateHosts` | `true` | Reject localhost, private IPs, `.local`, `.internal`, and similar hosts. Set `false`/`0`/`no`/`off` to disable. |
161
192
 
162
193
  ### Credentials and retries
163
194
 
@@ -167,8 +198,6 @@ one vocabulary pair, one pipeline. `—` marks host-integration entries the file
167
198
  | `DEEPSEEK_MAX_RETRIES` | `maxRetries` | `2` | Retries for 408, 425, 429, and 5xx responses, 0–5. |
168
199
  | `DEEPSEEK_RETRY_BASE_DELAY_MS` | `retryBaseDelayMs` | `250` | Base delay for exponential backoff. `Retry-After` is honored up to 10 seconds. |
169
200
 
170
- Invalid values fall back to the shared default.
171
-
172
201
  ## Troubleshooting
173
202
 
174
203
  | Error or symptom | What to do |
@@ -192,9 +221,9 @@ Invalid values fall back to the shared default.
192
221
  | Runtime | Inside the Pi process (Node, host-provided) |
193
222
  | DeepSeek API | `/v1/tools/search`, `/v1/tools/fetch` |
194
223
 
195
- Tested with Pi 1.3.0 on Linux. The extension API is version-locked research surface: the
196
- `ToolDefinition` shape and the settings `packages` key are verified at the locked version
197
- (see [the per-host reference](../research/control-matrix.md#pi)).
224
+ Tested with Pi 1.3.0 on Linux. The extension API surface this package relies on — the
225
+ `ToolDefinition` shape and the settings `packages` key — is verified at the locked
226
+ version (see [the per-host reference](../research/control-matrix.md#pi)).
198
227
 
199
228
  ## Uninstall
200
229
 
@@ -13,7 +13,7 @@ guides: [Codex](agents/codex.md), [Claude Code](agents/claude-code.md),
13
13
 
14
14
  Every knob resolves through one chain, low → high, identically on every host:
15
15
 
16
- ```
16
+ ```text
17
17
  built-in defaults < shared config file < DEEPSEEK_* environment < host-native config
18
18
  ```
19
19
 
@@ -26,7 +26,7 @@ variable arrives, not a fifth layer).
26
26
  ## The shared config file
27
27
 
28
28
  `$XDG_CONFIG_HOME/deepseek-web-retrieval/config.json` when `XDG_CONFIG_HOME` is
29
- set (this wins on every platform, Windows included); otherwise
29
+ set to a non-empty value (this wins on every platform, Windows included); otherwise
30
30
  `~/.config/deepseek-web-retrieval/config.json` on Linux and macOS and
31
31
  `%APPDATA%\deepseek-web-retrieval\config.json` on Windows. camelCase keys:
32
32
 
@@ -49,7 +49,7 @@ environment, or OpenCode's stored credential).
49
49
  | Knob class | Shared file | Examples |
50
50
  | --- | --- | --- |
51
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` |
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`, `searchContentMaxChars`, `searchContentTimeoutMs` |
53
53
  | Host integration — the host's tool landscape | rejected — warned about and ignored | `searchEnabled`, `setDefaultWebsearch`, `apiKeySource` |
54
54
 
55
55
  Keep host-independent intent in the shared file; prefer a host layer when a
@@ -58,8 +58,8 @@ host-sensitive knob would cross its host's cap or deadline.
58
58
  ## The knob vocabulary
59
59
 
60
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
61
+ options, the `DEEPSEEK_*` variable for the environment. The table is
62
+ generated from the same config vocabulary the adapters resolve through, so it cannot
63
63
  drift from the code (`bun run docs:matrix` regenerates it; CI checks it).
64
64
 
65
65
  <!-- generated: knob-table — edit src/core/config-summary.ts or env.ts, then run `bun run docs:matrix` -->
@@ -76,6 +76,8 @@ drift from the code (`bun run docs:matrix` regenerates it; CI checks it).
76
76
  | `language` | `DEEPSEEK_SEARCH_LANGUAGE` | unset |
77
77
  | `searchLocalization` | `DEEPSEEK_SEARCH_LOCALIZATION` | unset |
78
78
  | `maxSummaryChars` | `DEEPSEEK_SEARCH_MAX_SUMMARY_CHARS` | 1200 |
79
+ | `searchContentMaxChars` | `DEEPSEEK_SEARCH_CONTENT_MAX_CHARS` | 1500 |
80
+ | `searchContentTimeoutMs` | `DEEPSEEK_SEARCH_CONTENT_TIMEOUT_MS` | 60000 |
79
81
  | `searchTimeoutMs` | `DEEPSEEK_SEARCH_TIMEOUT_MS` | 30000 |
80
82
  | `fetchMaxChars` | `DEEPSEEK_FETCH_MAX_CHARS` | 16000 |
81
83
  | `fetchAllowedDomains` | `DEEPSEEK_FETCH_ALLOWED_DOMAINS` | unset |
@@ -84,6 +86,7 @@ drift from the code (`bun run docs:matrix` regenerates it; CI checks it).
84
86
  | `fetchBlockPrivateHosts` | `DEEPSEEK_FETCH_BLOCK_PRIVATE_HOSTS` | true |
85
87
  | `maxRetries` | `DEEPSEEK_MAX_RETRIES` | 2 |
86
88
  | `retryBaseDelayMs` | `DEEPSEEK_RETRY_BASE_DELAY_MS` | 250 |
89
+ | `baseUrl` | `DEEPSEEK_BASE_URL` | https://api.deepseek.com |
87
90
 
88
91
  <!-- /generated: knob-table -->
89
92
 
@@ -96,8 +99,9 @@ drift from the code (`bun run docs:matrix` regenerates it; CI checks it).
96
99
  `fetchBlockPrivateHosts`, `searchEnabled`, and `fetchEnabled` are on by
97
100
  default; the environment can only turn them off (`false`, `0`, `no`, or
98
101
  `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.
102
+ they exist only as `DEEPSEEK_ENABLE_SEARCH` and `DEEPSEEK_ENABLE_FETCH`.
103
+ Setting any of them to `true` explicitly changes nothing: the default is
104
+ already on.
101
105
  - **Invalid environment values are dropped** at startup and the default
102
106
  applies. `npx deepseek-web-retrieval config` reports every dropped or
103
107
  unknown `DEEPSEEK_*` variable as a warning and exits 1, alongside file
@@ -105,5 +109,18 @@ drift from the code (`bun run docs:matrix` regenerates it; CI checks it).
105
109
  - **`searchLocalization` must be a valid IPv4 or IPv6 address**; anything else
106
110
  is dropped at option resolution. It is deployment-level configuration, never
107
111
  a per-call tool parameter.
112
+ - **The content envelope is deployment-level too.** `searchContentMaxChars` and
113
+ `searchContentTimeoutMs` apply only when a call asks for inline page content
114
+ (`need_content: true`, a per-call tool parameter on the MCP server and the Pi
115
+ extension; OpenCode's provider input has no channel for it, so search stays
116
+ summary-only there). They are never tool parameters — the model cannot see
117
+ which host caps its output. `searchContentMaxChars: 0` drops page bodies
118
+ entirely and the summary digest renders.
119
+ - **`baseUrl` routes the retrieval API** (`/v1/tools/search` and
120
+ `/v1/tools/fetch` are composed from it). It is infrastructure routing for
121
+ proxies, gateways, and mock upstreams — not a provider switch, and the package
122
+ never falls back to another search service. Only an http(s) URL survives
123
+ resolution (trailing slashes are stripped); anything else drops back to
124
+ `https://api.deepseek.com`.
108
125
  - **Domains** in the environment are comma-separated; entries that are not
109
126
  domains are dropped (and reported by `config`).
@@ -46,7 +46,7 @@ shared base strengthens rather than weakens them (one policy instead of three dr
46
46
 
47
47
  Precedence, low → high, identical semantics in every adapter:
48
48
 
49
- ```
49
+ ```text
50
50
  built-in defaults < shared config file < DEEPSEEK_* environment < host-native config
51
51
  ```
52
52
 
@@ -133,9 +133,13 @@ A bad shared value now fails everywhere at once, so effective configuration must
133
133
 
134
134
  ## What this design does not change
135
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.
136
+ - `need_content` is no longer hardcoded `false` — it became a per-call tool parameter
137
+ with a deployment-owned envelope
138
+ ([search-content-design.md](search-content-design.md),
139
+ [ADR-0003](../adr/0003-need-content-per-call-boolean.md)).
140
+ - `ori_question` remains per-call-only — supplied through the tool input named
141
+ `original_question` (with the Pi session fallback) and sent to the API as `ori_question`;
142
+ it is not configurable.
139
143
  - Auth surfaces, capability tiers, and the installer's plan/confirm flow.
140
144
 
141
145
  ## Testing
@@ -154,7 +158,8 @@ existing layers changes. Documentation updates ship with the code: README gains
154
158
  "Configuration" section with the precedence table and the two-vocabulary table; the host
155
159
  guides' option tables gain a shared-file column; the control-matrix note's Pi sentence
156
160
  ("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.
161
+ `DEEPSEEK_*` environment") is updated to name the file as part of that surface. All of
162
+ these updates are shipped and in place.
158
163
 
159
164
  **Amendment, 2026-10-23** — two changes approved after this design was confirmed alter
160
165
  the effective behavior of *untouched* configurations, superseding the "no existing