deepseek-web-retrieval 0.3.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.
- package/CHANGELOG.md +96 -230
- package/README.md +53 -25
- package/dist/cli.js +1358 -617
- package/docs/agents/codex.md +32 -11
- package/docs/agents/opencode-v2.md +5 -5
- package/docs/agents/pi.md +92 -90
- package/docs/design/installer-design.md +72 -9
- package/docs/hosts.jsonc +2 -2
- package/docs/integration.md +14 -9
- package/docs/research/control-matrix.md +48 -13
- package/docs/research/mcp-compatibility.md +9 -4
- package/docs/research/search-api-defaults.md +309 -0
- package/examples/pi.settings.jsonc +17 -0
- package/index.ts +2 -2
- package/package.json +35 -4
- package/src/core/auth.ts +4 -6
- package/src/core/client.ts +65 -53
- package/src/core/constants.ts +30 -24
- package/src/core/descriptions.ts +30 -6
- package/src/core/domains.ts +56 -67
- package/src/core/env.ts +112 -0
- package/src/core/format.ts +53 -50
- package/src/core/index.ts +9 -9
- package/src/core/jsonc.ts +6 -6
- package/src/core/object.ts +4 -0
- package/src/core/options.ts +21 -26
- package/src/core/search.ts +45 -44
- package/src/core/transport.ts +109 -113
- package/src/core/types.ts +47 -47
- package/src/core/url.ts +20 -20
- package/src/installer/apply.ts +256 -81
- package/src/installer/cli.ts +135 -104
- package/src/installer/detect.ts +46 -31
- package/src/installer/formats/index.ts +26 -28
- package/src/installer/formats/json.ts +81 -77
- package/src/installer/formats/rules.ts +55 -28
- package/src/installer/formats/toml.ts +88 -81
- package/src/installer/formats/value.ts +34 -30
- package/src/installer/hosts/codex.ts +100 -54
- package/src/installer/hosts/index.ts +126 -41
- package/src/installer/hosts/opencode.ts +70 -51
- package/src/installer/hosts/pi.ts +269 -75
- package/src/installer/hosts/shared.ts +19 -14
- package/src/installer/operation-lock.ts +54 -0
- package/src/installer/ownership.ts +129 -0
- package/src/installer/paths.ts +7 -5
- package/src/installer/persistence.ts +66 -0
- package/src/installer/render.ts +29 -29
- package/src/installer/revert.ts +33 -29
- package/src/installer/rulesContent.ts +24 -13
- package/src/installer/state.ts +116 -31
- package/src/installer/types.ts +74 -56
- package/src/installer/uninstall.ts +40 -17
- package/src/mcp/auth.ts +7 -8
- package/src/mcp/cli.ts +38 -20
- package/src/mcp/client.ts +35 -0
- package/src/mcp/server.ts +68 -97
- package/src/opencode/auth.ts +24 -26
- package/src/opencode/plugin.ts +99 -87
- package/src/pi/extension.ts +164 -0
- package/src/pi/settings.ts +49 -0
- package/src/mcp/config.ts +0 -124
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
3
|
+
import type { AgentToolResult, ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { apiKeyFromEnv } from "../core/auth";
|
|
6
|
+
import type { RequestOptions, RetrievalClient } from "../core/client";
|
|
7
|
+
import { inspectFetchTarget, retrievalClient } from "../core/client";
|
|
8
|
+
import { API_KEY_ENV, ENABLE_FETCH_ENV, ENABLE_SEARCH_ENV } from "../core/constants";
|
|
9
|
+
import {
|
|
10
|
+
FETCH_TOOL_DESCRIPTION,
|
|
11
|
+
FETCH_URL_PARAMETER,
|
|
12
|
+
SEARCH_QUERY_PARAMETER,
|
|
13
|
+
SEARCH_TOOL_DESCRIPTION,
|
|
14
|
+
} from "../core/descriptions";
|
|
15
|
+
import type { Env } from "../core/env";
|
|
16
|
+
import { pluginOptionsFromEnv } from "../core/env";
|
|
17
|
+
import { formatSearchResults, toStructuredResult } from "../core/format";
|
|
18
|
+
import { resolvePluginOptions } from "../core/options";
|
|
19
|
+
import type { ResolvedOptions } from "../core/types";
|
|
20
|
+
import type { ExtensionSettings } from "./settings";
|
|
21
|
+
import { readExtensionSettings, SETTINGS_NAMESPACE } from "./settings";
|
|
22
|
+
|
|
23
|
+
const SEARCH_PARAMETERS = Type.Object({
|
|
24
|
+
query: Type.String({ minLength: 1, description: SEARCH_QUERY_PARAMETER }),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const SEARCH_OUTPUT = Type.Object({
|
|
28
|
+
results: Type.Array(
|
|
29
|
+
Type.Object({
|
|
30
|
+
url: Type.String(),
|
|
31
|
+
title: Type.Optional(Type.String()),
|
|
32
|
+
content: Type.Optional(Type.String()),
|
|
33
|
+
published: Type.Optional(Type.Number()),
|
|
34
|
+
}),
|
|
35
|
+
),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const FETCH_PARAMETERS = Type.Object({
|
|
39
|
+
url: Type.String({ minLength: 1, description: FETCH_URL_PARAMETER }),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const FETCH_OUTPUT = Type.Object({
|
|
43
|
+
url: Type.String(),
|
|
44
|
+
title: Type.String(),
|
|
45
|
+
content: Type.String(),
|
|
46
|
+
truncated: Type.Boolean(),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/** Pi may execute a tool without an abort signal; the core requires one. */
|
|
50
|
+
const NEVER_ABORTS = new AbortController().signal;
|
|
51
|
+
|
|
52
|
+
/**
|
|
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).
|
|
58
|
+
*/
|
|
59
|
+
export function resolveExtensionOptions(env: Env = process.env, settings: ExtensionSettings = {}): ResolvedOptions {
|
|
60
|
+
const options: Record<string, unknown> = { ...pluginOptionsFromEnv(env) };
|
|
61
|
+
if (settings.enableSearch === false) options.searchEnabled = false;
|
|
62
|
+
if (settings.enableFetch === false) options.fetchEnabled = false;
|
|
63
|
+
return resolvePluginOptions(options);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Returns the both-disabled message, or undefined when a tool stays enabled. */
|
|
67
|
+
export function extensionPrecheck(config: ResolvedOptions): string | undefined {
|
|
68
|
+
if (config.search.enabled || config.fetch.enabled) return undefined;
|
|
69
|
+
return (
|
|
70
|
+
`Both tools are disabled (${ENABLE_SEARCH_ENV} and ${ENABLE_FETCH_ENV} are false, or the ` +
|
|
71
|
+
`${SETTINGS_NAMESPACE} settings namespace disabled them); at least one must stay enabled.`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The search tool as Pi registers it; exported for tests. */
|
|
76
|
+
export function searchTool(config: ResolvedOptions, client: RetrievalClient = retrievalClient) {
|
|
77
|
+
return defineTool({
|
|
78
|
+
name: "deepseek_web_search",
|
|
79
|
+
label: "DeepSeek Web Search",
|
|
80
|
+
description: SEARCH_TOOL_DESCRIPTION,
|
|
81
|
+
promptSnippet: "Search the web with DeepSeek and return ranked results with summaries.",
|
|
82
|
+
parameters: SEARCH_PARAMETERS,
|
|
83
|
+
outputSchema: SEARCH_OUTPUT,
|
|
84
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
85
|
+
exposure: "direct",
|
|
86
|
+
async execute(_toolCallId, params, signal): Promise<AgentToolResult> {
|
|
87
|
+
try {
|
|
88
|
+
const results = await client.searchWeb(params.query, config.search, requestContext(config, signal));
|
|
89
|
+
return {
|
|
90
|
+
content: [{ type: "text", text: formatSearchResults(results) }],
|
|
91
|
+
structuredContent: { results: results.map(toStructuredResult) },
|
|
92
|
+
details: undefined,
|
|
93
|
+
};
|
|
94
|
+
} catch (error) {
|
|
95
|
+
return toErrorResult(error);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The fetch tool as Pi registers it; exported for tests. */
|
|
102
|
+
export function fetchTool(config: ResolvedOptions, client: RetrievalClient = retrievalClient) {
|
|
103
|
+
return defineTool({
|
|
104
|
+
name: "deepseek_fetch",
|
|
105
|
+
label: "DeepSeek Fetch",
|
|
106
|
+
description: FETCH_TOOL_DESCRIPTION,
|
|
107
|
+
promptSnippet: "Fetch one web page as clean Markdown with DeepSeek.",
|
|
108
|
+
parameters: FETCH_PARAMETERS,
|
|
109
|
+
outputSchema: FETCH_OUTPUT,
|
|
110
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
111
|
+
exposure: "direct",
|
|
112
|
+
async execute(_toolCallId, params, signal): Promise<AgentToolResult> {
|
|
113
|
+
try {
|
|
114
|
+
const target = inspectFetchTarget(params.url, config.fetch);
|
|
115
|
+
const page = await client.fetchDocument(target, config.fetch, requestContext(config, signal));
|
|
116
|
+
return {
|
|
117
|
+
content: [{ type: "text", text: page.content }],
|
|
118
|
+
structuredContent: {
|
|
119
|
+
url: page.url,
|
|
120
|
+
title: page.title,
|
|
121
|
+
content: page.content,
|
|
122
|
+
truncated: page.truncated,
|
|
123
|
+
},
|
|
124
|
+
details: undefined,
|
|
125
|
+
};
|
|
126
|
+
} catch (error) {
|
|
127
|
+
return toErrorResult(error);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The Pi extension entry. Pi loads this file through the package's `pi`
|
|
135
|
+
* manifest and calls the default export inside its own process: the tools run
|
|
136
|
+
* without MCP's 20 KB result truncation and 60 s request timeout, exposure is
|
|
137
|
+
* `direct` by Pi's own default (no codemode trap), and the key is read
|
|
138
|
+
* straight from the Pi process environment.
|
|
139
|
+
*/
|
|
140
|
+
export default function deepSeekWebRetrieval(pi: ExtensionAPI): void {
|
|
141
|
+
const config = resolveExtensionOptions(process.env, readExtensionSettings(homedir(), process.cwd()));
|
|
142
|
+
const precheck = extensionPrecheck(config);
|
|
143
|
+
if (precheck) throw new Error(precheck);
|
|
144
|
+
if (config.search.enabled) pi.registerTool(searchTool(config));
|
|
145
|
+
if (config.fetch.enabled) pi.registerTool(fetchTool(config));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function requestContext(config: ResolvedOptions, signal: AbortSignal | undefined): RequestOptions {
|
|
149
|
+
return { signal: signal ?? NEVER_ABORTS, auth: { apiKey: requireApiKey() }, retry: config.retry };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function requireApiKey(): string {
|
|
153
|
+
const key = apiKeyFromEnv();
|
|
154
|
+
if (key) return key;
|
|
155
|
+
throw new Error(
|
|
156
|
+
`${API_KEY_ENV} is not set. The DeepSeek extension reads it from the environment of the Pi ` +
|
|
157
|
+
"process; set it in the shell that launches Pi.",
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function toErrorResult(error: unknown): AgentToolResult {
|
|
162
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
163
|
+
return { content: [{ type: "text", text: message }], details: undefined, isError: true };
|
|
164
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { isPlainObject } from "../core/object";
|
|
4
|
+
|
|
5
|
+
/** The settings.json namespace the installer writes capability switches into. */
|
|
6
|
+
export const SETTINGS_NAMESPACE = "deepseekWebRetrieval";
|
|
7
|
+
|
|
8
|
+
/** The namespace shape. Only an explicit false is meaningful, mirroring the env switches. */
|
|
9
|
+
export interface ExtensionSettings {
|
|
10
|
+
readonly enableSearch?: boolean;
|
|
11
|
+
readonly enableFetch?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Reads the namespace from Pi's settings files, project over global (Pi's own
|
|
16
|
+
* merge order — a project key overrides a global one, so a project can
|
|
17
|
+
* re-enable against a global off). Pi 1.3.0 tolerates unknown keys in
|
|
18
|
+
* settings.json — reads ignore them and Pi's own rewrites preserve them
|
|
19
|
+
* (verified against 1.3.0: `pi install` rewriting the file kept the namespace
|
|
20
|
+
* intact), so the installer can sit its keys next to Pi's own. Missing or
|
|
21
|
+
* unparseable files contribute nothing.
|
|
22
|
+
*/
|
|
23
|
+
export function readExtensionSettings(home: string, cwd: string): ExtensionSettings {
|
|
24
|
+
const merged: Record<string, unknown> = {
|
|
25
|
+
...readNamespace(join(home, ".pi", "agent", "settings.json")),
|
|
26
|
+
...readNamespace(join(cwd, ".pi", "settings.json")),
|
|
27
|
+
};
|
|
28
|
+
return {
|
|
29
|
+
...(merged.enableSearch === false ? { enableSearch: false } : {}),
|
|
30
|
+
...(merged.enableFetch === false ? { enableFetch: false } : {}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readNamespace(file: string): Record<string, unknown> {
|
|
35
|
+
let text: string;
|
|
36
|
+
try {
|
|
37
|
+
text = readFileSync(file, "utf8");
|
|
38
|
+
} catch {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(text) as unknown;
|
|
43
|
+
if (!isPlainObject(parsed)) return {};
|
|
44
|
+
const namespace = parsed[SETTINGS_NAMESPACE];
|
|
45
|
+
return isPlainObject(namespace) ? namespace : {};
|
|
46
|
+
} catch {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/mcp/config.ts
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { ENABLE_FETCH_ENV, ENABLE_SEARCH_ENV } from "../core/constants"
|
|
2
|
-
import { resolvePluginOptions } from "../core/options"
|
|
3
|
-
import type { PluginOptions, ResolvedOptions } from "../core/types"
|
|
4
|
-
|
|
5
|
-
/** The environment an MCP server reads its key and options from. */
|
|
6
|
-
export type Env = Readonly<Record<string, string | undefined>>
|
|
7
|
-
|
|
8
|
-
/** Each supported option mapped onto its DEEPSEEK_* variable, grouped by value type. */
|
|
9
|
-
const NUMBER_ENV = {
|
|
10
|
-
count: "DEEPSEEK_SEARCH_COUNT",
|
|
11
|
-
maxResults: "DEEPSEEK_SEARCH_MAX_RESULTS",
|
|
12
|
-
minRerank: "DEEPSEEK_SEARCH_MIN_RERANK",
|
|
13
|
-
minAuthority: "DEEPSEEK_SEARCH_MIN_AUTHORITY",
|
|
14
|
-
maxSummaryChars: "DEEPSEEK_SEARCH_MAX_SUMMARY_CHARS",
|
|
15
|
-
searchTimeoutMs: "DEEPSEEK_SEARCH_TIMEOUT_MS",
|
|
16
|
-
fetchMaxChars: "DEEPSEEK_FETCH_MAX_CHARS",
|
|
17
|
-
fetchTimeoutMs: "DEEPSEEK_FETCH_TIMEOUT_MS",
|
|
18
|
-
maxRetries: "DEEPSEEK_MAX_RETRIES",
|
|
19
|
-
retryBaseDelayMs: "DEEPSEEK_RETRY_BASE_DELAY_MS",
|
|
20
|
-
} as const
|
|
21
|
-
|
|
22
|
-
const TEXT_ENV = {
|
|
23
|
-
language: "DEEPSEEK_SEARCH_LANGUAGE",
|
|
24
|
-
} as const
|
|
25
|
-
|
|
26
|
-
const DOMAINS_ENV = {
|
|
27
|
-
blockedDomains: "DEEPSEEK_SEARCH_BLOCKED_DOMAINS",
|
|
28
|
-
includeDomains: "DEEPSEEK_SEARCH_INCLUDE_DOMAINS",
|
|
29
|
-
fetchAllowedDomains: "DEEPSEEK_FETCH_ALLOWED_DOMAINS",
|
|
30
|
-
fetchBlockedDomains: "DEEPSEEK_FETCH_BLOCKED_DOMAINS",
|
|
31
|
-
} as const
|
|
32
|
-
|
|
33
|
-
const FLAG_ENV = {
|
|
34
|
-
allowUnknownAuthority: "DEEPSEEK_SEARCH_ALLOW_UNKNOWN_AUTHORITY",
|
|
35
|
-
} as const
|
|
36
|
-
|
|
37
|
-
/** Shared defaults are true; only an explicit false value disables them. */
|
|
38
|
-
const FALSE_ONLY_ENV = {
|
|
39
|
-
fetchBlockPrivateHosts: "DEEPSEEK_FETCH_BLOCK_PRIVATE_HOSTS",
|
|
40
|
-
searchEnabled: ENABLE_SEARCH_ENV,
|
|
41
|
-
fetchEnabled: ENABLE_FETCH_ENV,
|
|
42
|
-
} as const
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Maps `DEEPSEEK_*` environment variables onto the shared plugin option keys and
|
|
46
|
-
* resolves them through the same pipeline as the OpenCode adapter, so defaults
|
|
47
|
-
* and clamping stay identical across hosts. Invalid values are dropped and the
|
|
48
|
-
* shared default applies.
|
|
49
|
-
*/
|
|
50
|
-
export function resolveEnvOptions(env: Env = process.env): ResolvedOptions {
|
|
51
|
-
return resolvePluginOptions(pluginOptionsFromEnv(env))
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function pluginOptionsFromEnv(env: Env): PluginOptions {
|
|
55
|
-
const options: Record<string, unknown> = {}
|
|
56
|
-
|
|
57
|
-
putNumber(options, "count", env)
|
|
58
|
-
putNumber(options, "maxResults", env)
|
|
59
|
-
putNumber(options, "minRerank", env)
|
|
60
|
-
putNumber(options, "minAuthority", env)
|
|
61
|
-
putBoolean(options, "allowUnknownAuthority", env)
|
|
62
|
-
putDomains(options, "blockedDomains", env)
|
|
63
|
-
putDomains(options, "includeDomains", env)
|
|
64
|
-
putText(options, "language", env)
|
|
65
|
-
putNumber(options, "maxSummaryChars", env)
|
|
66
|
-
putNumber(options, "searchTimeoutMs", env)
|
|
67
|
-
|
|
68
|
-
putNumber(options, "fetchMaxChars", env)
|
|
69
|
-
putDomains(options, "fetchAllowedDomains", env)
|
|
70
|
-
putDomains(options, "fetchBlockedDomains", env)
|
|
71
|
-
putNumber(options, "fetchTimeoutMs", env)
|
|
72
|
-
putFalseOnly(options, "fetchBlockPrivateHosts", env)
|
|
73
|
-
putFalseOnly(options, "searchEnabled", env)
|
|
74
|
-
putFalseOnly(options, "fetchEnabled", env)
|
|
75
|
-
|
|
76
|
-
putNumber(options, "maxRetries", env)
|
|
77
|
-
putNumber(options, "retryBaseDelayMs", env)
|
|
78
|
-
|
|
79
|
-
return options
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function putNumber(target: Record<string, unknown>, key: keyof typeof NUMBER_ENV, env: Env): void {
|
|
83
|
-
const value = env[NUMBER_ENV[key]]
|
|
84
|
-
if (value === undefined) return
|
|
85
|
-
const trimmed = value.trim()
|
|
86
|
-
if (!trimmed) return
|
|
87
|
-
const parsed = Number(trimmed)
|
|
88
|
-
if (Number.isFinite(parsed)) target[key] = parsed
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function putText(target: Record<string, unknown>, key: keyof typeof TEXT_ENV, env: Env): void {
|
|
92
|
-
const value = env[TEXT_ENV[key]]
|
|
93
|
-
if (value === undefined) return
|
|
94
|
-
const trimmed = value.trim()
|
|
95
|
-
if (trimmed) target[key] = trimmed
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function putBoolean(target: Record<string, unknown>, key: keyof typeof FLAG_ENV, env: Env): void {
|
|
99
|
-
if (isTrueValue(env[FLAG_ENV[key]])) target[key] = true
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function putFalseOnly(target: Record<string, unknown>, key: keyof typeof FALSE_ONLY_ENV, env: Env): void {
|
|
103
|
-
if (isFalseValue(env[FALSE_ONLY_ENV[key]])) target[key] = false
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function putDomains(target: Record<string, unknown>, key: keyof typeof DOMAINS_ENV, env: Env): void {
|
|
107
|
-
const value = env[DOMAINS_ENV[key]]
|
|
108
|
-
if (value === undefined || !value.trim()) return
|
|
109
|
-
const domains = value
|
|
110
|
-
.split(",")
|
|
111
|
-
.map((domain) => domain.trim())
|
|
112
|
-
.filter((domain) => domain.length > 0)
|
|
113
|
-
if (domains.length > 0) target[key] = domains
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function isTrueValue(value: string | undefined): boolean {
|
|
117
|
-
const trimmed = value?.trim().toLowerCase()
|
|
118
|
-
return trimmed === "true" || trimmed === "1" || trimmed === "yes" || trimmed === "on"
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function isFalseValue(value: string | undefined): boolean {
|
|
122
|
-
const trimmed = value?.trim().toLowerCase()
|
|
123
|
-
return trimmed === "false" || trimmed === "0" || trimmed === "no" || trimmed === "off"
|
|
124
|
-
}
|