deepseek-web-retrieval 0.8.0 → 0.9.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 (40) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +13 -5
  3. package/dist/cli.js +281 -167
  4. package/docs/agents/claude-code.md +7 -1
  5. package/docs/agents/codex.md +7 -1
  6. package/docs/agents/opencode-v2.md +11 -3
  7. package/docs/agents/pi.md +6 -1
  8. package/docs/configuration.md +7 -0
  9. package/docs/design/installer-design.md +4 -4
  10. package/docs/research/ori-question-sourcing.md +12 -1
  11. package/package.json +3 -2
  12. package/src/core/auth.ts +7 -0
  13. package/src/core/client.ts +89 -25
  14. package/src/core/config-summary.ts +1 -0
  15. package/src/core/constants.ts +17 -2
  16. package/src/core/descriptions.ts +10 -1
  17. package/src/core/env.ts +3 -1
  18. package/src/core/format.ts +25 -4
  19. package/src/core/options.ts +47 -0
  20. package/src/core/search.ts +97 -14
  21. package/src/core/types.ts +17 -0
  22. package/src/installer/apply.ts +4 -4
  23. package/src/installer/cli.ts +2 -2
  24. package/src/installer/config-command.ts +4 -3
  25. package/src/installer/hosts/claude-code.ts +3 -17
  26. package/src/installer/hosts/codex.ts +3 -17
  27. package/src/installer/hosts/index.ts +2 -2
  28. package/src/installer/hosts/opencode.ts +1 -1
  29. package/src/installer/hosts/pi.ts +14 -15
  30. package/src/installer/hosts/shared.ts +23 -0
  31. package/src/installer/ownership.ts +2 -2
  32. package/src/installer/paths.ts +2 -2
  33. package/src/installer/{state.ts → record.ts} +15 -15
  34. package/src/installer/types.ts +1 -1
  35. package/src/installer/uninstall.ts +4 -4
  36. package/src/mcp/serve.ts +1 -2
  37. package/src/mcp/server.ts +16 -19
  38. package/src/opencode/plugin.ts +15 -10
  39. package/src/pi/extension.ts +23 -17
  40. package/src/mcp/auth.ts +0 -11
package/src/mcp/server.ts CHANGED
@@ -2,20 +2,23 @@ import type { CallToolResult, ServerContext } from "@modelcontextprotocol/server
2
2
  import { McpServer } from "@modelcontextprotocol/server";
3
3
  import * as z from "zod/v4";
4
4
  import pkg from "../../package.json";
5
+ import { requireApiKey } from "../core/auth";
5
6
  import type { RetrievalClient } from "../core/client";
6
- import { retrievalClient } from "../core/client";
7
- import { ENABLE_FETCH_ENV, ENABLE_SEARCH_ENV } from "../core/constants";
7
+ import { requestContext, retrievalClient } from "../core/client";
8
+ import { ENABLE_FETCH_ENV, ENABLE_SEARCH_ENV, MAX_FETCH_CHARS, MAX_SEARCH_COUNT } from "../core/constants";
8
9
  import {
10
+ FETCH_MAX_CHARS_PARAMETER,
9
11
  FETCH_TOOL_DESCRIPTION,
10
12
  FETCH_URL_PARAMETER,
13
+ SEARCH_COUNT_PARAMETER,
11
14
  SEARCH_ORI_QUESTION_PARAMETER,
12
15
  SEARCH_QUERY_PARAMETER,
13
16
  SEARCH_TOOL_DESCRIPTION,
14
17
  } from "../core/descriptions";
15
18
  import type { Env } from "../core/env";
16
19
  import { formatSearchResults, toStructuredResult } from "../core/format";
20
+ import { bothDisabledPrecheck, withPerCallCount } from "../core/options";
17
21
  import type { ResolvedOptions } from "../core/types";
18
- import { requireApiKey } from "./auth";
19
22
  import { omitStructuredContent } from "./client";
20
23
 
21
24
  export const MCP_SERVER_NAME = "deepseek-web-retrieval";
@@ -23,10 +26,12 @@ export const MCP_SERVER_NAME = "deepseek-web-retrieval";
23
26
  const SEARCH_INPUT = z.object({
24
27
  query: z.string().min(1).describe(SEARCH_QUERY_PARAMETER),
25
28
  original_question: z.string().optional().describe(SEARCH_ORI_QUESTION_PARAMETER),
29
+ count: z.number().int().min(1).max(MAX_SEARCH_COUNT).optional().describe(SEARCH_COUNT_PARAMETER),
26
30
  });
27
31
 
28
32
  const FETCH_INPUT = z.object({
29
33
  url: z.string().min(1).describe(FETCH_URL_PARAMETER),
34
+ maxChars: z.number().int().min(0).max(MAX_FETCH_CHARS).optional().describe(FETCH_MAX_CHARS_PARAMETER),
30
35
  });
