deepseek-web-retrieval 0.5.1 → 0.7.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 (46) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/README.md +167 -45
  3. package/dist/cli.js +2303 -1836
  4. package/docs/adr/0002-shared-config-base-layer.md +43 -0
  5. package/docs/agents/codex.md +60 -32
  6. package/docs/agents/opencode-v2.md +69 -39
  7. package/docs/agents/pi.md +60 -31
  8. package/docs/design/config-layering-design.md +180 -0
  9. package/docs/design/installer-design.md +33 -20
  10. package/docs/integration.md +41 -7
  11. package/docs/research/control-matrix.md +17 -4
  12. package/docs/research/mcp-compatibility.md +9 -2
  13. package/docs/research/ori-question-sourcing.md +336 -0
  14. package/docs/research/result-quality-passthrough.md +159 -0
  15. package/docs/research/search-api-defaults.md +18 -2
  16. package/docs/research/search-inline-content.md +244 -0
  17. package/examples/opencode.global.jsonc +3 -2
  18. package/package.json +2 -1
  19. package/src/core/auth.ts +3 -0
  20. package/src/core/client.ts +26 -4
  21. package/src/core/config-file.ts +168 -0
  22. package/src/core/config-summary.ts +82 -0
  23. package/src/core/constants.ts +15 -1
  24. package/src/core/descriptions.ts +6 -2
  25. package/src/core/env.ts +99 -32
  26. package/src/core/format.ts +19 -1
  27. package/src/core/options.ts +22 -2
  28. package/src/core/search.ts +6 -0
  29. package/src/core/types.ts +11 -0
  30. package/src/installer/apply.ts +5 -3
  31. package/src/installer/cli.ts +92 -25
  32. package/src/installer/config-command.ts +116 -0
  33. package/src/installer/hosts/codex.ts +5 -3
  34. package/src/installer/hosts/opencode.ts +9 -5
  35. package/src/installer/hosts/pi.ts +10 -6
  36. package/src/installer/ownership.ts +54 -1
  37. package/src/installer/paths.ts +7 -1
  38. package/src/installer/state.ts +73 -19
  39. package/src/installer/types.ts +12 -0
  40. package/src/mcp/auth.ts +2 -3
  41. package/src/mcp/cli.ts +9 -51
  42. package/src/mcp/serve.ts +60 -0
  43. package/src/mcp/server.ts +7 -2
  44. package/src/opencode/plugin.ts +22 -2
  45. package/src/pi/extension.ts +78 -9
  46. package/src/version.ts +12 -0
package/src/mcp/server.ts CHANGED
@@ -8,6 +8,7 @@ import { ENABLE_FETCH_ENV, ENABLE_SEARCH_ENV } from "../core/constants";
8
8
  import {
9
9
  FETCH_TOOL_DESCRIPTION,
10
10
  FETCH_URL_PARAMETER,
11
+ SEARCH_ORI_QUESTION_PARAMETER,
11
12
  SEARCH_QUERY_PARAMETER,
12
13
  SEARCH_TOOL_DESCRIPTION,
13
14
  } from "../core/descriptions";
@@ -21,6 +22,7 @@ export const MCP_SERVER_NAME = "deepseek-web-retrieval";
21
22
 
22
23
  const SEARCH_INPUT = z.object({
23
24
  query: z.string().min(1).describe(SEARCH_QUERY_PARAMETER),
25
+ original_question: z.string().optional().describe(SEARCH_ORI_QUESTION_PARAMETER),
24
26
  });
25
27
 
