deepseek-web-retrieval 0.2.0 → 0.4.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 (54) hide show
  1. package/CHANGELOG.md +204 -1
  2. package/README.md +55 -28
  3. package/dist/cli.js +4224 -184
  4. package/docs/adr/0001-refuse-drift-between-plan-and-apply.md +29 -0
  5. package/docs/agents/codex.md +193 -0
  6. package/docs/agents/domain.md +51 -0
  7. package/docs/agents/issue-tracker.md +45 -0
  8. package/docs/{opencode-v2.md → agents/opencode-v2.md} +34 -11
  9. package/docs/{pi.md → agents/pi.md} +39 -11
  10. package/docs/agents/triage-labels.md +15 -0
  11. package/docs/design/installer-design.md +184 -0
  12. package/docs/hosts.jsonc +59 -33
  13. package/docs/integration.md +45 -52
  14. package/docs/research/control-matrix.md +257 -0
  15. package/docs/{mcp-compatibility.md → research/mcp-compatibility.md} +39 -108
  16. package/docs/research/search-api-defaults.md +309 -0
  17. package/package.json +5 -3
  18. package/src/core/auth.ts +1 -1
  19. package/src/core/client.ts +42 -249
  20. package/src/core/constants.ts +14 -3
  21. package/src/core/descriptions.ts +19 -0
  22. package/src/core/format.ts +31 -1
  23. package/src/core/jsonc.ts +16 -0
  24. package/src/core/options.ts +3 -4
  25. package/src/core/search.ts +5 -7
  26. package/src/core/transport.ts +253 -0
  27. package/src/core/types.ts +13 -11
  28. package/src/installer/apply.ts +176 -0
  29. package/src/installer/cli.ts +215 -0
  30. package/src/installer/detect.ts +85 -0
  31. package/src/installer/formats/index.ts +58 -0
  32. package/src/installer/formats/json.ts +151 -0
  33. package/src/installer/formats/rules.ts +62 -0
  34. package/src/installer/formats/toml.ts +163 -0
  35. package/src/installer/formats/value.ts +57 -0
  36. package/src/installer/hosts/codex.ts +128 -0
  37. package/src/installer/hosts/index.ts +134 -0
  38. package/src/installer/hosts/opencode.ts +118 -0
  39. package/src/installer/hosts/pi.ts +115 -0
  40. package/src/installer/hosts/shared.ts +31 -0
  41. package/src/installer/paths.ts +18 -0
  42. package/src/installer/render.ts +62 -0
  43. package/src/installer/revert.ts +61 -0
  44. package/src/installer/rulesContent.ts +32 -0
  45. package/src/installer/state.ts +63 -0
  46. package/src/installer/types.ts +126 -0
  47. package/src/installer/uninstall.ts +33 -0
  48. package/src/mcp/auth.ts +2 -1
  49. package/src/mcp/cli.ts +21 -5
  50. package/src/mcp/config.ts +63 -32
  51. package/src/mcp/server.ts +81 -65
  52. package/src/opencode/auth.ts +3 -2
  53. package/src/opencode/plugin.ts +93 -56
  54. package/src/mcp/format.ts +0 -31
@@ -1,12 +1,10 @@
1
1
  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
- import { assertFetchHostPolicy } from "../core/domains"
5
- import { formatFetchedContent } from "../core/format"
4
+ import { fetchDocument, inspectFetchTarget, searchWeb } from "../core/client"
5
+ import { FETCH_MARKDOWN_ONLY, FETCH_OUTPUT_TRUNCATED } from "../core/descriptions"
6
6
  import { resolveFetchTimeoutMs, resolvePluginOptions } from "../core/options"
7
- import { fetchPage, searchWeb } from "../core/client"
8
- import type { ResolvedOptions } from "../core/types"
9
- import { parseFetchUrl } from "../core/url"
7
+ import type { PluginOptions, ResolvedOptions, SearchResult } from "../core/types"
10
8
  import { resolveApiKey } from "./auth"