31
36
 
32
37
  /**
@@ -49,15 +54,15 @@ export function createDeepSeekServer(
49
54
  inputSchema: SEARCH_INPUT,
50
55
  annotations: { readOnlyHint: true },
51
56
  },
52
- async ({ query, original_question }, ctx) => {
57
+ async ({ query, original_question, count }, ctx) => {
53
58
  try {
54
- const request = requestContext(config, env, ctx.mcpReq.signal);
55
- const results = await client.searchWeb(query, config.search, {
59
+ const request = requestContext(config, requireApiKey(env), ctx.mcpReq.signal);
60
+ const outcome = await client.searchWeb(query, withPerCallCount(config.search, count), {
56
61
  ...request,
57
62
  oriQuestion: original_question,
58
63
  });
59
- return callResult(server, ctx, formatSearchResults(results), {
60
- results: results.map(toStructuredResult),
64
+ return callResult(server, ctx, formatSearchResults(outcome.results, outcome), {
65
+ results: outcome.results.map(toStructuredResult),
61
66
  });
62
67
  } catch (error) {
63
68
  return toErrorResult(error);
@@ -75,9 +80,9 @@ export function createDeepSeekServer(
75
80
  inputSchema: FETCH_INPUT,
76
81
  annotations: { readOnlyHint: true },
77
82
  },
78
- async ({ url }, ctx) => {
83
+ async ({ url, maxChars }, ctx) => {
79
84
  try {
80
- const request = requestContext(config, env, ctx.mcpReq.signal);
85
+ const request = requestContext(config, requireApiKey(env), ctx.mcpReq.signal, { maxChars });
81
86
  const page = await client.fetchDocument(url, config.fetch, request);
82
87
  return callResult(server, ctx, page.content, {
83
88
  url: page.url,
@@ -95,10 +100,6 @@ export function createDeepSeekServer(
95
100
  return server;
96
101
  }
97
102
 
98
- function requestContext(config: ResolvedOptions, env: Env, signal: AbortSignal) {
99
- return { signal, auth: { apiKey: requireApiKey(env) }, retry: config.retry };
100
- }
101
-
102
103
  /**
103
104
  * Text content always reaches the model; the structured mirror is dropped for
104
105
  * clients that would replace the text with its serialized JSON (Codex does).
@@ -118,11 +119,7 @@ function callResult(
118
119
 
119
120
  /** Returns the both-disabled message, or undefined when a tool stays enabled. */
120
121
  export function servePrecheck(config: ResolvedOptions): string | undefined {
121
- if (config.search.enabled || config.fetch.enabled) return undefined;
122
- return (
123
- `Both tools are disabled (${ENABLE_SEARCH_ENV} and ${ENABLE_FETCH_ENV} are false); ` +
124
- "at least one must stay enabled."
125
- );
122
+ return bothDisabledPrecheck(config, `${ENABLE_SEARCH_ENV} and ${ENABLE_FETCH_ENV} are false`);
126
123
  }
127
124
 
128
125
  function toErrorResult(error: unknown): CallToolResult {
@@ -2,10 +2,10 @@ import { Plugin } from "@opencode/plugin";
2
2
  import type { Context } from "@opencode/plugin/promise/plugin";
3
3
  import type { ToolContext } from "@opencode/plugin/promise/tool";
4
4
  import type { RetrievalClient } from "../core/client";
5
- import { inspectFetchTarget, retrievalClient } from "../core/client";
5
+ import { inspectFetchTarget, requestContext, retrievalClient } from "../core/client";
6
6
  import { sharedFileOptions } from "../core/config-file";
7
7
  import { configDeviations } from "../core/config-summary";
8
- import { FETCH_MARKDOWN_ONLY, FETCH_OUTPUT_TRUNCATED } from "../core/descriptions";
8
+ import { FETCH_MARKDOWN_ONLY, FETCH_MAX_CHARS_PARAMETER, FETCH_OUTPUT_TRUNCATED } from "../core/descriptions";
9
9
  import { pluginOptionsFromEnv } from "../core/env";
10
10
  import { formatQualityMarker } from "../core/format";
11
11
  import { resolveFetchTimeoutMs, resolvePluginOptions } from "../core/options";
@@ -16,6 +16,7 @@ type FetchInput = {
16
16
  readonly url: string;
17
17
  readonly format?: "markdown" | "text" | "html";
18
18
  readonly timeout?: number;
19
+ readonly maxChars?: number;
19
20
  };
20
21
 
21
22
  const FETCH_INPUT = {
@@ -38,6 +39,12 @@ const FETCH_INPUT = {
38
39
  description:
39
40
  "Optional timeout in seconds (maximum: 120). The effective limit is the shorter of this and fetchTimeoutMs.",
40
41
  },
42
+ maxChars: {
43
+ type: "integer",
44
+ minimum: 0,
45
+ maximum: 1_000_000,
46
+ description: FETCH_MAX_CHARS_PARAMETER,
47
+ },
41
48
  },
42
49
  required: ["url"],
43
50
  additionalProperties: false,
@@ -89,11 +96,6 @@ export function resolveRegistration(input?: PluginOptions): Registration {
89
96
  };
90
97
  }
