deepseek-web-retrieval 0.4.0 → 0.5.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 (61) hide show
  1. package/CHANGELOG.md +93 -244
  2. package/README.md +16 -11
  3. package/dist/cli.js +1366 -615
  4. package/docs/agents/codex.md +28 -7
  5. package/docs/agents/opencode-v2.md +1 -1
  6. package/docs/agents/pi.md +88 -86
  7. package/docs/design/installer-design.md +72 -9
  8. package/docs/hosts.jsonc +2 -2
  9. package/docs/integration.md +14 -9
  10. package/docs/research/control-matrix.md +48 -13
  11. package/docs/research/mcp-compatibility.md +9 -4
  12. package/examples/pi.settings.jsonc +17 -0
  13. package/index.ts +2 -2
  14. package/package.json +35 -4
  15. package/src/core/auth.ts +4 -6
  16. package/src/core/client.ts +65 -53
  17. package/src/core/constants.ts +24 -24
  18. package/src/core/descriptions.ts +30 -6
  19. package/src/core/domains.ts +56 -67
  20. package/src/core/env.ts +112 -0
  21. package/src/core/format.ts +53 -50
  22. package/src/core/index.ts +9 -9
  23. package/src/core/jsonc.ts +6 -6
  24. package/src/core/object.ts +4 -0
  25. package/src/core/options.ts +20 -25
  26. package/src/core/search.ts +45 -44
  27. package/src/core/transport.ts +109 -113
  28. package/src/core/types.ts +47 -47
  29. package/src/core/url.ts +20 -20
  30. package/src/installer/apply.ts +256 -81
  31. package/src/installer/cli.ts +135 -104
  32. package/src/installer/detect.ts +46 -31
  33. package/src/installer/formats/index.ts +26 -28
  34. package/src/installer/formats/json.ts +81 -77
  35. package/src/installer/formats/rules.ts +55 -28
  36. package/src/installer/formats/toml.ts +88 -81
  37. package/src/installer/formats/value.ts +34 -30
  38. package/src/installer/hosts/codex.ts +100 -54
  39. package/src/installer/hosts/index.ts +126 -41
  40. package/src/installer/hosts/opencode.ts +70 -51
  41. package/src/installer/hosts/pi.ts +269 -75
  42. package/src/installer/hosts/shared.ts +19 -14
  43. package/src/installer/operation-lock.ts +54 -0
  44. package/src/installer/ownership.ts +129 -0
  45. package/src/installer/paths.ts +7 -5
  46. package/src/installer/persistence.ts +66 -0
  47. package/src/installer/render.ts +29 -29
  48. package/src/installer/revert.ts +33 -29
  49. package/src/installer/rulesContent.ts +24 -13
  50. package/src/installer/state.ts +116 -31
  51. package/src/installer/types.ts +74 -56
  52. package/src/installer/uninstall.ts +40 -17
  53. package/src/mcp/auth.ts +7 -8
  54. package/src/mcp/cli.ts +38 -20
  55. package/src/mcp/client.ts +35 -0
  56. package/src/mcp/server.ts +68 -97
  57. package/src/opencode/auth.ts +24 -26
  58. package/src/opencode/plugin.ts +99 -87
  59. package/src/pi/extension.ts +164 -0
  60. package/src/pi/settings.ts +49 -0
  61. package/src/mcp/config.ts +0 -112
