deepseek-web-retrieval 0.9.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to this project are documented here, one sentence per item.
4
4
 
5
+ ## 0.10.0
6
+
7
+ - Add `need_content` as an optional per-call boolean on the MCP and Pi search tools: `true` attaches each result's page body as Markdown (structure-preserving, indented into the result list) in place of its summary, and a missing body falls back to the summary; OpenCode's fixed provider input keeps that path summary-only.
8
+ - Add the `searchContentMaxChars` (1,500 characters) and `searchContentTimeoutMs` (60,000 ms) deployment knobs — the content envelope every `need_content` call runs inside, never tool parameters, recorded in ADR-0003 and the search content design note.
9
+ - State the `need_content` facts — excerpt length, latency, price, and the fetch alternative — in the tool and parameter descriptions, with both full-page routes named side by side, and no usage policy: when content is worth it stays the model's per-call judgment.
10
+ - Extend the inline-content research note with the vendor reading that no surveyed architecture couples a content flag to another parameter's value range: budgets are static-range, conditional-effect.
11
+ - Add a keyless `pi-mock` e2e leg that drives a real Pi session search with `need_content: true` against the mock upstream and asserts the flag reached the wire and the page body reached the model, with the digest legs now also asserting `need_content: false` by default.
12
+
5
13
  ## 0.9.0
6
14
 
7
15
  - Accept an optional per-call `count` (1–50) on the MCP and Pi search tools and an optional per-call `maxChars` (0–1,000,000) on every adapter's fetch, each overriding the configured value for that one call.
@@ -13,7 +21,7 @@ All notable changes to this project are documented here, one sentence per item.
13
21
 
14
22
  ## 0.8.0
15
23
 