26
28
  const FETCH_INPUT = z.object({
@@ -47,10 +49,13 @@ export function createDeepSeekServer(
47
49
  inputSchema: SEARCH_INPUT,
48
50
  annotations: { readOnlyHint: true },
49
51
  },
50
- async ({ query }, ctx) => {
52
+ async ({ query, original_question }, ctx) => {
51
53
  try {
52
54
  const request = requestContext(config, env, ctx.mcpReq.signal);
53
- const results = await client.searchWeb(query, config.search, request);
55
+ const results = await client.searchWeb(query, config.search, {
56
+ ...request,
57
+ oriQuestion: original_question,
58
+ });
54
59
  return callResult(server, ctx, formatSearchResults(results), {
55
60
  results: results.map(toStructuredResult),
56
61
  });
@@ -3,7 +3,11 @@ 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
5
  import { inspectFetchTarget, retrievalClient } from "../core/client";
6
+ import { sharedFileOptions } from "../core/config-file";
7
+ import { configDeviations } from "../core/config-summary";
6
8
  import { FETCH_MARKDOWN_ONLY, FETCH_OUTPUT_TRUNCATED } from "../core/descriptions";
9
+ import { pluginOptionsFromEnv } from "../core/env";
10
+ import { formatQualityMarker } from "../core/format";
7
11
  import { resolveFetchTimeoutMs, resolvePluginOptions } from "../core/options";
8
12
  import type { PluginOptions, ResolvedOptions, SearchResult } from "../core/types";
9
13
  import { resolveApiKey } from "./auth";
@@ -58,10 +62,14 @@ interface OpenCodeSearchResult {
58
62
 
59
63
  /** Maps the core's neutral SearchResult onto the shape OpenCode expects. */
60
64
  function toOpenCodeResult(result: SearchResult): OpenCodeSearchResult {
65
+ // OpenCode's result shape is closed and only `content` reaches the model,
66
+ // so the quality marker rides at the end of the content string.
67
+ const marker = formatQualityMarker(result);
68
+ const content = marker ? (result.content ? `${result.content} ${marker}` : marker) : result.content;
61
69
  return {
62
70
  url: result.url,
63
71
  ...(result.title ? { title: result.title } : {}),
64
- ...(result.content ? { content: result.content } : {}),
72
+ ...(content ? { content } : {}),
65
73
  ...(result.published !== undefined ? { time: { published: result.published } } : { time: {} }),
66
74
  };
67
75
  }
@@ -143,8 +151,20 @@ export function createOpenCodePlugin(client: RetrievalClient = retrievalClient)
143
151
  return Plugin.define({
144
152
  id: "deepseek-web-retrieval",
145
153
  async setup(ctx) {
146
- const config = resolvePluginOptions(ctx.options);
154
+ // Defaults < shared config file < DEEPSEEK_* environment < opencode.json
155
+ // options: the host layer keeps winning exactly as before — the change
156
+ // is that the file and the environment now configure this host too.
157
+ const config = resolvePluginOptions({
158
+ ...sharedFileOptions(),
159
+ ...pluginOptionsFromEnv(process.env),
160
+ ...ctx.options,
161
+ });
147
162
  const registration = resolveRegistration(ctx.options);
163
+ // Deviations only, printed once at setup: one stderr line so a
164
+ // multi-layer resolution is visible; nothing deviating means no line at
165
+ // all (same contract as the MCP server).
166
+ const deviations = configDeviations(config);
167
+ if (deviations) console.error(`deepseek-web-retrieval config: ${deviations}`);
148
168
 
149
169
  if (config.search.enabled) {
150
170
  await ctx.websearch.transform((editor) => {
@@ -1,14 +1,17 @@
1
1
  import { homedir } from "node:os";
2
2
  import { Type } from "@earendil-works/pi-ai";
3
- import type { AgentToolResult, ExtensionAPI } from "@earendil-works/pi-coding-agent";
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
6
  import type { RequestOptions, RetrievalClient } from "../core/client";
7
7
  import { inspectFetchTarget, retrievalClient } from "../core/client";
8
+ import { sharedFileOptions } from "../core/config-file";
9
+ import { configDeviations } from "../core/config-summary";
8
10
  import { API_KEY_ENV, ENABLE_FETCH_ENV, ENABLE_SEARCH_ENV } from "../core/constants";
9
11
  import {
10
12
  FETCH_TOOL_DESCRIPTION,
11
13
  FETCH_URL_PARAMETER,
14
+ SEARCH_ORI_QUESTION_PARAMETER,
12
15
  SEARCH_QUERY_PARAMETER,
13
16
  SEARCH_TOOL_DESCRIPTION,
14
17
  } from "../core/descriptions";
@@ -22,6 +25,11 @@ import { readExtensionSettings, SETTINGS_NAMESPACE } from "./settings";
22
25
 
23
26
  const SEARCH_PARAMETERS = Type.Object({
24
27
  query: Type.String({ minLength: 1, description: SEARCH_QUERY_PARAMETER }),
28
+ original_question: Type.Optional(
29
+ Type.String({
30
+ description: `${SEARCH_ORI_QUESTION_PARAMETER} When left blank, the most recent user message in the session is sent instead, truncated to fit.`,
31
+ }),
32
+ ),
25
33
  });
26
34
 
27
35
  const SEARCH_OUTPUT = Type.Object({
@@ -31,6 +39,9 @@ const SEARCH_OUTPUT = Type.Object({
31
39
  title: Type.Optional(Type.String()),
32
40
  content: Type.Optional(Type.String()),
33
41
  published: Type.Optional(Type.Number()),
42
+ rerankScore: Type.Optional(Type.Number()),
43
+ authority: Type.Optional(Type.Number()),
44
+ siteName: Type.Optional(Type.String()),
34
45
  }),
35
46
  ),
36
47
  });
@@ -50,14 +61,15 @@ const FETCH_OUTPUT = Type.Object({
50
61
  const NEVER_ABORTS = new AbortController().signal;
51
62
 
52
63
  /**
53
- * Resolves the extension's options: the shared DEEPSEEK_* environment
54
- * pipeline, with the settings namespace layered on top. Settings outrank the
55
- * environment because the installer wrote them as the explicit install-time
56
- * intent (the off tier lands there — the installer cannot write the shell
57
- * environment).
64
+ * Resolves the extension's options: defaults < shared config file < the shared
65
+ * DEEPSEEK_* environment pipeline < the settings namespace. The file's path
66
+ * follows the passed environment (XDG_CONFIG_HOME), so the function stays
67
+ * injectable. Settings outrank both lower layers because the installer wrote
68
+ * them as the explicit install-time intent (the off tier lands there — the
69
+ * installer cannot write the shell environment).
58
70
  */
59
71
  export function resolveExtensionOptions(env: Env = process.env, settings: ExtensionSettings = {}): ResolvedOptions {
60
- const options: Record<string, unknown> = { ...pluginOptionsFromEnv(env) };
72
+ const options: Record<string, unknown> = { ...sharedFileOptions({ env }), ...pluginOptionsFromEnv(env) };
61
73
  if (settings.enableSearch === false) options.searchEnabled = false;
62
74
  if (settings.enableFetch === false) options.fetchEnabled = false;
63
75
  return resolvePluginOptions(options);
@@ -83,9 +95,15 @@ export function searchTool(config: ResolvedOptions, client: RetrievalClient = re
83
95
  outputSchema: SEARCH_OUTPUT,
84
96
  annotations: { readOnlyHint: true, openWorldHint: true },
85
97
  exposure: "direct",
86
- async execute(_toolCallId, params, signal): Promise<AgentToolResult> {
98
+ async execute(_toolCallId, params, signal, _onUpdate, ctx): Promise<AgentToolResult> {
87
99
  try {
88
- const results = await client.searchWeb(params.query, config.search, requestContext(config, signal));
100
+ // The explicit parameter wins; only when the model left it blank does
101
+ // the session fallback get a say.
102
+ const explicit = params.original_question?.trim() ? params.original_question : undefined;
103
+ const results = await client.searchWeb(params.query, config.search, {
104
+ ...requestContext(config, signal),
105
+ oriQuestion: explicit ?? sessionOriQuestion(ctx),
106
+ });
89
107
  return {
90
108
  content: [{ type: "text", text: formatSearchResults(results) }],
91
109
  structuredContent: { results: results.map(toStructuredResult) },
@@ -141,6 +159,11 @@ export default function deepSeekWebRetrieval(pi: ExtensionAPI): void {
141
159
  const config = resolveExtensionOptions(process.env, readExtensionSettings(homedir(), process.cwd()));
142
160
  const precheck = extensionPrecheck(config);
143
161
  if (precheck) throw new Error(precheck);
162
+ // Deviations only, printed once at registration: one stderr line so a
163
+ // multi-layer resolution is visible where Pi surfaces extension output;
164
+ // nothing deviating means no line at all (same contract as the MCP server).
165
+ const deviations = configDeviations(config);
166
+ if (deviations) console.error(`deepseek-web-retrieval config: ${deviations}`);
144
167
  if (config.search.enabled) pi.registerTool(searchTool(config));
145
168
  if (config.fetch.enabled) pi.registerTool(fetchTool(config));
146
169
  }
@@ -149,6 +172,52 @@ function requestContext(config: ResolvedOptions, signal: AbortSignal | undefined
149
172
  return { signal: signal ?? NEVER_ABORTS, auth: { apiKey: requireApiKey() }, retry: config.retry };
150
173
  }
151
174
 
175
+ /**
176
+ * Best-effort `ori_question` fallback for when the model left the tool input
177
+ * blank: the most recent user message text in the session projection, readable
178
+ * synchronously at call time. Best-effort is the contract — Pi's runtime may
179
+ * call `execute` without a context (the fifth argument is typed non-optional
180
+ * but `wrapToolDefinition` passes `ctx ?? ctxFactory?.()`), and any surprise
181
+ * (missing manager, thrown error, odd shape) must degrade to "no fallback"
182
+ * rather than fail the search itself. Length handling stays in the core
183
+ * client, which trims and clamps to the API's 128-char budget.
184
+ */
185
+ function sessionOriQuestion(ctx: ExtensionToolContext | undefined): string | undefined {
186
+ try {
187
+ const messages = ctx?.sessionManager?.buildSessionProjection()?.messages;
188
+ if (!Array.isArray(messages)) return undefined;
189
+ for (let i = messages.length - 1; i >= 0; i--) {
190
+ const text = userMessageText(messages[i]);
191
+ if (text !== undefined) return text;
192
+ }
193
+ return undefined;
194
+ } catch {
195
+ return undefined;
196
+ }
197
+ }
198
+
199
+ /** The trimmed text of a user message's string or `text`-block content, if any. */
200
+ function userMessageText(message: unknown): string | undefined {
201
+ if (typeof message !== "object" || message === null) return undefined;
202
+ const { role, content } = message as { role?: unknown; content?: unknown };
203
+ if (role !== "user") return undefined;
204
+ const text =
205
+ typeof content === "string"
206
+ ? content
207
+ : Array.isArray(content)
208
+ ? content
209
+ .map((part) =>
210
+ typeof part === "object" && part !== null && (part as { type?: unknown }).type === "text"
211
+ ? (part as { text?: unknown }).text
212
+ : undefined,
213
+ )
214
+ .filter((part): part is string => typeof part === "string")
215
+ .join(" ")
216
+ : undefined;
217
+ const trimmed = text?.trim();
218
+ return trimmed ? trimmed : undefined;
219
+ }
220
+
152
221
  function requireApiKey(): string {
153
222
  const key = apiKeyFromEnv();
154
223
  if (key) return key;
package/src/version.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ /**
4
+ * The published package version, read once at startup. Resolves to the repo
5
+ * root when run from source and to the package root when run from the bundled
6
+ * dist/ output; npm always ships package.json alongside it.
7
+ */
8
+ export const TOOL_VERSION: string = (
9
+ JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as {
10
+ version: string;
11
+ }
12
+ ).version;