package/src/mcp/server.ts CHANGED
@@ -1,97 +1,64 @@
1
- import { McpServer } from "@modelcontextprotocol/server"
2
- import type { CallToolResult } from "@modelcontextprotocol/server"
3
- import * as z from "zod/v4"
4
- import { fetchDocument, searchWeb } from "../core/client"
5
- import { ENABLE_FETCH_ENV, ENABLE_SEARCH_ENV } from "../core/constants"
1
+ import type { CallToolResult, ServerContext } from "@modelcontextprotocol/server";
2
+ import { McpServer } from "@modelcontextprotocol/server";
3
+ import * as z from "zod/v4";
4
+ import pkg from "../../package.json";
5
+ import type { RetrievalClient } from "../core/client";
6
+ import { retrievalClient } from "../core/client";
7
+ import { ENABLE_FETCH_ENV, ENABLE_SEARCH_ENV } from "../core/constants";
6
8
  import {
7
- FETCH_OUTPUT_TRUNCATED,
8
- FETCH_SERVICE_LIMITS,
9
- SEARCH_EMPTY_IS_NORMAL,
10
- SEARCH_QUERY_OPERATORS,
11
- SEARCH_RANKED_FILTERED,
12
- } from "../core/descriptions"
13
- import { formatSearchResults } from "../core/format"
14
- import type { ResolvedOptions, SearchResult } from "../core/types"
15
- import pkg from "../../package.json"
16
- import { requireApiKey } from "./auth"
17
- import type { Env } from "./config"
9
+ FETCH_TOOL_DESCRIPTION,
10
+ FETCH_URL_PARAMETER,
11
+ SEARCH_QUERY_PARAMETER,
12
+ SEARCH_TOOL_DESCRIPTION,
13
+ } from "../core/descriptions";
14
+ import type { Env } from "../core/env";
15
+ import { formatSearchResults, toStructuredResult } from "../core/format";
16
+ import type { ResolvedOptions } from "../core/types";
17
+ import { requireApiKey } from "./auth";
18
+ import { omitStructuredContent } from "./client";
18
19
 
19
- export const MCP_SERVER_NAME = "deepseek-web-retrieval"
20
+ export const MCP_SERVER_NAME = "deepseek-web-retrieval";
20
21
 
21
22
  const SEARCH_INPUT = z.object({
22
- query: z
23
- .string()
24
- .min(1)
25
- .describe(
26
- "Search query, 128 characters or fewer including operators (site:, -site:, quoted phrases, lang:).",
27
- ),
28
- })
29
-
30
- const SEARCH_OUTPUT = z.object({
31
- results: z.array(
32
- z.object({
33
- url: z.string(),
34
- title: z.string().optional(),
35
- content: z.string().optional(),
36
- published: z.number().optional(),
37
- }),
38
- ),
39
- })
23
+ query: z.string().min(1).describe(SEARCH_QUERY_PARAMETER),
24
+ });
40
25
 
41
26
  const FETCH_INPUT = z.object({
42
- url: z.string().min(1).describe("Absolute http:// or https:// URL to fetch. Credentials are rejected."),
43
- })
44
-
45
- const FETCH_OUTPUT = z.object({
46
- url: z.string(),
47
- title: z.string(),
48
- content: z.string(),
49
- truncated: z.boolean(),
50
- })
51
-
52
- const SEARCH_DESCRIPTION =
53
- "Search the web with DeepSeek's in-house search engine and return a short, filtered result " +
54
- "list (title, URL, summary). " +
55
- `${SEARCH_RANKED_FILTERED} ` +
56
- `${SEARCH_EMPTY_IS_NORMAL} ` +
57
- `${SEARCH_QUERY_OPERATORS} ` +
58
- "To read a full page, pass a result URL to the deepseek_fetch tool."
59
-
60
- const FETCH_DESCRIPTION =
61
- "Fetch one web page as clean Markdown through DeepSeek's Fetch API. " +
62
- `${FETCH_SERVICE_LIMITS} ` +
63
- `${FETCH_OUTPUT_TRUNCATED}`
27
+ url: z.string().min(1).describe(FETCH_URL_PARAMETER),
28
+ });
64
29
 
65
30
  /**
66
31
  * Builds the host-agnostic MCP server exposing the two DeepSeek retrieval tools.
67
32
  * `serveStdio` and `createMcpHandler` serve both protocol eras from this factory.
68
33
  */