91
98
 
92
- /** One place builds the per-request context the core expects. */
93
- function requestContext(config: ResolvedOptions, apiKey: string, signal: AbortSignal, timeoutMs?: number) {
94
- return { signal, auth: { apiKey }, retry: config.retry, ...(timeoutMs === undefined ? {} : { timeoutMs }) };
95
- }
96
-
97
99
  async function runFetch(
98
100
  ctx: Context,
99
101
  config: ResolvedOptions,
@@ -135,7 +137,7 @@ async function runFetch(
135
137
  const page = await client.fetchDocument(
136
138
  target,
137
139
  config.fetch,
138
- requestContext(config, apiKey, context.signal, timeoutMs),
140
+ requestContext(config, apiKey, context.signal, { timeoutMs, maxChars: input.maxChars }),
139
141
  );
140
142
  return {
141
143
  content: page.content,
@@ -173,12 +175,15 @@ export function createOpenCodePlugin(client: RetrievalClient = retrievalClient)
173
175
  name: "DeepSeek",
174
176
  execute: async (input, { signal }) => {
175
177
  const apiKey = await resolveApiKey(ctx, config.auth.source);
176
- const results = await client.searchWeb(
178
+ const outcome = await client.searchWeb(
177
179
  input.query,
178
180
  config.search,
179
181
  requestContext(config, apiKey, signal),
180
182
  );
181
- return results.map(toOpenCodeResult);
183
+ // OpenCode's result shape is closed, so the effective-query header
184
+ // has nowhere to ride on this path (documented in the guide);
185
+ // degradation notes still surface through thrown error messages.
186
+ return outcome.results.map(toOpenCodeResult);
182
187
  },
183
188
  });
184
189
  if (registration.setDefault) editor.default.set("deepseek");
@@ -3,14 +3,16 @@ import { Type } from "@earendil-works/pi-ai";
3
3
  import type { AgentToolResult, ExtensionAPI, ExtensionToolContext } from "@earendil-works/pi-coding-agent";
4
4
  import { defineTool } from "@earendil-works/pi-coding-agent";
5
5
  import { apiKeyFromEnv } from "../core/auth";
6
- import type { RequestOptions, RetrievalClient } from "../core/client";
7
- import { inspectFetchTarget, retrievalClient } from "../core/client";
6
+ import type { RetrievalClient } from "../core/client";
7
+ import { inspectFetchTarget, requestContext, retrievalClient } from "../core/client";
8
8
  import { sharedFileOptions } from "../core/config-file";
9
9
  import { configDeviations } from "../core/config-summary";
10
- import { API_KEY_ENV, ENABLE_FETCH_ENV, ENABLE_SEARCH_ENV } from "../core/constants";
10
+ import { API_KEY_ENV, ENABLE_FETCH_ENV, ENABLE_SEARCH_ENV, MAX_FETCH_CHARS, MAX_SEARCH_COUNT } from "../core/constants";
11
11
  import {
12
+ FETCH_MAX_CHARS_PARAMETER,
12
13
  FETCH_TOOL_DESCRIPTION,
13
14
  FETCH_URL_PARAMETER,
15
+ SEARCH_COUNT_PARAMETER,
14
16
  SEARCH_ORI_QUESTION_PARAMETER,
15
17
  SEARCH_QUERY_PARAMETER,
16
18
  SEARCH_TOOL_DESCRIPTION,
@@ -18,7 +20,7 @@ import {
18
20
  import type { Env } from "../core/env";
19
21
  import { pluginOptionsFromEnv } from "../core/env";
20
22
  import { formatSearchResults, toStructuredResult } from "../core/format";
21
- import { resolvePluginOptions } from "../core/options";
23
+ import { bothDisabledPrecheck, resolvePluginOptions, withPerCallCount } from "../core/options";
22
24
  import type { ResolvedOptions } from "../core/types";
23
25
  import type { ExtensionSettings } from "./settings";
24
26
  import { readExtensionSettings, SETTINGS_NAMESPACE } from "./settings";
@@ -30,6 +32,7 @@ const SEARCH_PARAMETERS = Type.Object({
30
32
  description: `${SEARCH_ORI_QUESTION_PARAMETER} When left blank, the most recent user message in the session is sent instead, truncated to fit.`,
31
33
  }),
32
34
  ),
35
+ count: Type.Optional(Type.Integer({ minimum: 1, maximum: MAX_SEARCH_COUNT, description: SEARCH_COUNT_PARAMETER })),
33
36
  });
34
37
 
35
38
  const SEARCH_OUTPUT = Type.Object({
@@ -48,6 +51,9 @@ const SEARCH_OUTPUT = Type.Object({
48
51
 
49
52
  const FETCH_PARAMETERS = Type.Object({
50
53
  url: Type.String({ minLength: 1, description: FETCH_URL_PARAMETER }),
54
+ maxChars: Type.Optional(
55
+ Type.Integer({ minimum: 0, maximum: MAX_FETCH_CHARS, description: FETCH_MAX_CHARS_PARAMETER }),
56
+ ),
51
57
  });
52
58
 
53
59
  const FETCH_OUTPUT = Type.Object({
@@ -77,10 +83,10 @@ export function resolveExtensionOptions(env: Env = process.env, settings: Extens
77
83
 
78
84
  /** Returns the both-disabled message, or undefined when a tool stays enabled. */
79
85
  export function extensionPrecheck(config: ResolvedOptions): string | undefined {
80
- if (config.search.enabled || config.fetch.enabled) return undefined;
81
- return (
82
- `Both tools are disabled (${ENABLE_SEARCH_ENV} and ${ENABLE_FETCH_ENV} are false, or the ` +
83
- `${SETTINGS_NAMESPACE} settings namespace disabled them); at least one must stay enabled.`
86
+ return bothDisabledPrecheck(
87
+ config,
88
+ `${ENABLE_SEARCH_ENV} and ${ENABLE_FETCH_ENV} are false, or the ` +
89
+ `${SETTINGS_NAMESPACE} settings namespace disabled them`,
84
90
  );
85
91
  }
86
92
 
@@ -100,13 +106,13 @@ export function searchTool(config: ResolvedOptions, client: RetrievalClient = re
100
106
  // The explicit parameter wins; only when the model left it blank does
101
107
  // the session fallback get a say.
102
108
  const explicit = params.original_question?.trim() ? params.original_question : undefined;
103
- const results = await client.searchWeb(params.query, config.search, {
104
- ...requestContext(config, signal),
109
+ const outcome = await client.searchWeb(params.query, withPerCallCount(config.search, params.count), {
110
+ ...requestContext(config, requireApiKey(), signal ?? NEVER_ABORTS),
105
111
  oriQuestion: explicit ?? sessionOriQuestion(ctx),
106
112
  });
107
113
  return {
108
- content: [{ type: "text", text: formatSearchResults(results) }],
109
- structuredContent: { results: results.map(toStructuredResult) },
114
+ content: [{ type: "text", text: formatSearchResults(outcome.results, outcome) }],
115
+ structuredContent: { results: outcome.results.map(toStructuredResult) },
110
116
  details: undefined,
111
117
  };
112
118
  } catch (error) {
@@ -130,7 +136,11 @@ export function fetchTool(config: ResolvedOptions, client: RetrievalClient = ret
130
136
  async execute(_toolCallId, params, signal): Promise<AgentToolResult> {
131
137
  try {
132
138
  const target = inspectFetchTarget(params.url, config.fetch);
133
- const page = await client.fetchDocument(target, config.fetch, requestContext(config, signal));
139
+ const page = await client.fetchDocument(
140
+ target,
141
+ config.fetch,
142
+ requestContext(config, requireApiKey(), signal ?? NEVER_ABORTS, { maxChars: params.maxChars }),
143
+ );
134
144
  return {
135
145
  content: [{ type: "text", text: page.content }],
136
146
  structuredContent: {
@@ -168,10 +178,6 @@ export default function deepSeekWebRetrieval(pi: ExtensionAPI): void {
168
178
  if (config.fetch.enabled) pi.registerTool(fetchTool(config));
169
179
  }
170
180
 
171
- function requestContext(config: ResolvedOptions, signal: AbortSignal | undefined): RequestOptions {
172
- return { signal: signal ?? NEVER_ABORTS, auth: { apiKey: requireApiKey() }, retry: config.retry };
173
- }
174
-
175
181
  /**
176
182
  * Best-effort `ori_question` fallback for when the model left the tool input
177
183
  * blank: the most recent user message text in the session projection, readable
package/src/mcp/auth.ts DELETED
@@ -1,11 +0,0 @@
1
- import { apiKeyFromEnv, MISSING_API_KEY_MESSAGE } from "../core/auth";
2
- import type { Env } from "../core/env";
3
-
4
- export { MISSING_API_KEY_MESSAGE };
5
-
6
- /** Resolves the API key from the environment or fails with shared guidance. */
7
- export function requireApiKey(env: Env): string {
8
- const apiKey = apiKeyFromEnv(env);
9
- if (!apiKey) throw new Error(MISSING_API_KEY_MESSAGE);
10
- return apiKey;
11
- }