11
9
 
12
10
  type FetchInput = {
@@ -41,10 +39,53 @@ const FETCH_INPUT = {
41
39
  } as const
42
40
 
43
41
  const DEEPSEEK_FETCH_DESCRIPTION =
44
- 'Fetch one web page as clean Markdown through DeepSeek\'s Fetch API. This separate tool remains available when the built-in webfetch is replaced. Only format "markdown" is supported. Output is truncated to the configured size.'
42
+ "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}`
45
44
 
46
45
  const WEBFETCH_DESCRIPTION =
47
- 'Fetch content from an HTTP or HTTPS URL and return it as Markdown through DeepSeek\'s Fetch API. This is the built-in webfetch replacement enabled by fetchOverrideWebfetch. DeepSeek does not render JavaScript and some sites return 403/404. Only format "markdown" is supported. Output is truncated to the configured size.'
46
+ "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}`
49
+
50
+ /** OpenCode's WebSearch.Result shape, which wraps the published date in `time`. */
51
+ interface OpenCodeSearchResult {
52
+ readonly url: string
53
+ readonly title?: string
54
+ readonly content?: string
55
+ readonly time: { readonly published?: number }
56
+ }
57
+
58
+ /** Maps the core's neutral SearchResult onto the shape OpenCode expects. */
59
+ function toOpenCodeResult(result: SearchResult): OpenCodeSearchResult {
60
+ return {
61
+ url: result.url,
62
+ ...(result.title ? { title: result.title } : {}),
63
+ ...(result.content ? { content: result.content } : {}),
64
+ ...(result.published !== undefined
65
+ ? { time: { published: result.published } }
66
+ : { time: {} }),
67
+ }
68
+ }
69
+
70
+ /** How the plugin registers itself with OpenCode; resolved where it is consumed. */
71
+ export interface Registration {
72
+ readonly setDefault: boolean
73
+ readonly overrideWebfetch: boolean
74
+ readonly registerTool: boolean
75
+ }
76
+
77
+ export function resolveRegistration(input?: PluginOptions): Registration {
78
+ return {
79
+ setDefault: input?.setDefaultWebsearch === true,
80
+ overrideWebfetch: input?.fetchOverrideWebfetch === true,
81
+ registerTool: input?.fetchRegisterTool !== false,
82
+ }
83
+ }
84
+
85
+ /** One place builds the per-request context the core expects. */
86
+ function requestContext(config: ResolvedOptions, apiKey: string, signal: AbortSignal, timeoutMs?: number) {
87
+ return { signal, auth: { apiKey }, retry: config.retry, ...(timeoutMs === undefined ? {} : { timeoutMs }) }
88
+ }
48
89
 
49
90
  async function runFetch(
50
91
  ctx: Context,
@@ -64,10 +105,9 @@ async function runFetch(
64
105
  if (typeof input.url !== "string") {
65
106
  throw new Error('"url" must be a string')
66
107
  }
67
- const target = parseFetchUrl(input.url)
68
- // Fail before the permission prompt; fetchPage enforces this again for core
69
- // consumers that skip the pre-check.
70
- assertFetchHostPolicy(target, config.fetch)
108
+ // Inspect before the permission prompt so an invalid or blocked target fails
109
+ // without asking the user; fetchPage enforces the policy again.
110
+ const target = inspectFetchTarget(input.url, config.fetch)
71
111
  const timeoutMs = resolveFetchTimeoutMs(config.fetch.timeoutMs, input.timeout)
72
112
 
73
113
  // Resolved before the permission prompt so a missing key fails fast instead
@@ -84,18 +124,13 @@ async function runFetch(
84
124
  source: { type: "tool", messageID: context.messageID, id: context.id },
85
125
  })
86
126
 
87
- const page = await fetchPage(
88
- target,
89
- { ...config.fetch, timeoutMs },
90
- { signal: context.signal, auth: { apiKey }, retry: config.retry },
91
- )
92
- const formatted = formatFetchedContent(page.title, page.content, config.fetch.maxChars)
127
+ const page = await fetchDocument(target, config.fetch, requestContext(config, apiKey, context.signal, timeoutMs))
93
128
  return {
94
- content: formatted.content,
129
+ content: page.content,
95
130
  metadata: {
96
131
  url: page.url,
97
132
  title: page.title,
98
- truncated: formatted.truncated,
133
+ truncated: page.truncated,
99
134
  },
100
135
  }
101
136
  }
@@ -104,47 +139,49 @@ export default Plugin.define({
104
139
  id: "deepseek-web-retrieval",
105
140
  async setup(ctx) {
106
141
  const config = resolvePluginOptions(ctx.options)
142
+ const registration = resolveRegistration(ctx.options)
107
143
 
108
- await ctx.websearch.transform((editor) => {
109
- editor.add({
110
- id: "deepseek",
111
- name: "DeepSeek",
112
- execute: async (input, { signal }) => {
113
- const apiKey = await resolveApiKey(ctx, config.auth.source)
114
- return searchWeb(input.query, config.search, {
115
- signal,
116
- auth: { apiKey },
117
- retry: config.retry,
118
- })
119
- },
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")
120
156
  })
121
- if (config.search.setDefault) editor.default.set("deepseek")
122
- })
157
+ }
123
158
 
124
- await ctx.tool.transform((editor) => {
125
- if (config.fetch.overrideWebfetch) editor.remove("webfetch")
159
+ if (config.fetch.enabled) {
160
+ await ctx.tool.transform((editor) => {
161
+ if (registration.overrideWebfetch) editor.remove("webfetch")
126
162
 
127
- if (config.fetch.registerTool) {
128
- editor.add({
129
- name: "deepseek_fetch",
130
- description: DEEPSEEK_FETCH_DESCRIPTION,
131
- input: FETCH_INPUT,
132
- options: { codemode: false, permission: "deepseek_fetch" },
133
- execute: (input, context) =>
134
- runFetch(ctx, config, input as FetchInput, context, "deepseek_fetch", false),
135
- })
136
- }
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
+ }
137
173
 
138
- if (config.fetch.overrideWebfetch) {
139
- editor.add({
140
- name: "webfetch",
141
- description: WEBFETCH_DESCRIPTION,
142
- input: FETCH_INPUT,
143
- options: { codemode: false, permission: "webfetch" },
144
- execute: (input, context) =>
145
- runFetch(ctx, config, input as FetchInput, context, "webfetch", true),
146
- })
147
- }
148
- })
174
+ if (registration.overrideWebfetch) {
175
+ 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
+ }
149
186
  },
150
187
  })
package/src/mcp/format.ts DELETED
@@ -1,31 +0,0 @@
1
- import type { WebSearchResult } from "../core/types"
2
-
3
- /**
4
- * Renders processed search results as Markdown for the text content block.
5
- * Hosts that ignore `structuredContent` (Gemini CLI, Continue) read this text.
6
- */
7
- export function formatSearchResults(results: readonly WebSearchResult[]): string {
8
- if (results.length === 0) return "No results found."
9
-
10
- return results
11
- .map((result, index) => {
12
- const title = result.title?.trim() || result.url
13
- const lines = [`${index + 1}. [${escapeBrackets(title)}](${result.url})`]
14
- if (result.content) {
15
- lines.push(` ${result.content.replaceAll("\n", " ")}`)
16
- }
17
- if (result.time.published !== undefined) {
18
- lines.push(` Published: ${formatPublished(result.time.published)}`)
19
- }
20
- return lines.join("\n")
21
- })
22
- .join("\n\n")
23
- }
24
-
25
- function escapeBrackets(value: string): string {
26
- return value.replaceAll("[", "\\[").replaceAll("]", "\\]")
27
- }
28
-
29
- function formatPublished(epochMs: number): string {
30
- return new Date(epochMs).toISOString().slice(0, 10)
31
- }