69
- export function createDeepSeekServer(config: ResolvedOptions, env: Env = process.env): McpServer {
70
- const server = new McpServer({ name: MCP_SERVER_NAME, version: pkg.version })
34
+ export function createDeepSeekServer(
35
+ config: ResolvedOptions,
36
+ env: Env = process.env,
37
+ client: RetrievalClient = retrievalClient,
38
+ ): McpServer {
39
+ const server = new McpServer({ name: MCP_SERVER_NAME, version: pkg.version });
71
40
 
72
41
  if (config.search.enabled) {
73
42
  server.registerTool(
74
43
  "deepseek_web_search",
75
44
  {
76
45
  title: "DeepSeek Web Search",
77
- description: SEARCH_DESCRIPTION,
46
+ description: SEARCH_TOOL_DESCRIPTION,
78
47
  inputSchema: SEARCH_INPUT,
79
- outputSchema: SEARCH_OUTPUT,
80
48
  annotations: { readOnlyHint: true },
81
49
  },
82
50
  async ({ query }, ctx) => {
83
51
  try {
84
- const request = requestContext(config, env, ctx.mcpReq.signal)
85
- const results = await searchWeb(query, config.search, request)
86
- return {
87
- content: [{ type: "text", text: formatSearchResults(results) }],
88
- structuredContent: { results: results.map(toStructuredResult) },
89
- }
52
+ const request = requestContext(config, env, ctx.mcpReq.signal);
53
+ const results = await client.searchWeb(query, config.search, request);
54
+ return callResult(server, ctx, formatSearchResults(results), {
55
+ results: results.map(toStructuredResult),
56
+ });
90
57
  } catch (error) {
91
- return toErrorResult(error)
58
+ return toErrorResult(error);
92
59
  }
93
60
  },
94
- )
61
+ );
95
62
  }
96
63
 
97
64
  if (config.fetch.enabled) {
@@ -99,57 +66,61 @@ export function createDeepSeekServer(config: ResolvedOptions, env: Env = process
99
66
  "deepseek_fetch",
100
67
  {
101
68
  title: "DeepSeek Fetch",
102
- description: FETCH_DESCRIPTION,
69
+ description: FETCH_TOOL_DESCRIPTION,
103
70
  inputSchema: FETCH_INPUT,
104
- outputSchema: FETCH_OUTPUT,
105
71
  annotations: { readOnlyHint: true },
106
72
  },
107
73
  async ({ url }, ctx) => {
108
74
  try {
109
- const request = requestContext(config, env, ctx.mcpReq.signal)
110
- const page = await fetchDocument(url, config.fetch, request)
111
- return {
112
- content: [{ type: "text", text: page.content }],
113
- structuredContent: {
114
- url: page.url,
115
- title: page.title,
116
- content: page.content,
117
- truncated: page.truncated,
118
- },
119
- }
75
+ const request = requestContext(config, env, ctx.mcpReq.signal);
76
+ const page = await client.fetchDocument(url, config.fetch, request);
77
+ return callResult(server, ctx, page.content, {
78
+ url: page.url,
79
+ title: page.title,
80
+ content: page.content,
81
+ truncated: page.truncated,
82
+ });
120
83
  } catch (error) {
121
- return toErrorResult(error)
84
+ return toErrorResult(error);
122
85
  }
123
86
  },
124
- )
87
+ );
125
88
  }
126
89
 
127
- return server
90
+ return server;
128
91
  }
129
92
 
130
93
  function requestContext(config: ResolvedOptions, env: Env, signal: AbortSignal) {
131
- return { signal, auth: { apiKey: requireApiKey(env) }, retry: config.retry }
94
+ return { signal, auth: { apiKey: requireApiKey(env) }, retry: config.retry };
132
95
  }
133
96
 
134
- function toStructuredResult(result: SearchResult) {
135
- return {
136
- url: result.url,
137
- ...(result.title ? { title: result.title } : {}),
138
- ...(result.content ? { content: result.content } : {}),
139
- ...(result.published !== undefined ? { published: result.published } : {}),
140
- }
97
+ /**
98
+ * Text content always reaches the model; the structured mirror is dropped for
99
+ * clients that would replace the text with its serialized JSON (Codex does).
100
+ * The tools declare no `outputSchema` on purpose: the SDK rejects text-only
101
+ * results for a tool with a declared schema ("no structured content was
102
+ * provided"), and no host in the matrix validates the schema anyway.
103
+ */
104
+ function callResult(
105
+ server: McpServer,
106
+ ctx: ServerContext,
107
+ text: string,
108
+ structured: Record<string, unknown>,
109
+ ): CallToolResult {
110
+ if (omitStructuredContent(server, ctx)) return { content: [{ type: "text", text }] };
111
+ return { content: [{ type: "text", text }], structuredContent: structured };
141
112
  }
142
113
 
143
114
  /** Returns the both-disabled message, or undefined when a tool stays enabled. */
144
115
  export function servePrecheck(config: ResolvedOptions): string | undefined {
145
- if (config.search.enabled || config.fetch.enabled) return undefined
116
+ if (config.search.enabled || config.fetch.enabled) return undefined;
146
117
  return (
147
118
  `Both tools are disabled (${ENABLE_SEARCH_ENV} and ${ENABLE_FETCH_ENV} are false); ` +
148
119
  "at least one must stay enabled."
149
- )
120
+ );
150
121
  }
