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.
Files changed (62) hide show
  1. package/CHANGELOG.md +96 -230
  2. package/README.md +53 -25
  3. package/dist/cli.js +1358 -617
  4. package/docs/agents/codex.md +32 -11
  5. package/docs/agents/opencode-v2.md +5 -5
  6. package/docs/agents/pi.md +92 -90
  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/docs/research/search-api-defaults.md +309 -0
  13. package/examples/pi.settings.jsonc +17 -0
  14. package/index.ts +2 -2
  15. package/package.json +35 -4
  16. package/src/core/auth.ts +4 -6
  17. package/src/core/client.ts +65 -53
  18. package/src/core/constants.ts +30 -24
  19. package/src/core/descriptions.ts +30 -6
  20. package/src/core/domains.ts +56 -67
  21. package/src/core/env.ts +112 -0
  22. package/src/core/format.ts +53 -50
  23. package/src/core/index.ts +9 -9
  24. package/src/core/jsonc.ts +6 -6
  25. package/src/core/object.ts +4 -0
  26. package/src/core/options.ts +21 -26
  27. package/src/core/search.ts +45 -44
  28. package/src/core/transport.ts +109 -113
  29. package/src/core/types.ts +47 -47
  30. package/src/core/url.ts +20 -20
  31. package/src/installer/apply.ts +256 -81
  32. package/src/installer/cli.ts +135 -104
  33. package/src/installer/detect.ts +46 -31
  34. package/src/installer/formats/index.ts +26 -28
  35. package/src/installer/formats/json.ts +81 -77
  36. package/src/installer/formats/rules.ts +55 -28
  37. package/src/installer/formats/toml.ts +88 -81
  38. package/src/installer/formats/value.ts +34 -30
  39. package/src/installer/hosts/codex.ts +100 -54
  40. package/src/installer/hosts/index.ts +126 -41
  41. package/src/installer/hosts/opencode.ts +70 -51
  42. package/src/installer/hosts/pi.ts +269 -75
  43. package/src/installer/hosts/shared.ts +19 -14
  44. package/src/installer/operation-lock.ts +54 -0
  45. package/src/installer/ownership.ts +129 -0
  46. package/src/installer/paths.ts +7 -5
  47. package/src/installer/persistence.ts +66 -0
  48. package/src/installer/render.ts +29 -29
  49. package/src/installer/revert.ts +33 -29
  50. package/src/installer/rulesContent.ts +24 -13
  51. package/src/installer/state.ts +116 -31
  52. package/src/installer/types.ts +74 -56
  53. package/src/installer/uninstall.ts +40 -17
  54. package/src/mcp/auth.ts +7 -8
  55. package/src/mcp/cli.ts +38 -20
  56. package/src/mcp/client.ts +35 -0
  57. package/src/mcp/server.ts +68 -97
  58. package/src/opencode/auth.ts +24 -26
  59. package/src/opencode/plugin.ts +99 -87
  60. package/src/pi/extension.ts +164 -0
  61. package/src/pi/settings.ts +49 -0
  62. 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
- }