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.
- package/CHANGELOG.md +21 -1
- package/README.md +167 -45
- package/dist/cli.js +2303 -1836
- package/docs/adr/0002-shared-config-base-layer.md +43 -0
- package/docs/agents/codex.md +60 -32
- package/docs/agents/opencode-v2.md +69 -39
- package/docs/agents/pi.md +60 -31
- package/docs/design/config-layering-design.md +180 -0
- package/docs/design/installer-design.md +33 -20
- package/docs/integration.md +41 -7
- package/docs/research/control-matrix.md +17 -4
- package/docs/research/mcp-compatibility.md +9 -2
- package/docs/research/ori-question-sourcing.md +336 -0
- package/docs/research/result-quality-passthrough.md +159 -0
- package/docs/research/search-api-defaults.md +18 -2
- package/docs/research/search-inline-content.md +244 -0
- package/examples/opencode.global.jsonc +3 -2
- package/package.json +2 -1
- package/src/core/auth.ts +3 -0
- package/src/core/client.ts +26 -4
- package/src/core/config-file.ts +168 -0
- package/src/core/config-summary.ts +82 -0
- package/src/core/constants.ts +15 -1
- package/src/core/descriptions.ts +6 -2
- package/src/core/env.ts +99 -32
- package/src/core/format.ts +19 -1
- package/src/core/options.ts +22 -2
- package/src/core/search.ts +6 -0
- package/src/core/types.ts +11 -0
- package/src/installer/apply.ts +5 -3
- package/src/installer/cli.ts +92 -25
- package/src/installer/config-command.ts +116 -0
- package/src/installer/hosts/codex.ts +5 -3
- package/src/installer/hosts/opencode.ts +9 -5
- package/src/installer/hosts/pi.ts +10 -6
- package/src/installer/ownership.ts +54 -1
- package/src/installer/paths.ts +7 -1
- package/src/installer/state.ts +73 -19
- package/src/installer/types.ts +12 -0
- package/src/mcp/auth.ts +2 -3
- package/src/mcp/cli.ts +9 -51
- package/src/mcp/serve.ts +60 -0
- package/src/mcp/server.ts +7 -2
- package/src/opencode/plugin.ts +22 -2
- package/src/pi/extension.ts +78 -9
- 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,
|
|
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
|
});
|
package/src/opencode/plugin.ts
CHANGED
|
@@ -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
|
-
...(
|
|
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
|
-
|
|
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) => {
|
package/src/pi/extension.ts
CHANGED
|
@@ -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:
|
|
54
|
-
* pipeline
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
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
|
-
|
|
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;
|