151
122
 
152
123
  function toErrorResult(error: unknown): CallToolResult {
153
- const message = error instanceof Error ? error.message : String(error)
154
- return { content: [{ type: "text", text: message }], isError: true }
124
+ const message = error instanceof Error ? error.message : String(error);
125
+ return { content: [{ type: "text", text: message }], isError: true };
155
126
  }
@@ -1,23 +1,23 @@
1
- import type { Context } from "@opencode/plugin/promise/plugin"
2
- import { API_KEY_ENV } from "../core/constants"
3
- import { apiKeyFromEnv } from "../core/auth"
4
- import type { ApiKeySource } from "../core/types"
1
+ import type { Context } from "@opencode/plugin/promise/plugin";
2
+ import { apiKeyFromEnv } from "../core/auth";
3
+ import { API_KEY_ENV } from "../core/constants";
4
+ import type { ApiKeySource } from "../core/types";
5
5
 
6
- const DEEPSEEK_INTEGRATION_ID = "deepseek"
6
+ const DEEPSEEK_INTEGRATION_ID = "deepseek";
7
7
 
8
8
  /** OpenCode-specific guidance for a missing key; the core stays agent-agnostic. */
9
9
  function missingApiKeyError(source: ApiKeySource): Error {
10
10
  if (source === "env") {
11
- return new Error(`${API_KEY_ENV} is not set. Export it before starting OpenCode.`)
11
+ return new Error(`${API_KEY_ENV} is not set. Export it before starting OpenCode.`);
12
12
  }
13
13
  if (source === "opencode") {
14
14
  return new Error(
15
15
  'No DeepSeek credential is stored in OpenCode. Run "opencode auth login" and connect DeepSeek, or set apiKeySource to "env".',
16
- )
16
+ );
17
17
  }
18
18
  return new Error(
19
19
  `DeepSeek API key is not configured. Connect DeepSeek in OpenCode ("opencode auth login") or set ${API_KEY_ENV}.`,
20
- )
20
+ );
21
21
  }
22
22
 
23
23
  /**
@@ -26,45 +26,43 @@ function missingApiKeyError(source: ApiKeySource): Error {
26
26
  * `DEEPSEEK_API_KEY` remains available for headless setups.
27
27
  */
28
28
  export async function resolveApiKey(ctx: Context, source: ApiKeySource): Promise<string> {
29
- const envKey = apiKeyFromEnv(process.env)
29
+ const envKey = apiKeyFromEnv(process.env);
30
30
 
31
31
  if (source === "env") {
32
- if (envKey) return envKey
33
- throw missingApiKeyError("env")
32
+ if (envKey) return envKey;
33
+ throw missingApiKeyError("env");
34
34
  }
35
35
 
36
- let stored: string | undefined
37
- let storedError: unknown
36
+ let stored: string | undefined;
37
+ let storedError: unknown;
38
38
  try {
39
- const connection = await ctx.integration.connection.active(DEEPSEEK_INTEGRATION_ID)
40
- const credential = connection
41
- ? await ctx.integration.connection.resolve(connection)
42
- : undefined
39
+ const connection = await ctx.integration.connection.active(DEEPSEEK_INTEGRATION_ID);
40
+ const credential = connection ? await ctx.integration.connection.resolve(connection) : undefined;
43
41
  if (credential?.type === "key") {
44
- const key = credential.key.trim()
45
- if (key) stored = key
42
+ const key = credential.key.trim();
43
+ if (key) stored = key;
46
44
  }
47
45
  } catch (error) {
48
- storedError = error
46
+ storedError = error;
49
47
  }
50
48
 
51
- if (stored) return stored
49
+ if (stored) return stored;
52
50
  if (source === "opencode") {
53
51
  if (storedError) {
54
52
  throw new Error(
55
53
  'Could not read the DeepSeek credential from OpenCode. Run "opencode auth login" and connect DeepSeek.',
56
54
  { cause: storedError },
57
- )
55
+ );
58
56
  }
59
- throw missingApiKeyError("opencode")
57
+ throw missingApiKeyError("opencode");
60
58
  }