16
- - Add Claude Code as a supported host: an MCP-only integration registered in `.mcp.json` (project scope) or `~/.claude.json` (user scope), with a full installer host module (plan/apply/uninstall, both scopes), a `docs/agents/claude-code.md` guide, a version probe with drift note, and managed-policy detection that reports a `managed-settings.json` layer outranking the install.
24
+ - Add Claude Code as a supported host: an MCP-only integration registered in `.mcp.json` (project scope) or `~/.claude.json` (Claude Code's user scope — global scope in this repo's vocabulary), with a full installer host module (plan/apply/uninstall, both scopes), a `docs/agents/claude-code.md` guide, a version probe with drift note, and managed-policy detection that reports a `managed-settings.json` layer outranking the install.
17
25
  - Route Claude Code tiers through its own settings files: forced capabilities deny the built-in `WebSearch`/`WebFetch` via `permissions.deny` in `.claude/settings.json` or `~/.claude/settings.json` (merged with the user's entries), off capabilities deny the full `mcp__deepseek__*` tool ids, and preferred writes a rules block into `CLAUDE.md`; web-capable competitor servers are excluded under forced via server-level `mcp__<server>` deny rules instead of refusing the tier.
18
26
  - Add a billed `claude-code` Docker e2e scenario that runs headless `claude -p` prompts with DeepSeek as the LLM over its Anthropic-compatible endpoint (single key for both legs) and asserts the deny-based kill switch keeps the built-in tools out of the session, plus installer/reinit coverage of the new host in the keyless scenarios.
19
27
  - Guard Pi's global config files with the shared-file ownership sidecar, and release claims before deleting the install record on uninstall, so two installs under different config homes can no longer claim the same `~/.pi/agent` file.
package/README.md CHANGED
@@ -4,8 +4,9 @@
4
4
 
5
5
  Search the web and fetch pages as Markdown through
6
6
  [DeepSeek's APIs](https://api-docs.deepseek.com/guides/search), using a **native OpenCode
7
- plugin**, a **native Pi extension**, or **MCP for Codex and Claude Code**. All four share
8
- the same retrieval core, with results formatted for each host.
7
+ plugin**, a **native [Pi](https://pi.dev/) extension** (Pi, the open-source coding agent),
8
+ or **MCP for Codex and Claude Code**. All four share the same retrieval core, with results
9
+ formatted for each host.
9
10
 
10
11
  This is an unofficial community project, not affiliated with DeepSeek or the supported
11
12
  host projects.
@@ -16,8 +17,9 @@ Jump to: [Features](#features) · [Quickstart](#quickstart) · [Usage](#usage)
16
17
 
17
18
  ## Features
18
19
 
19
- - **One package, four hosts** — a native OpenCode v2 plugin, a native Pi extension,
20
- and a stdio MCP server for Codex and Claude Code, all sharing one retrieval core
20
+ - **One package, four hosts** — a native plugin for OpenCode v2 (the 2.x series), a
21
+ native Pi extension, and a stdio MCP server for Codex and Claude Code, all sharing one
22
+ retrieval core
21
23
  - **Routing control per tool** — search and fetch are set independently to optional,
22
24
  preferred, forced, or off; the tier table in Usage explains each
23
25
  - **Review before anything is written** — `init` prints the full plan and writes only
@@ -59,8 +61,9 @@ Choose your host:
59
61
  | Claude Code | MCP server over stdio in `.mcp.json` or `~/.claude.json` | [Claude Code guide](docs/agents/claude-code.md) |
60
62
 
61
63
  Set `DEEPSEEK_API_KEY` in the environment that launches your host; OpenCode can also
62
- use its stored DeepSeek key. Run `init` for your host to preview a configuration — the
63
- command shape is the quickstart's; each host guide shows its recommended tiers.
64
+ use its stored DeepSeek key. Run `init` for your host to preview a configuration — take
65
+ the quickstart command and swap `--host` and `--scope` for your host's values; each host
66
+ guide shows its recommended tiers.
64
67
 
65
68
  `--search` and `--fetch` each pick a tier — how strongly the host steers toward the
66
69
  DeepSeek tool, chosen independently per capability:
@@ -87,7 +90,8 @@ Files:
87
90
  upsert marked rules block
88
91
 
89
92
  Notes:
90
- - Codex stdio children do not inherit the parent environment, so the entry forwards DEEPSEEK_API_KEY and every DEEPSEEK_* option variable explicitly via env_vars; shell-set knobs reach the server without hand-editing config.toml (the shared config file works without any forwarding).
93
+ - Codex stdio children do not inherit the parent environment, so the entry forwards DEEPSEEK_API_KEY and every DEEPSEEK_* option variable explicitly via env_vars; shell-set knobs reach the server without hand-editing config.toml.
94
+ - The shared config file needs no forwarding at all: the server reads it itself.
91
95
  - Rules block upserted in ~/.codex/AGENTS.md; Codex reads ~/.codex/AGENTS.md plus every AGENTS.md from the project root to the cwd.
92
96
  - Shared config ownership is recorded in ~/.codex/.config.toml.deepseek-web-retrieval.codex.owner.json; uninstall releases it. Keep this local metadata with the config.
93
97
 
@@ -116,19 +120,21 @@ What this adapter layer adds on top of the
116
120
  own capabilities (Markdown-only output, no JavaScript rendering, no private networks or
117
121
  binary files, per-request billing) are documented there.
118
122
 
119
- | Default | Value | Knob |
123
+ | Setting | Default | Knob |
120
124
  | --- | --- | --- |
121
125
  | Results per search | 10 | `count` |
122
126
  | Fetch output cap | 16,000 characters | `fetchMaxChars` |
123
127
  | Search summary cap | 1,200 characters | `maxSummaryChars` |
124
- | Request timeout, body read included | 30 s | `searchTimeoutMs` / `fetchTimeoutMs` |
128
+ | Inline content cap, per result (when requested) | 1,500 characters | `searchContentMaxChars` |
129
+ | Request timeout, body read included | 30 s (60 s for `need_content` calls) | `searchTimeoutMs` / `fetchTimeoutMs` / `searchContentTimeoutMs` |
125
130
  | Transient retries | 2 (range 0–5) | `maxRetries` |
126
131
 
127
132
  ### Search
128
133
 
129
- - Requests summaries only (`need_content: false`, falling back from `summary` to
130
- `snippet`); full page content never enters the results. A summary the cap shortens ends
131
- with an ellipsis.
134
+ - Requests summaries by default (`need_content: false`, falling back from `summary` to
135
+ `snippet`); a summary the cap shortens ends with an ellipsis. When a call sets
136
+ `need_content: true`, each result instead carries its page body as Markdown, clamped
137
+ to `searchContentMaxChars` per result — a missing body falls back to the summary.
132
138
  - Deduplicates URLs; an empty result set is normal, not an error.
133
139
  - Builds `blockedDomains`, `includeDomains`, and `language` into query operators (part of
134
140
  DeepSeek's documented search syntax). A combined query over the API's 128-character limit
@@ -144,8 +150,11 @@ binary files, per-request billing) are documented there.
144
150
  the Pi extension), the tool accepts an optional `original_question`: the user's
145
151
  question in their own words. It is forwarded as DeepSeek's `ori_question`, trimmed
146
152
  and truncated to 128 characters; on the Pi extension, a blank input falls back to the
147
- most recent user message in the session. Those paths also take an optional per-call
148
- `count` (1–50) overriding the configured result count for that call.
153
+ most recent user message in the session.
154
+ - The same paths also take an optional per-call `count` (1–50), overriding the
155
+ configured result count for that one call, and an optional per-call `need_content`
156
+ boolean requesting page bodies in the results (see
157
+ [search content design](docs/design/search-content-design.md)).
149
158
 
150
159
  ### Fetch
151
160
 
@@ -228,8 +237,8 @@ bun run check
228
237
  ```
229
238
 
230
239
  Use `bun run lint:fix` to apply Biome's safe lint fixes and formatting. See
231
- [CONTRIBUTING.md](CONTRIBUTING.md) for development checks, e2e scenarios, and
232
- [automated tag releases](CONTRIBUTING.md#automated-releases).
240
+ [CONTRIBUTING.md](CONTRIBUTING.md) for development checks and e2e scenarios, and
241
+ [automated tag releases](CONTRIBUTING.md#automated-releases-maintainers).
233
242
 
234
243
  The test suite mocks the DeepSeek API and the OpenCode and Pi host APIs; real API calls
235
244
  require a funded DeepSeek account. Shared retrieval logic lives in `src/core/`; adapters
package/dist/cli.js CHANGED
@@ -16,7 +16,7 @@ var __export = (target, all) => {
16
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
 
18
18
  // src/core/constants.ts
19
- var DEFAULT_API_BASE_URL = "https://api.deepseek.com", SEARCH_ENDPOINT_PATH = "/v1/tools/search", FETCH_ENDPOINT_PATH = "/v1/tools/fetch", SEARCH_ENDPOINT, FETCH_ENDPOINT, DEFAULT_SEARCH_COUNT = 10, DEFAULT_MAX_RESULTS = 10, DEFAULT_MIN_RERANK = 0, DEFAULT_MIN_AUTHORITY = 0, DEFAULT_MAX_SUMMARY_CHARS = 1200, DEFAULT_FETCH_MAX_CHARS = 16000, MAX_FETCH_CHARS = 1e6, MAX_SEARCH_COUNT = 50, DEFAULT_TIMEOUT_MS = 30000, MIN_TIMEOUT_MS = 1000, MAX_TIMEOUT_MS = 120000, MAX_QUERY_CHARS = 128, MAX_ORI_QUESTION_CHARS = 128, MAX_URL_CHARS = 8192, MAX_ERROR_BODY_CHARS = 2000, MAX_RESPONSE_BYTES, FETCH_TRUNCATION_MARKER = `
19
+ var DEFAULT_API_BASE_URL = "https://api.deepseek.com", SEARCH_ENDPOINT_PATH = "/v1/tools/search", FETCH_ENDPOINT_PATH = "/v1/tools/fetch", SEARCH_ENDPOINT, FETCH_ENDPOINT, DEFAULT_SEARCH_COUNT = 10, DEFAULT_MAX_RESULTS = 10, DEFAULT_MIN_RERANK = 0, DEFAULT_MIN_AUTHORITY = 0, DEFAULT_MAX_SUMMARY_CHARS = 1200, DEFAULT_SEARCH_CONTENT_MAX_CHARS = 1500, DEFAULT_SEARCH_CONTENT_TIMEOUT_MS = 60000, DEFAULT_FETCH_MAX_CHARS = 16000, MAX_FETCH_CHARS = 1e6, MAX_SEARCH_COUNT = 50, DEFAULT_TIMEOUT_MS = 30000, MIN_TIMEOUT_MS = 1000, MAX_TIMEOUT_MS = 120000, MAX_QUERY_CHARS = 128, MAX_ORI_QUESTION_CHARS = 128, MAX_URL_CHARS = 8192, MAX_ERROR_BODY_CHARS = 2000, MAX_RESPONSE_BYTES, FETCH_TRUNCATION_MARKER = `
20
20
 
21
21
  [content truncated]`, DEFAULT_API_KEY_SOURCE = "auto", DEFAULT_MAX_RETRIES = 2, MAX_RETRIES = 5, DEFAULT_RETRY_BASE_DELAY_MS = 250, MAX_RETRY_DELAY_MS = 1e4, API_KEY_ENV = "DEEPSEEK_API_KEY", BASE_URL_ENV = "DEEPSEEK_BASE_URL", ENABLE_SEARCH_ENV = "DEEPSEEK_ENABLE_SEARCH", ENABLE_FETCH_ENV = "DEEPSEEK_ENABLE_FETCH", SEARCH_LOCALIZATION_ENV = "DEEPSEEK_SEARCH_LOCALIZATION";
22
22
  var init_constants = __esm(() => {
@@ -162,6 +162,8 @@ function resolveSearchOptions(input) {
162
162
  language: optionalText(input?.language),
163
163
  localization: localizationValue(input?.searchLocalization),
164
164
  maxSummaryChars: clampInt(input?.maxSummaryChars, DEFAULT_MAX_SUMMARY_CHARS, 0, 1e6),
165
+ contentMaxChars: clampInt(input?.searchContentMaxChars, DEFAULT_SEARCH_CONTENT_MAX_CHARS, 0, 1e6),
166
+ contentTimeoutMs: clampInt(input?.searchContentTimeoutMs, DEFAULT_SEARCH_CONTENT_TIMEOUT_MS, MIN_TIMEOUT_MS, MAX_TIMEOUT_MS),
165
167
  timeoutMs: clampInt(input?.searchTimeoutMs, DEFAULT_TIMEOUT_MS, MIN_TIMEOUT_MS, MAX_TIMEOUT_MS),
166
168
  enabled: input?.searchEnabled !== false
167
169
  };
@@ -279,6 +281,8 @@ var init_config_summary = __esm(() => {
279
281
  { key: "language", read: (o) => o.search.language },
280
282
  { key: "searchLocalization", read: (o) => o.search.localization },
281
283
  { key: "maxSummaryChars", read: (o) => o.search.maxSummaryChars },
284
+ { key: "searchContentMaxChars", read: (o) => o.search.contentMaxChars },
285
+ { key: "searchContentTimeoutMs", read: (o) => o.search.contentTimeoutMs },
282
286
  { key: "searchTimeoutMs", read: (o) => o.search.timeoutMs },
283
287
  { key: "fetchMaxChars", read: (o) => o.fetch.maxChars },
284
288
  { key: "fetchAllowedDomains", read: (o) => o.fetch.allowedDomains },
@@ -4170,7 +4174,9 @@ No results found.` : "No results found.";
4170
4174
  const marker = formatQualityMarker(result);
4171
4175
  const lines = [`${index + 1}. [${escapeBrackets(title)}](${result.url})${marker ? ` ${marker}` : ""}`];
4172
4176
  if (result.content) {
4173
- lines.push(` ${result.content.replaceAll(`
4177
+ lines.push(result.contentKind === "page" ? result.content.split(`
4178
+ `).map((line) => ` ${line}`).join(`
4179
+ `) : ` ${result.content.replaceAll(`
4174
4180
  `, " ")}`);
4175
4181
  }
4176
4182
  if (result.published !== undefined) {
@@ -4354,9 +4360,14 @@ function processResults(raw, options) {
4354
4360
  continue;
4355
4361
  }
4356
4362
  seen.add(url);
4357
- const rawContent = optionalText2(item.summary) ?? optionalText2(item.snippet) ?? "";
4358
- const cutContent = truncate(rawContent, options.maxSummaryChars);
4359
- const content = cutContent && charCount(rawContent) > options.maxSummaryChars ? `${cutContent}…` : cutContent;
4363
+ const body = optionalText2(item.content);
4364
+ const digest = optionalText2(item.summary) ?? optionalText2(item.snippet) ?? "";
4365
+ const fromPage = body !== undefined && options.contentMaxChars > 0;
4366
+ const rawContent = fromPage && body !== undefined ? body : digest;
4367
+ const contentBudget = fromPage ? options.contentMaxChars : options.maxSummaryChars;
4368
+ const cutContent = truncate(rawContent, contentBudget);
4369
+ const content = cutContent && charCount(rawContent) > contentBudget ? `${cutContent}…` : cutContent;
4370
+ const contentKind = fromPage ? "page" : "digest";
4360
4371
  const published = parsePublished(item.date);
4361
4372
  const title = optionalText2(item.title);
4362
4373
  const rerankScore = typeof rerank === "number" && Number.isFinite(rerank) ? rerank : undefined;
@@ -4365,7 +4376,7 @@ function processResults(raw, options) {
4365
4376
  results.push({
4366
4377
  url,
4367
4378
  ...title ? { title } : {},
4368
- ...content ? { content } : {},
4379
+ ...content ? { content, contentKind } : {},
4369
4380
  ...published !== undefined ? { published } : {},
4370
4381
  ...rerankScore !== undefined ? { rerankScore } : {},
4371
4382
  ...authorityScore !== undefined ? { authority: authorityScore } : {},
@@ -4620,12 +4631,13 @@ async function searchWeb(query, options, request) {
4620
4631
  }
4621
4632
  const composed = composeSearchQuery(trimmed, options);
4622
4633
  const builtQuery = composed.query;
4623
- const timeout = effectiveTimeout(options.timeoutMs, request.timeoutMs);
4634
+ const needContent = request.needContent === true;
4635
+ const timeout = effectiveTimeout(needContent ? options.contentTimeoutMs : options.timeoutMs, request.timeoutMs);
4624
4636
  const oriQuestion = oriQuestionValue(request.oriQuestion);
4625
4637
  const payload = await postJson(apiUrl(request.baseUrl, SEARCH_ENDPOINT_PATH), {
4626
4638
  query: builtQuery,
4627
4639
  count: options.count,
4628
- need_content: false,
4640
+ need_content: needContent,
4629
4641
  ...oriQuestion === undefined ? {} : { ori_question: oriQuestion },
4630
4642
  ...options.localization === undefined ? {} : { localization: options.localization }
4631
4643
  }, "DeepSeek Search", { ...request, timeoutMs: timeout });
@@ -4705,6 +4717,8 @@ function envOptionsReport(env = process.env) {
4705
4717
  putText(options, "searchLocalization", env);
4706
4718
  putNumber(options, "maxSummaryChars", env, warnings);
4707
4719
  putNumber(options, "searchTimeoutMs", env, warnings);
4720
+ putNumber(options, "searchContentMaxChars", env, warnings);
4721
+ putNumber(options, "searchContentTimeoutMs", env, warnings);
4708
4722
  putNumber(options, "fetchMaxChars", env, warnings);
4709
4723
  putDomains(options, "fetchAllowedDomains", env, warnings);
4710
4724
  putDomains(options, "fetchBlockedDomains", env, warnings);
@@ -4792,6 +4806,8 @@ var init_env = __esm(() => {
4792
4806
  minAuthority: "DEEPSEEK_SEARCH_MIN_AUTHORITY",
4793
4807
  maxSummaryChars: "DEEPSEEK_SEARCH_MAX_SUMMARY_CHARS",
4794
4808
  searchTimeoutMs: "DEEPSEEK_SEARCH_TIMEOUT_MS",
4809
+ searchContentMaxChars: "DEEPSEEK_SEARCH_CONTENT_MAX_CHARS",
4810
+ searchContentTimeoutMs: "DEEPSEEK_SEARCH_CONTENT_TIMEOUT_MS",
4795
4811
  fetchMaxChars: "DEEPSEEK_FETCH_MAX_CHARS",
4796
4812
  fetchTimeoutMs: "DEEPSEEK_FETCH_TIMEOUT_MS",
4797
4813
  maxRetries: "DEEPSEEK_MAX_RETRIES",
@@ -6222,7 +6238,7 @@ var init_package = __esm(() => {
6222
6238
  package_default = {
6223
6239
  $schema: "https://json.schemastore.org/package.json",
6224
6240
  name: "deepseek-web-retrieval",
6225
- version: "0.9.0",
6241
+ version: "0.10.0",
6226
6242
  description: "Token-efficient DeepSeek Search and Fetch for coding agents, starting with OpenCode v2.",
6227
6243
  type: "module",
6228
6244
  license: "MIT",
@@ -6326,13 +6342,14 @@ var init_package = __esm(() => {
6326
6342
  });
6327
6343
 
6328
6344
  // src/core/descriptions.ts
6329
- var FETCH_SERVICE_LIMITS = "DeepSeek does not render JavaScript and cannot reach private networks; PDFs, images, and other binary files are unsupported.", FETCH_OUTPUT_TRUNCATED = "Output is truncated to the configured size; the optional maxChars parameter raises or lowers that size for one call.", SEARCH_RANKED_FILTERED = "Results are ranked by relevance and source authority, deduplicated by URL, and capped at the configured maximum.", SEARCH_EMPTY_IS_NORMAL = "An empty result list is normal, not an error.", SEARCH_QUERY_OPERATORS = "Use site:/-site: operators in the query to include or exclude domains, and after:/before: with a year or date (e.g. after:2025-06-01) to filter by publish time; the full query must stay within 128 characters.", SEARCH_TOOL_DESCRIPTION, FETCH_TOOL_DESCRIPTION, SEARCH_QUERY_PARAMETER = "Search query, 128 characters or fewer including operators (site:, -site:, quoted phrases, lang:, after:, before:).", SEARCH_ORI_QUESTION_PARAMETER, FETCH_URL_PARAMETER = "Absolute http:// or https:// URL to fetch. Credentials are rejected.", FETCH_MAX_CHARS_PARAMETER, SEARCH_COUNT_PARAMETER;
6345
+ var FETCH_SERVICE_LIMITS = "DeepSeek does not render JavaScript and cannot reach private networks; PDFs, images, and other binary files are unsupported.", FETCH_OUTPUT_TRUNCATED = "Output is truncated to the configured size; the optional maxChars parameter raises or lowers that size for one call.", SEARCH_RANKED_FILTERED = "Results are ranked by relevance and source authority, deduplicated by URL, and capped at the configured maximum.", SEARCH_EMPTY_IS_NORMAL = "An empty result list is normal, not an error.", SEARCH_QUERY_OPERATORS = "Use site:/-site: operators in the query to include or exclude domains, and after:/before: with a year or date (e.g. after:2025-06-01) to filter by publish time; the full query must stay within 128 characters.", SEARCH_TOOL_DESCRIPTION, FETCH_TOOL_DESCRIPTION, SEARCH_QUERY_PARAMETER = "Search query, 128 characters or fewer including operators (site:, -site:, quoted phrases, lang:, after:, before:).", SEARCH_ORI_QUESTION_PARAMETER, FETCH_URL_PARAMETER = "Absolute http:// or https:// URL to fetch. Credentials are rejected.", FETCH_MAX_CHARS_PARAMETER, SEARCH_COUNT_PARAMETER, SEARCH_NEED_CONTENT_PARAMETER;
6330
6346
  var init_descriptions = __esm(() => {
6331
- SEARCH_TOOL_DESCRIPTION = "Search the web with DeepSeek's in-house search engine and return a short, filtered result " + "list (title, URL, summary). " + `${SEARCH_RANKED_FILTERED} ` + `${SEARCH_EMPTY_IS_NORMAL} ` + `${SEARCH_QUERY_OPERATORS} ` + "To read a full page, pass a result URL to the deepseek_fetch tool.";
6347
+ SEARCH_TOOL_DESCRIPTION = "Search the web with DeepSeek's in-house search engine and return a short, filtered result " + "list (title, URL, summary). " + `${SEARCH_RANKED_FILTERED} ` + `${SEARCH_EMPTY_IS_NORMAL} ` + `${SEARCH_QUERY_OPERATORS} ` + "To read one full page in depth, pass a result URL to the deepseek_fetch tool; to carry each result's " + "page body in this response, set need_content to true.";
6332
6348
  FETCH_TOOL_DESCRIPTION = "Fetch one web page as clean Markdown through DeepSeek's Fetch API. " + `${FETCH_SERVICE_LIMITS} ` + `${FETCH_OUTPUT_TRUNCATED}`;
6333
6349
  SEARCH_ORI_QUESTION_PARAMETER = "The user's original question or task that triggered this search, in the user's own words rather than a " + "reformulated query. Optional; 1-128 characters, and longer values are truncated.";
6334
6350
  FETCH_MAX_CHARS_PARAMETER = "Optional per-call character budget for this fetch, 0-1,000,000, title included. Overrides the configured cap " + "for this call; a [content truncated] marker is appended when the output is cut.";
6335
6351
  SEARCH_COUNT_PARAMETER = "Optional number of results for this call, 1-50. Overrides the configured count and result cap for this call; " + "billing is per request, not per result.";
6352
+ SEARCH_NEED_CONTENT_PARAMETER = "Optional; set true to attach each result's page body as Markdown — an excerpt clamped to a configured per-result " + "budget, not a full page — in place of its short summary. Slower per call (DeepSeek fetches the result pages) and " + "priced the same per request; for a full page, call deepseek_fetch.";
6336
6353
  });
6337
6354
 
6338
6355
  // src/mcp/server.ts
@@ -6346,12 +6363,13 @@ function createDeepSeekServer(config, env = process.env, client = retrievalClien
6346
6363
  description: SEARCH_TOOL_DESCRIPTION,
6347
6364
  inputSchema: SEARCH_INPUT,
6348
6365
  annotations: { readOnlyHint: true }
6349
- }, async ({ query, original_question, count }, ctx) => {
6366
+ }, async ({ query, original_question, count, need_content }, ctx) => {
6350
6367
  try {
6351
6368
  const request = requestContext(config, requireApiKey(env), ctx.mcpReq.signal);
6352
6369
  const outcome = await client.searchWeb(query, withPerCallCount(config.search, count), {
6353
6370
  ...request,
6354
- oriQuestion: original_question
6371
+ oriQuestion: original_question,
6372
+ needContent: need_content === true
6355
6373
  });
6356
6374
  return callResult(server, ctx, formatSearchResults(outcome.results, outcome), {
6357
6375
  results: outcome.results.map(toStructuredResult)
@@ -6409,7 +6427,8 @@ var init_server = __esm(() => {
6409
6427
  SEARCH_INPUT = z2.object({
6410
6428
  query: z2.string().min(1).describe(SEARCH_QUERY_PARAMETER),
6411
6429
  original_question: z2.string().optional().describe(SEARCH_ORI_QUESTION_PARAMETER),
6412
- count: z2.number().int().min(1).max(MAX_SEARCH_COUNT).optional().describe(SEARCH_COUNT_PARAMETER)
6430
+ count: z2.number().int().min(1).max(MAX_SEARCH_COUNT).optional().describe(SEARCH_COUNT_PARAMETER),
6431
+ need_content: z2.boolean().optional().describe(SEARCH_NEED_CONTENT_PARAMETER)
6413
6432
  });
6414
6433
  FETCH_INPUT = z2.object({
6415
6434
  url: z2.string().min(1).describe(FETCH_URL_PARAMETER),
@@ -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.
@@ -176,7 +179,8 @@ operators change the query actually sent, the result text opens with
176
179
  `Searched for: <effective query>` (plus any degradation applied to fit the API's
177
180
  128-character limit), so the model can attribute results to the configuration in effect.
178
181
 
179
- Tool output is capped at 25k tokens by default (`MAX_MCP_OUTPUT_TOKENS`), and successful
182
+ Tool output is capped at 25k tokens by default (`MAX_MCP_OUTPUT_TOKENS`, a Claude Code
183
+ environment setting), and successful
180
184
  text results longer than 50,000 characters are saved to a file and referenced — not
181
185
  dropped silently.
182
186
 
@@ -255,7 +259,7 @@ one vocabulary pair, one pipeline. `—` marks host-integration entries the file
255
259
  | Claude Code | locked in [`docs/hosts.jsonc`](../hosts.jsonc) |
256
260
  | MCP revision | `2026-07-28` negotiated; server accepts `2024-11-05`–`2026-07-28` |
257
261
  | MCP SDK | `@modelcontextprotocol/server` 2.3.1 |
258
- | Runtime | Node.js 20+ (stdio) |
262
+ | Runtime | Node.js 20 or newer (stdio) |
259
263
  | DeepSeek API | `/v1/tools/search`, `/v1/tools/fetch` |
260
264
 
261
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
@@ -231,7 +231,7 @@ one vocabulary pair, one pipeline. `—` marks host-integration entries the file
231
231
  | Codex | locked in [`docs/hosts.jsonc`](../hosts.jsonc) |
232
232
  | MCP revision | `2025-06-18` negotiated (`mcp_2026_07_28` behind a flag); server accepts `2024-11-05`–`2026-07-28` |
233
233
  | MCP SDK | `@modelcontextprotocol/server` 2.3.1 |
234
- | Runtime | Node.js 20+ (stdio) |
234
+ | Runtime | Node.js 20 or newer (stdio) |
235
235
  | DeepSeek API | `/v1/tools/search`, `/v1/tools/fetch` |
236
236
 
237
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
 
@@ -177,8 +180,10 @@ The model calls `websearch` (provider `deepseek`) or `deepseek_fetch`. When
177
180
  `fetchOverrideWebfetch` is enabled, Markdown fetches go through `webfetch` instead.
178
181
 
179
182
  DeepSeek's optional `ori_question` is not sent on this path, and the model cannot request a
180
- per-call result count: OpenCode's `websearch` tool input is fixed to `query` (verified unchanged
181
- at 2.1.4), so the provider has no channel for either. Upstream requests:
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:
182
187
  [anomalyco/opencode#58635](https://github.com/anomalyco/opencode/issues/58635) (session linkage)
183
188
  and [#58636](https://github.com/anomalyco/opencode/issues/58636) (per-call `count`); see the
184
189
  [research note](../research/ori-question-sourcing.md). The fetch tools are not constrained:
@@ -186,7 +191,8 @@ they accept an optional per-call `maxChars` (0–1,000,000) that overrides the c
186
191
  `fetchMaxChars` for one call, so the model can widen the budget after seeing a
187
192
  `[content truncated]` marker. The effective-query header and query degradation notes have no
188
193
  channel on this path either — OpenCode's result shape is closed — but degradation still runs,
189
- and failures that cannot degrade surface as errors.
194
+ and a query that exceeds the length limit and cannot be degraded further surfaces as an
195
+ error.
190
196
 
191
197
  ## Configuration
192
198
 
@@ -210,14 +216,14 @@ environment or file channel.
210
216
 
211
217
  | Option | Environment | Default | Description |
212
218
  | --- | --- | --- | --- |
213
- | `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`. |
214
220
  | `count` | `DEEPSEEK_SEARCH_COUNT` | `10` | Results requested from DeepSeek, 1–50. Billing is per request, not per result. |
215
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. |
216
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. |
217
223
  | `minAuthority` | `DEEPSEEK_SEARCH_MIN_AUTHORITY` | `0` | Drop known authorities below this tier. Authority is 0–4. |
218
- | `allowUnknownAuthority` | `DEEPSEEK_SEARCH_ALLOW_UNKNOWN_AUTHORITY` | `true` | Keep results whose authority is missing or unknown. Only an explicit `false` disables it. |
219
- | `blockedDomains` | `DEEPSEEK_SEARCH_BLOCKED_DOMAINS` | `[]` | Add `-site:` operators to the search query. |
220
- | `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. |
221
227
  | `language` | `DEEPSEEK_SEARCH_LANGUAGE` | unset | Add a `lang:` operator, for example `zh,en`. |
222
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. |
223
229
  | `maxSummaryChars` | `DEEPSEEK_SEARCH_MAX_SUMMARY_CHARS` | `1200` | Per-result summary cap, 0–1,000,000. |
@@ -233,8 +239,8 @@ environment or file channel.
233
239
  | `fetchTimeoutMs` | `DEEPSEEK_FETCH_TIMEOUT_MS` | `30000` | Fetch timeout, 1,000–120,000 ms. |
234
240
  | `fetchOverrideWebfetch` | — | `false` | Replace the built-in `webfetch`. |
235
241
  | `fetchRegisterTool` | — | `true` | Register the separate `deepseek_fetch` tool. |
236
- | `fetchAllowedDomains` | `DEEPSEEK_FETCH_ALLOWED_DOMAINS` | `[]` | If non-empty, only these hosts and subdomains may be fetched. |
237
- | `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. |
238
244
  | `fetchBlockPrivateHosts` | `DEEPSEEK_FETCH_BLOCK_PRIVATE_HOSTS` | `true` | Reject localhost, private IPs, `.local`, `.internal`, and similar hosts. |
239
245
 
240
246
  ### Credentials and retries