61
- if (envKey) return envKey
59
+ if (envKey) return envKey;
62
60
  if (storedError) {
63
61
  throw new Error(
64
62
  `DeepSeek API key is not configured, and the OpenCode credential could not be read. ` +
65
63
  `Set ${API_KEY_ENV} or run "opencode auth login".`,
66
64
  { cause: storedError },
67
- )
65
+ );
68
66
  }
69
- throw missingApiKeyError("auto")
67
+ throw missingApiKeyError("auto");
70
68
  }
@@ -1,17 +1,18 @@
1
- import { Plugin } from "@opencode/plugin"
2
- import type { Context } from "@opencode/plugin/promise/plugin"
3
- import type { ToolContext } from "@opencode/plugin/promise/tool"
4
- import { fetchDocument, inspectFetchTarget, searchWeb } from "../core/client"
5
- import { FETCH_MARKDOWN_ONLY, FETCH_OUTPUT_TRUNCATED } from "../core/descriptions"
6
- import { resolveFetchTimeoutMs, resolvePluginOptions } from "../core/options"
7
- import type { PluginOptions, ResolvedOptions, SearchResult } from "../core/types"
8
- import { resolveApiKey } from "./auth"
1
+ import { Plugin } from "@opencode/plugin";
2
+ import type { Context } from "@opencode/plugin/promise/plugin";
3
+ import type { ToolContext } from "@opencode/plugin/promise/tool";
4
+ import type { RetrievalClient } from "../core/client";
5
+ import { inspectFetchTarget, retrievalClient } from "../core/client";
6
+ import { FETCH_MARKDOWN_ONLY, FETCH_OUTPUT_TRUNCATED } from "../core/descriptions";
7
+ import { resolveFetchTimeoutMs, resolvePluginOptions } from "../core/options";
8
+ import type { PluginOptions, ResolvedOptions, SearchResult } from "../core/types";
9
+ import { resolveApiKey } from "./auth";
9
10
 
10
11
  type FetchInput = {
11
- readonly url: string
12
- readonly format?: "markdown" | "text" | "html"
13
- readonly timeout?: number
14
- }
12
+ readonly url: string;
13
+ readonly format?: "markdown" | "text" | "html";
14
+ readonly timeout?: number;
15
+ };
15
16
 
16
17
  const FETCH_INPUT = {
17
18
  type: "object",
@@ -36,23 +37,23 @@ const FETCH_INPUT = {
36
37
  },
37
38
  required: ["url"],
38
39
  additionalProperties: false,
39
- } as const
40
+ } as const;
40
41
 
41
42
  const DEEPSEEK_FETCH_DESCRIPTION =
42
43
  "Fetch one web page as clean Markdown through DeepSeek's Fetch API. This separate tool remains " +
43
- `available when the built-in webfetch is replaced. ${FETCH_MARKDOWN_ONLY} ${FETCH_OUTPUT_TRUNCATED}`
44
+ `available when the built-in webfetch is replaced. ${FETCH_MARKDOWN_ONLY} ${FETCH_OUTPUT_TRUNCATED}`;
44
45
 
45
46
  const WEBFETCH_DESCRIPTION =
46
47
  "Fetch content from an HTTP or HTTPS URL and return it as Markdown through DeepSeek's Fetch API. " +
47
- "This is the built-in webfetch replacement enabled by fetchOverrideWebfetch. DeepSeek does not " +
48
- `render JavaScript and some sites return 403/404. ${FETCH_MARKDOWN_ONLY} ${FETCH_OUTPUT_TRUNCATED}`
48
+ "This is the built-in webfetch replacement enabled by fetchOverrideWebfetch. DeepSeek does not " +
49
+ `render JavaScript and some sites return 403/404. ${FETCH_MARKDOWN_ONLY} ${FETCH_OUTPUT_TRUNCATED}`;
49
50
 
50
51
  /** OpenCode's WebSearch.Result shape, which wraps the published date in `time`. */
51
52
  interface OpenCodeSearchResult {
52
- readonly url: string
53
- readonly title?: string
54
- readonly content?: string
55
- readonly time: { readonly published?: number }
53
+ readonly url: string;
54
+ readonly title?: string;
55
+ readonly content?: string;
56
+ readonly time: { readonly published?: number };
56
57
  }
57
58
 
58
59
  /** Maps the core's neutral SearchResult onto the shape OpenCode expects. */
@@ -61,17 +62,15 @@ function toOpenCodeResult(result: SearchResult): OpenCodeSearchResult {
61
62
  url: result.url,
62
63
  ...(result.title ? { title: result.title } : {}),
63
64
  ...(result.content ? { content: result.content } : {}),
64
- ...(result.published !== undefined
65
- ? { time: { published: result.published } }
66
- : { time: {} }),
67
- }
65
+ ...(result.published !== undefined ? { time: { published: result.published } } : { time: {} }),
66
+ };
68
67
  }
69
68
 
70
69
  /** How the plugin registers itself with OpenCode; resolved where it is consumed. */
71
70
  export interface Registration {
72
- readonly setDefault: boolean
73
- readonly overrideWebfetch: boolean
74
- readonly registerTool: boolean
71
+ readonly setDefault: boolean;
72
+ readonly overrideWebfetch: boolean;
73
+ readonly registerTool: boolean;
75
74
  }
76
75
 
77
76
  export function resolveRegistration(input?: PluginOptions): Registration {
@@ -79,12 +78,12 @@ export function resolveRegistration(input?: PluginOptions): Registration {
79
78
  setDefault: input?.setDefaultWebsearch === true,
80
79
  overrideWebfetch: input?.fetchOverrideWebfetch === true,
81
80
  registerTool: input?.fetchRegisterTool !== false,
82
- }
81
+ };
83
82
  }
84
83
 
85
84
  /** One place builds the per-request context the core expects. */
86
85
  function requestContext(config: ResolvedOptions, apiKey: string, signal: AbortSignal, timeoutMs?: number) {
87
- return { signal, auth: { apiKey }, retry: config.retry, ...(timeoutMs === undefined ? {} : { timeoutMs }) }
86
+ return { signal, auth: { apiKey }, retry: config.retry, ...(timeoutMs === undefined ? {} : { timeoutMs }) };
88
87
  }
89
88
 
90
89
  async function runFetch(
@@ -94,25 +93,26 @@ async function runFetch(
94
93
  context: ToolContext,
95
94
  permission: string,
96
95
  saveAll: boolean,
96
+ client: RetrievalClient,
97
97
  ) {
98
- const format = input.format ?? "markdown"
98
+ const format = input.format ?? "markdown";
99
99
  if (format !== "markdown") {
100
100
  throw new Error(
101
101
  `DeepSeek Fetch only returns Markdown; format "${format}" is not supported. Omit format or use "markdown".`,
102
- )
102
+ );
103
103
  }
104
104
 
105
105
  if (typeof input.url !== "string") {
106
- throw new Error('"url" must be a string')
106
+ throw new Error('"url" must be a string');
107
107
  }
108
108
  // Inspect before the permission prompt so an invalid or blocked target fails
109
109
  // without asking the user; fetchPage enforces the policy again.
110
- const target = inspectFetchTarget(input.url, config.fetch)
111
- const timeoutMs = resolveFetchTimeoutMs(config.fetch.timeoutMs, input.timeout)
110
+ const target = inspectFetchTarget(input.url, config.fetch);
111
+ const timeoutMs = resolveFetchTimeoutMs(config.fetch.timeoutMs, input.timeout);
112
112
 
113
113
  // Resolved before the permission prompt so a missing key fails fast instead
114
114
  // of asking the user to approve a request that cannot run.
115
- const apiKey = await resolveApiKey(ctx, config.auth.source)
115
+ const apiKey = await resolveApiKey(ctx, config.auth.source);
116
116
 
117
117
  await ctx.permission.assert({
118
118
  sessionID: context.sessionID,
@@ -122,9 +122,13 @@ async function runFetch(
122
122
  save: saveAll ? ["*"] : [target.href],
123
123
  metadata: saveAll ? { ...input, url: target.href, format } : { url: target.href },
124
124
  source: { type: "tool", messageID: context.messageID, id: context.id },
125
- })
125
+ });
126
126
 
127
- const page = await fetchDocument(target, config.fetch, requestContext(config, apiKey, context.signal, timeoutMs))
127
+ const page = await client.fetchDocument(
128
+ target,
129
+ config.fetch,
130
+ requestContext(config, apiKey, context.signal, timeoutMs),
131
+ );
128
132
  return {
129
133
  content: page.content,
130
134
  metadata: {
@@ -132,56 +136,64 @@ async function runFetch(
132
136
  title: page.title,
133
137
  truncated: page.truncated,
134
138
  },
135
- }
139
+ };
136
140
  }
137
141
 
138
- export default Plugin.define({
139
- id: "deepseek-web-retrieval",
140
- async setup(ctx) {
141
- const config = resolvePluginOptions(ctx.options)
142
- const registration = resolveRegistration(ctx.options)
143
-
144
- if (config.search.enabled) {
145
- await ctx.websearch.transform((editor) => {
146
- editor.add({
147
- id: "deepseek",
148
- name: "DeepSeek",
149
- execute: async (input, { signal }) => {
150
- const apiKey = await resolveApiKey(ctx, config.auth.source)
151
- const results = await searchWeb(input.query, config.search, requestContext(config, apiKey, signal))
152
- return results.map(toOpenCodeResult)
153
- },
154
- })
155
- if (registration.setDefault) editor.default.set("deepseek")
156
- })
157
- }
158
-
159
- if (config.fetch.enabled) {
160
- await ctx.tool.transform((editor) => {
161
- if (registration.overrideWebfetch) editor.remove("webfetch")
162
-
163
- if (registration.registerTool) {
164
- editor.add({
165
- name: "deepseek_fetch",
166
- description: DEEPSEEK_FETCH_DESCRIPTION,
167
- input: FETCH_INPUT,
168
- options: { codemode: false, permission: "deepseek_fetch" },
169
- execute: (input, context) =>
170
- runFetch(ctx, config, input as FetchInput, context, "deepseek_fetch", false),
171
- })
172
- }
173
-
174
- if (registration.overrideWebfetch) {
142
+ export function createOpenCodePlugin(client: RetrievalClient = retrievalClient) {
143
+ return Plugin.define({
144
+ id: "deepseek-web-retrieval",
145
+ async setup(ctx) {
146
+ const config = resolvePluginOptions(ctx.options);
147
+ const registration = resolveRegistration(ctx.options);
148
+
149
+ if (config.search.enabled) {
150
+ await ctx.websearch.transform((editor) => {
175
151
  editor.add({
176
- name: "webfetch",
177
- description: WEBFETCH_DESCRIPTION,
178
- input: FETCH_INPUT,
179
- options: { codemode: false, permission: "webfetch" },
180
- execute: (input, context) =>
181
- runFetch(ctx, config, input as FetchInput, context, "webfetch", true),
182
- })
183
- }
184
- })
185
- }
186
- },
187
- })
152
+ id: "deepseek",
153
+ name: "DeepSeek",
154
+ execute: async (input, { signal }) => {
155
+ const apiKey = await resolveApiKey(ctx, config.auth.source);
156
+ const results = await client.searchWeb(
157
+ input.query,
158
+ config.search,
159
+ requestContext(config, apiKey, signal),
160
+ );
161
+ return results.map(toOpenCodeResult);
162
+ },
163
+ });
164
+ if (registration.setDefault) editor.default.set("deepseek");
165
+ });
166
+ }
167
+
168
+ if (config.fetch.enabled) {
169
+ await ctx.tool.transform((editor) => {
170
+ if (registration.overrideWebfetch) editor.remove("webfetch");
171
+
172
+ if (registration.registerTool) {
173
+ editor.add({
174
+ name: "deepseek_fetch",
175
+ description: DEEPSEEK_FETCH_DESCRIPTION,
176
+ input: FETCH_INPUT,
177
+ options: { codemode: false, permission: "deepseek_fetch" },
178
+ execute: (input, context) =>
179
+ runFetch(ctx, config, input as FetchInput, context, "deepseek_fetch", false, client),
180
+ });
181
+ }
182
+
183
+ if (registration.overrideWebfetch) {
184
+ editor.add({
185
+ name: "webfetch",
186
+ description: WEBFETCH_DESCRIPTION,
187
+ input: FETCH_INPUT,
188
+ options: { codemode: false, permission: "webfetch" },
189
+ execute: (input, context) =>
190
+ runFetch(ctx, config, input as FetchInput, context, "webfetch", true, client),
191
+ });
192
+ }
193
+ });
194
+ }
195
+ },
196
+ });
197
+ }
198
+
199
+ export default createOpenCodePlugin();