deepseek-web-retrieval 0.6.0 → 0.8.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 +23 -1
- package/README.md +113 -53
- package/dist/cli.js +2427 -1813
- package/docs/adr/0002-shared-config-base-layer.md +43 -0
- package/docs/agents/claude-code.md +273 -0
- package/docs/agents/codex.md +50 -27
- package/docs/agents/opencode-v2.md +59 -33
- package/docs/agents/pi.md +50 -27
- package/docs/configuration.md +109 -0
- package/docs/design/config-layering-design.md +180 -0
- package/docs/design/installer-design.md +24 -13
- package/docs/hosts.jsonc +5 -1
- package/docs/integration.md +15 -9
- package/docs/research/control-matrix.md +49 -11
- package/docs/research/mcp-compatibility.md +6 -3
- 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 +102 -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/cli.ts +50 -16
- package/src/installer/config-command.ts +116 -0
- package/src/installer/hosts/claude-code.ts +325 -0
- package/src/installer/hosts/codex.ts +9 -4
- package/src/installer/hosts/index.ts +3 -0
- package/src/installer/hosts/opencode.ts +9 -5
- package/src/installer/hosts/pi.ts +34 -9
- package/src/installer/ownership.ts +54 -1
- package/src/installer/paths.ts +7 -1
- package/src/installer/state.ts +73 -20
- package/src/installer/types.ts +11 -1
- package/src/installer/uninstall.ts +4 -1
- package/src/mcp/auth.ts +2 -3
- package/src/mcp/cli.ts +4 -3
- package/src/mcp/serve.ts +10 -1
- package/src/mcp/server.ts +7 -2
- package/src/opencode/plugin.ts +22 -2
- package/src/pi/extension.ts +78 -9
package/dist/cli.js
CHANGED
|
@@ -15,11 +15,351 @@ var __export = (target, all) => {
|
|
|
15
15
|
};
|
|
16
16
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
17
|
|
|
18
|
-
// src/
|
|
18
|
+
// src/core/constants.ts
|
|
19
|
+
var SEARCH_ENDPOINT = "https://api.deepseek.com/v1/tools/search", FETCH_ENDPOINT = "https://api.deepseek.com/v1/tools/fetch", DEFAULT_SEARCH_COUNT = 10, DEFAULT_MAX_RESULTS = 10, DEFAULT_MIN_RERANK = 0, DEFAULT_MIN_AUTHORITY = 0, DEFAULT_MAX_SUMMARY_CHARS = 1200, DEFAULT_FETCH_MAX_CHARS = 16000, DEFAULT_TIMEOUT_MS = 30000, MIN_TIMEOUT_MS = 1000, MAX_TIMEOUT_MS = 120000, MAX_QUERY_CHARS = 128, MAX_ORI_QUESTION_CHARS = 128, MAX_URL_CHARS = 8192, MAX_ERROR_BODY_CHARS = 2000, MAX_RESPONSE_BYTES, FETCH_TRUNCATION_MARKER = `
|
|
20
|
+
|
|
21
|
+
[content truncated]`, DEFAULT_API_KEY_SOURCE = "auto", DEFAULT_MAX_RETRIES = 2, MAX_RETRIES = 5, DEFAULT_RETRY_BASE_DELAY_MS = 250, MAX_RETRY_DELAY_MS = 1e4, API_KEY_ENV = "DEEPSEEK_API_KEY", ENABLE_SEARCH_ENV = "DEEPSEEK_ENABLE_SEARCH", ENABLE_FETCH_ENV = "DEEPSEEK_ENABLE_FETCH", SEARCH_LOCALIZATION_ENV = "DEEPSEEK_SEARCH_LOCALIZATION";
|
|
22
|
+
var init_constants = __esm(() => {
|
|
23
|
+
MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// src/core/domains.ts
|
|
27
|
+
import { isIP } from "node:net";
|
|
28
|
+
function normalizeDomain(value) {
|
|
29
|
+
const trimmed = value.trim();
|
|
30
|
+
if (!trimmed || /\s/.test(trimmed))
|
|
31
|
+
return;
|
|
32
|
+
const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed);
|
|
33
|
+
if (hasScheme && !/^https?:\/\//i.test(trimmed))
|
|
34
|
+
return;
|
|
35
|
+
let hostname;
|
|
36
|
+
try {
|
|
37
|
+
hostname = new URL(hasScheme ? trimmed : `http://${trimmed}`).hostname;
|
|
38
|
+
} catch {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
return normalizeHostname(hostname);
|
|
42
|
+
}
|
|
43
|
+
function normalizeHostname(value) {
|
|
44
|
+
return value.trim().toLowerCase().replace(/^\[/, "").replace(/\]$/, "").replace(/\.+$/, "");
|
|
45
|
+
}
|
|
46
|
+
function domainMatches(hostname, domain) {
|
|
47
|
+
const host = normalizeHostname(hostname);
|
|
48
|
+
const target = normalizeHostname(domain);
|
|
49
|
+
if (!host || !target)
|
|
50
|
+
return false;
|
|
51
|
+
return host === target || host.endsWith(`.${target}`);
|
|
52
|
+
}
|
|
53
|
+
function normalizeDomains(values) {
|
|
54
|
+
if (!Array.isArray(values))
|
|
55
|
+
return [];
|
|
56
|
+
const domains = new Set;
|
|
57
|
+
for (const value of values) {
|
|
58
|
+
if (typeof value !== "string")
|
|
59
|
+
continue;
|
|
60
|
+
const domain = normalizeDomain(value);
|
|
61
|
+
if (domain)
|
|
62
|
+
domains.add(domain);
|
|
63
|
+
}
|
|
64
|
+
return [...domains];
|
|
65
|
+
}
|
|
66
|
+
function isPrivateHostname(value) {
|
|
67
|
+
const host = normalizeHostname(value);
|
|
68
|
+
if (!host)
|
|
69
|
+
return true;
|
|
70
|
+
if (host === "localhost" || host === "local")
|
|
71
|
+
return true;
|
|
72
|
+
if (host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal") || host.endsWith(".lan") || host.endsWith(".home.arpa")) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
const version = isIP(host);
|
|
76
|
+
if (version === 4)
|
|
77
|
+
return isPrivateIpv4(host);
|
|
78
|
+
if (version === 6)
|
|
79
|
+
return isPrivateIpv6(host);
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
function assertFetchHostPolicy(url, options) {
|
|
83
|
+
const host = normalizeHostname(url.hostname);
|
|
84
|
+
if (options.blockPrivateHosts && isPrivateHostname(host)) {
|
|
85
|
+
throw new Error(`Fetch blocked for ${host}: local/private addresses are not reachable by DeepSeek. ` + "Set fetchBlockPrivateHosts=false only if you really need to send the URL.");
|
|
86
|
+
}
|
|
87
|
+
const blocked = options.blockedDomains.find((domain) => domainMatches(host, domain));
|
|
88
|
+
if (blocked) {
|
|
89
|
+
throw new Error(`Fetch blocked for ${host}: matched fetchBlockedDomains entry "${blocked}"`);
|
|
90
|
+
}
|
|
91
|
+
if (options.allowedDomains.length > 0 && !options.allowedDomains.some((domain) => domainMatches(host, domain))) {
|
|
92
|
+
throw new Error(`Fetch not allowed for ${host}: add it to fetchAllowedDomains (exact host or subdomain)`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function isPrivateIpv4(host) {
|
|
96
|
+
const parts = host.split(".").map((part) => Number(part));
|
|
97
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
const [a, b] = parts;
|
|
101
|
+
return a === 0 || a === 10 || a === 127 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 198 && (b === 18 || b === 19) || a >= 224;
|
|
102
|
+
}
|
|
103
|
+
function isPrivateIpv6(host) {
|
|
104
|
+
const value = host.toLowerCase();
|
|
105
|
+
if (value === "::" || value === "::1")
|
|
106
|
+
return true;
|
|
107
|
+
if (value.startsWith("fc") || value.startsWith("fd"))
|
|
108
|
+
return true;
|
|
109
|
+
if (/^fe[89ab]/.test(value))
|
|
110
|
+
return true;
|
|
111
|
+
const dottedMapped = value.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/)?.[1];
|
|
112
|
+
if (dottedMapped)
|
|
113
|
+
return isPrivateIpv4(dottedMapped);
|
|
114
|
+
const mapped = ipv4FromIpv6Hextets(value, /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
|
|
115
|
+
if (mapped)
|
|
116
|
+
return isPrivateIpv4(mapped);
|
|
117
|
+
const compatible = ipv4FromIpv6Hextets(value, /^::([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
|
|
118
|
+
if (compatible)
|
|
119
|
+
return isPrivateIpv4(compatible);
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
function ipv4FromIpv6Hextets(value, pattern) {
|
|
123
|
+
const match = value.match(pattern);
|
|
124
|
+
if (!match)
|
|
125
|
+
return;
|
|
126
|
+
const high = Number.parseInt(match[1], 16);
|
|
127
|
+
const low = Number.parseInt(match[2], 16);
|
|
128
|
+
if (!Number.isFinite(high) || !Number.isFinite(low))
|
|
129
|
+
return;
|
|
130
|
+
return `${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`;
|
|
131
|
+
}
|
|
132
|
+
var init_domains = () => {};
|
|
133
|
+
|
|
134
|
+
// src/core/options.ts
|
|
135
|
+
import { isIP as isIP2 } from "node:net";
|
|
136
|
+
function resolvePluginOptions(input) {
|
|
137
|
+
return {
|
|
138
|
+
search: resolveSearchOptions(input),
|
|
139
|
+
fetch: resolveFetchOptions(input),
|
|
140
|
+
auth: resolveAuthOptions(input),
|
|
141
|
+
retry: resolveRetryOptions(input)
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function resolveSearchOptions(input) {
|
|
145
|
+
const count = clampInt(input?.count, DEFAULT_SEARCH_COUNT, 1, 50);
|
|
146
|
+
return {
|
|
147
|
+
count,
|
|
148
|
+
maxResults: clampInt(input?.maxResults, Number.isFinite(input?.count) ? count : DEFAULT_MAX_RESULTS, 0, 50),
|
|
149
|
+
minRerank: finiteNumber(input?.minRerank, DEFAULT_MIN_RERANK),
|
|
150
|
+
minAuthority: finiteNumber(input?.minAuthority, DEFAULT_MIN_AUTHORITY),
|
|
151
|
+
allowUnknownAuthority: input?.allowUnknownAuthority !== false,
|
|
152
|
+
blockedDomains: normalizeDomains(input?.blockedDomains),
|
|
153
|
+
includeDomains: normalizeDomains(input?.includeDomains),
|
|
154
|
+
language: optionalText(input?.language),
|
|
155
|
+
localization: localizationValue(input?.searchLocalization),
|
|
156
|
+
maxSummaryChars: clampInt(input?.maxSummaryChars, DEFAULT_MAX_SUMMARY_CHARS, 0, 1e6),
|
|
157
|
+
timeoutMs: clampInt(input?.searchTimeoutMs, DEFAULT_TIMEOUT_MS, MIN_TIMEOUT_MS, MAX_TIMEOUT_MS),
|
|
158
|
+
enabled: input?.searchEnabled !== false
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function resolveFetchOptions(input) {
|
|
162
|
+
return {
|
|
163
|
+
maxChars: clampInt(input?.fetchMaxChars, DEFAULT_FETCH_MAX_CHARS, 0, 1e6),
|
|
164
|
+
allowedDomains: normalizeDomains(input?.fetchAllowedDomains),
|
|
165
|
+
blockedDomains: normalizeDomains(input?.fetchBlockedDomains),
|
|
166
|
+
timeoutMs: clampInt(input?.fetchTimeoutMs, DEFAULT_TIMEOUT_MS, MIN_TIMEOUT_MS, MAX_TIMEOUT_MS),
|
|
167
|
+
blockPrivateHosts: input?.fetchBlockPrivateHosts !== false,
|
|
168
|
+
enabled: input?.fetchEnabled !== false
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function resolveAuthOptions(input) {
|
|
172
|
+
const value = input?.apiKeySource;
|
|
173
|
+
return {
|
|
174
|
+
source: value === "opencode" || value === "env" ? value : DEFAULT_API_KEY_SOURCE
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function resolveRetryOptions(input) {
|
|
178
|
+
return {
|
|
179
|
+
maxRetries: clampInt(input?.maxRetries, DEFAULT_MAX_RETRIES, 0, MAX_RETRIES),
|
|
180
|
+
baseDelayMs: clampInt(input?.retryBaseDelayMs, DEFAULT_RETRY_BASE_DELAY_MS, 0, MAX_RETRY_DELAY_MS)
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function optionalText(value) {
|
|
184
|
+
if (typeof value !== "string")
|
|
185
|
+
return;
|
|
186
|
+
const trimmed = value.trim();
|
|
187
|
+
return trimmed ? trimmed : undefined;
|
|
188
|
+
}
|
|
189
|
+
function localizationValue(value) {
|
|
190
|
+
const text = optionalText(value);
|
|
191
|
+
return text !== undefined && !text.includes("%") && isIP2(text) !== 0 ? text : undefined;
|
|
192
|
+
}
|
|
193
|
+
function finiteNumber(value, fallback) {
|
|
194
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
195
|
+
}
|
|
196
|
+
function clampInt(value, fallback, min, max) {
|
|
197
|
+
const number = typeof value === "number" && Number.isFinite(value) ? Math.trunc(value) : fallback;
|
|
198
|
+
return Math.min(max, Math.max(min, number));
|
|
199
|
+
}
|
|
200
|
+
var init_options = __esm(() => {
|
|
201
|
+
init_constants();
|
|
202
|
+
init_domains();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// src/core/config-summary.ts
|
|
206
|
+
function configDeviations(resolved) {
|
|
207
|
+
const defaults = configSummaryValues(resolvePluginOptions({}));
|
|
208
|
+
const effective = configSummaryValues(resolved);
|
|
209
|
+
const tokens = [];
|
|
210
|
+
for (const key of Object.keys(effective)) {
|
|
211
|
+
const value = effective[key];
|
|
212
|
+
if (sameValue(value, defaults[key]))
|
|
213
|
+
continue;
|
|
214
|
+
tokens.push(`${key}=${formatConfigValue(value, "length")}`);
|
|
215
|
+
}
|
|
216
|
+
return tokens.join(" ");
|
|
217
|
+
}
|
|
218
|
+
function configSummaryValues(options) {
|
|
219
|
+
const values = {};
|
|
220
|
+
for (const entry of VOCABULARY)
|
|
221
|
+
values[entry.key] = entry.read(options);
|
|
222
|
+
return values;
|
|
223
|
+
}
|
|
224
|
+
function formatConfigValue(value, arrays) {
|
|
225
|
+
if (Array.isArray(value)) {
|
|
226
|
+
if (arrays === "length")
|
|
227
|
+
return String(value.length);
|
|
228
|
+
return value.length > 0 ? value.join(", ") : "[]";
|
|
229
|
+
}
|
|
230
|
+
return value === undefined ? "unset" : String(value);
|
|
231
|
+
}
|
|
232
|
+
function sameValue(value, fallback) {
|
|
233
|
+
if (Array.isArray(value) && Array.isArray(fallback)) {
|
|
234
|
+
return value.length === fallback.length && value.every((entry, index) => entry === fallback[index]);
|
|
235
|
+
}
|
|
236
|
+
return value === fallback;
|
|
237
|
+
}
|
|
238
|
+
var VOCABULARY, CONFIG_KEYS;
|
|
239
|
+
var init_config_summary = __esm(() => {
|
|
240
|
+
init_options();
|
|
241
|
+
VOCABULARY = [
|
|
242
|
+
{ key: "count", read: (o) => o.search.count },
|
|
243
|
+
{ key: "maxResults", read: (o) => o.search.maxResults },
|
|
244
|
+
{ key: "minRerank", read: (o) => o.search.minRerank },
|
|
245
|
+
{ key: "minAuthority", read: (o) => o.search.minAuthority },
|
|
246
|
+
{ key: "allowUnknownAuthority", read: (o) => o.search.allowUnknownAuthority },
|
|
247
|
+
{ key: "blockedDomains", read: (o) => o.search.blockedDomains },
|
|
248
|
+
{ key: "includeDomains", read: (o) => o.search.includeDomains },
|
|
249
|
+
{ key: "language", read: (o) => o.search.language },
|
|
250
|
+
{ key: "searchLocalization", read: (o) => o.search.localization },
|
|
251
|
+
{ key: "maxSummaryChars", read: (o) => o.search.maxSummaryChars },
|
|
252
|
+
{ key: "searchTimeoutMs", read: (o) => o.search.timeoutMs },
|
|
253
|
+
{ key: "fetchMaxChars", read: (o) => o.fetch.maxChars },
|
|
254
|
+
{ key: "fetchAllowedDomains", read: (o) => o.fetch.allowedDomains },
|
|
255
|
+
{ key: "fetchBlockedDomains", read: (o) => o.fetch.blockedDomains },
|
|
256
|
+
{ key: "fetchTimeoutMs", read: (o) => o.fetch.timeoutMs },
|
|
257
|
+
{ key: "fetchBlockPrivateHosts", read: (o) => o.fetch.blockPrivateHosts },
|
|
258
|
+
{ key: "maxRetries", read: (o) => o.retry.maxRetries },
|
|
259
|
+
{ key: "retryBaseDelayMs", read: (o) => o.retry.baseDelayMs }
|
|
260
|
+
];
|
|
261
|
+
CONFIG_KEYS = VOCABULARY.map((entry) => entry.key);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// src/core/object.ts
|
|
265
|
+
function isPlainObject(value) {
|
|
266
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/core/config-file.ts
|
|
19
270
|
import { readFileSync } from "node:fs";
|
|
271
|
+
import { homedir } from "node:os";
|
|
272
|
+
import { posix, win32 } from "node:path";
|
|
273
|
+
function joinFor(platform, ...parts) {
|
|
274
|
+
return (platform === "win32" ? win32 : posix).join(...parts);
|
|
275
|
+
}
|
|
276
|
+
function resolveContext(context = {}) {
|
|
277
|
+
return {
|
|
278
|
+
env: context.env ?? process.env,
|
|
279
|
+
home: context.home ?? homedir(),
|
|
280
|
+
platform: context.platform ?? process.platform
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function userConfigHome(context) {
|
|
284
|
+
const { env, home, platform } = resolveContext(context);
|
|
285
|
+
const xdg = env.XDG_CONFIG_HOME;
|
|
286
|
+
if (xdg)
|
|
287
|
+
return xdg;
|
|
288
|
+
if (platform === "win32")
|
|
289
|
+
return env.APPDATA || joinFor(platform, home, "AppData", "Roaming");
|
|
290
|
+
return joinFor(platform, home, ".config");
|
|
291
|
+
}
|
|
292
|
+
function userConfigDir(context) {
|
|
293
|
+
const { env, home, platform } = resolveContext(context);
|
|
294
|
+
return joinFor(platform, userConfigHome({ env, home, platform }), SHARED_CONFIG_DIR);
|
|
295
|
+
}
|
|
296
|
+
function configFilePath(context) {
|
|
297
|
+
const { env, home, platform } = resolveContext(context);
|
|
298
|
+
return joinFor(platform, userConfigDir({ env, home, platform }), SHARED_CONFIG_FILE);
|
|
299
|
+
}
|
|
300
|
+
function readFileOptionsReport(file) {
|
|
301
|
+
let text;
|
|
302
|
+
try {
|
|
303
|
+
text = readFileSync(file, "utf8");
|
|
304
|
+
} catch (error) {
|
|
305
|
+
if (error.code === "ENOENT") {
|
|
306
|
+
return { options: {}, warnings: [], exists: false };
|
|
307
|
+
}
|
|
308
|
+
return { options: {}, warnings: [`cannot read ${file} — file ignored`], exists: true };
|
|
309
|
+
}
|
|
310
|
+
let parsed;
|
|
311
|
+
try {
|
|
312
|
+
parsed = JSON.parse(text);
|
|
313
|
+
} catch {
|
|
314
|
+
return { options: {}, warnings: [`${file} is not valid JSON — file ignored`], exists: true };
|
|
315
|
+
}
|
|
316
|
+
if (!isPlainObject(parsed)) {
|
|
317
|
+
return { options: {}, warnings: [`${file} is not a JSON object — file ignored`], exists: true };
|
|
318
|
+
}
|
|
319
|
+
const options = {};
|
|
320
|
+
const warnings = [];
|
|
321
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
322
|
+
if (SHARED_KEY_SET.has(key))
|
|
323
|
+
options[key] = value;
|
|
324
|
+
else if (HOST_INTEGRATION_KEYS.has(key))
|
|
325
|
+
warnings.push(`"${key}" is a host-integration key (host config only) — ignored`);
|
|
326
|
+
else
|
|
327
|
+
warnings.push(`unknown key "${key}" — ignored`);
|
|
328
|
+
}
|
|
329
|
+
return { options, warnings, exists: true };
|
|
330
|
+
}
|
|
331
|
+
function readFileOptions(file) {
|
|
332
|
+
const report = readFileOptionsReport(file);
|
|
333
|
+
for (const warning of report.warnings)
|
|
334
|
+
warn(warning);
|
|
335
|
+
return report.options;
|
|
336
|
+
}
|
|
337
|
+
function sharedFileOptions(context = {}) {
|
|
338
|
+
return readFileOptions(configFilePath(context));
|
|
339
|
+
}
|
|
340
|
+
function warn(line) {
|
|
341
|
+
console.error(`deepseek-web-retrieval config: ${line}`);
|
|
342
|
+
}
|
|
343
|
+
var SHARED_CONFIG_DIR = "deepseek-web-retrieval", SHARED_CONFIG_FILE = "config.json", SHARED_CONFIG_KEYS, SHARED_KEY_SET, HOST_INTEGRATION_KEYS;
|
|
344
|
+
var init_config_file = __esm(() => {
|
|
345
|
+
init_config_summary();
|
|
346
|
+
SHARED_CONFIG_KEYS = CONFIG_KEYS;
|
|
347
|
+
SHARED_KEY_SET = new Set(SHARED_CONFIG_KEYS);
|
|
348
|
+
HOST_INTEGRATION_KEYS = new Set([
|
|
349
|
+
"searchEnabled",
|
|
350
|
+
"fetchEnabled",
|
|
351
|
+
"setDefaultWebsearch",
|
|
352
|
+
"fetchOverrideWebfetch",
|
|
353
|
+
"fetchRegisterTool",
|
|
354
|
+
"apiKeySource"
|
|
355
|
+
]);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// src/version.ts
|
|
359
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
20
360
|
var TOOL_VERSION;
|
|
21
361
|
var init_version = __esm(() => {
|
|
22
|
-
TOOL_VERSION = JSON.parse(
|
|
362
|
+
TOOL_VERSION = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
23
363
|
});
|
|
24
364
|
|
|
25
365
|
// src/installer/persistence.ts
|
|
@@ -31,7 +371,7 @@ import {
|
|
|
31
371
|
lstatSync,
|
|
32
372
|
mkdirSync,
|
|
33
373
|
openSync,
|
|
34
|
-
readFileSync as
|
|
374
|
+
readFileSync as readFileSync3,
|
|
35
375
|
realpathSync,
|
|
36
376
|
renameSync,
|
|
37
377
|
rmSync,
|
|
@@ -65,7 +405,7 @@ function atomicWrite(file, text, mode) {
|
|
|
65
405
|
try {
|
|
66
406
|
fd = openSync(temporary, "wx", 384);
|
|
67
407
|
writeFileSync(fd, text);
|
|
68
|
-
if (
|
|
408
|
+
if (readFileSync3(temporary, "utf8") !== text)
|
|
69
409
|
throw new Error(`Incomplete write to ${file}`);
|
|
70
410
|
fchmodSync(fd, mode ?? existingMode ?? 438 & ~process.umask());
|
|
71
411
|
fsyncSync(fd);
|
|
@@ -1447,11 +1787,6 @@ var init_jsonc = __esm(() => {
|
|
|
1447
1787
|
init_main();
|
|
1448
1788
|
});
|
|
1449
1789
|
|
|
1450
|
-
// src/core/object.ts
|
|
1451
|
-
function isPlainObject(value) {
|
|
1452
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
1790
|
// src/installer/formats/value.ts
|
|
1456
1791
|
function deepEqual(a, b) {
|
|
1457
1792
|
if (a === b)
|
|
@@ -1505,11 +1840,11 @@ function appendEntries(existing, entries, path) {
|
|
|
1505
1840
|
var init_value = () => {};
|
|
1506
1841
|
|
|
1507
1842
|
// src/installer/formats/json.ts
|
|
1508
|
-
import { existsSync, readFileSync as
|
|
1843
|
+
import { existsSync, readFileSync as readFileSync4 } from "node:fs";
|
|
1509
1844
|
function loadJson(file) {
|
|
1510
1845
|
if (!existsSync(file))
|
|
1511
1846
|
return { exists: false, text: "", value: {}, lossy: false };
|
|
1512
|
-
const text =
|
|
1847
|
+
const text = readFileSync4(file, "utf8");
|
|
1513
1848
|
try {
|
|
1514
1849
|
return { exists: true, text, value: parseJsonc(text), lossy: false };
|
|
1515
1850
|
} catch (error) {
|
|
@@ -1641,7 +1976,7 @@ var init_json = __esm(() => {
|
|
|
1641
1976
|
});
|
|
1642
1977
|
|
|
1643
1978
|
// src/installer/formats/rules.ts
|
|
1644
|
-
import { existsSync as existsSync2, readFileSync as
|
|
1979
|
+
import { existsSync as existsSync2, readFileSync as readFileSync5 } from "node:fs";
|
|
1645
1980
|
function rulesMarkers(host) {
|
|
1646
1981
|
return {
|
|
1647
1982
|
begin: `<!-- deepseek-web-retrieval:${host}:rules:begin -->`,
|
|
@@ -1696,7 +2031,7 @@ function rulesCodec(host) {
|
|
|
1696
2031
|
format: "rules",
|
|
1697
2032
|
load: (file) => {
|
|
1698
2033
|
const exists = existsSync2(file);
|
|
1699
|
-
const text = exists ?
|
|
2034
|
+
const text = exists ? readFileSync5(file, "utf8") : "";
|
|
1700
2035
|
try {
|
|
1701
2036
|
blockRange(host, text);
|
|
1702
2037
|
return { exists, text, value: {}, lossy: false };
|
|
@@ -2904,11 +3239,11 @@ var init_dist = __esm(() => {
|
|
|
2904
3239
|
});
|
|
2905
3240
|
|
|
2906
3241
|
// src/installer/formats/toml.ts
|
|
2907
|
-
import { existsSync as existsSync3, readFileSync as
|
|
3242
|
+
import { existsSync as existsSync3, readFileSync as readFileSync6 } from "node:fs";
|
|
2908
3243
|
function loadToml(file) {
|
|
2909
3244
|
if (!existsSync3(file))
|
|
2910
3245
|
return { exists: false, text: "", value: {}, lossy: false };
|
|
2911
|
-
const text =
|
|
3246
|
+
const text = readFileSync6(file, "utf8");
|
|
2912
3247
|
try {
|
|
2913
3248
|
return { exists: true, text, value: parse3(text), lossy: hasTomlComment(text) };
|
|
2914
3249
|
} catch (error) {
|
|
@@ -3081,7 +3416,7 @@ var init_formats = __esm(() => {
|
|
|
3081
3416
|
});
|
|
3082
3417
|
|
|
3083
3418
|
// src/installer/revert.ts
|
|
3084
|
-
import { existsSync as existsSync4, readFileSync as
|
|
3419
|
+
import { existsSync as existsSync4, readFileSync as readFileSync7, rmdirSync, unlinkSync } from "node:fs";
|
|
3085
3420
|
import { dirname as dirname2 } from "node:path";
|
|
3086
3421
|
function pruneEmptyDir(dir) {
|
|
3087
3422
|
try {
|
|
@@ -3095,7 +3430,7 @@ function revertRecordedFile(file, host, opts) {
|
|
|
3095
3430
|
return lines;
|
|
3096
3431
|
}
|
|
3097
3432
|
const codec = codecFor(file.format, host);
|
|
3098
|
-
const text =
|
|
3433
|
+
const text = readFileSync7(file.file, "utf8");
|
|
3099
3434
|
if (file.format === "rules") {
|
|
3100
3435
|
const edit2 = { file: file.file, format: "rules", block: file.blockApplied ?? "" };
|
|
3101
3436
|
if (!codec.verify(text, edit2)) {
|
|
@@ -3144,16 +3479,29 @@ function stateDir(roots, scope) {
|
|
|
3144
3479
|
var init_paths = () => {};
|
|
3145
3480
|
|
|
3146
3481
|
// src/installer/state.ts
|
|
3147
|
-
import { existsSync as existsSync5, lstatSync as lstatSync2, readdirSync, readFileSync as
|
|
3482
|
+
import { existsSync as existsSync5, lstatSync as lstatSync2, readdirSync, readFileSync as readFileSync8, rmSync as rmSync2 } from "node:fs";
|
|
3148
3483
|
import { isAbsolute, join as join3 } from "node:path";
|
|
3149
3484
|
import { z } from "zod/v4";
|
|
3150
3485
|
function statePathFor(input) {
|
|
3151
3486
|
return join3(stateDir(input, input.scope), `install.${input.host}.json`);
|
|
3152
3487
|
}
|
|
3488
|
+
function platformOf(roots) {
|
|
3489
|
+
return roots.platform ?? process.platform;
|
|
3490
|
+
}
|
|
3491
|
+
function legacyGlobalStateDir(roots) {
|
|
3492
|
+
return join3(roots.home, ".config", "deepseek-web-retrieval");
|
|
3493
|
+
}
|
|
3494
|
+
function recordReadPath(input, platform = platformOf(input)) {
|
|
3495
|
+
const canonical = statePathFor(input);
|
|
3496
|
+
if (platform !== "win32" || input.scope !== "global" || existsSync5(canonical))
|
|
3497
|
+
return canonical;
|
|
3498
|
+
const legacy = join3(legacyGlobalStateDir(input), `install.${input.host}.json`);
|
|
3499
|
+
return existsSync5(legacy) ? legacy : canonical;
|
|
3500
|
+
}
|
|
3153
3501
|
function readRecordFile(file, host, scope) {
|
|
3154
3502
|
let text;
|
|
3155
3503
|
try {
|
|
3156
|
-
text =
|
|
3504
|
+
text = readFileSync8(file, "utf8");
|
|
3157
3505
|
} catch (error) {
|
|
3158
3506
|
if (error.code === "ENOENT") {
|
|
3159
3507
|
try {
|
|
@@ -3181,7 +3529,7 @@ function readRecordFile(file, host, scope) {
|
|
|
3181
3529
|
return result.data;
|
|
3182
3530
|
}
|
|
3183
3531
|
function readRecord(input) {
|
|
3184
|
-
return readRecordFile(
|
|
3532
|
+
return readRecordFile(recordReadPath(input), input.host, input.scope);
|
|
3185
3533
|
}
|
|
3186
3534
|
function claimedByOtherScope(input, files) {
|
|
3187
3535
|
const other = input.scope === "global" ? "project" : "global";
|
|
@@ -3210,32 +3558,49 @@ function appendedByPrevious(record, path) {
|
|
|
3210
3558
|
return entries;
|
|
3211
3559
|
}
|
|
3212
3560
|
function deleteRecord(input) {
|
|
3213
|
-
const file =
|
|
3561
|
+
const file = recordReadPath(input);
|
|
3214
3562
|
if (existsSync5(file))
|
|
3215
3563
|
rmSync2(file);
|
|
3216
3564
|
}
|
|
3217
|
-
function
|
|
3565
|
+
function readRecordsFromDir(dir, scope) {
|
|
3218
3566
|
const records = [];
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
if (record !== undefined)
|
|
3234
|
-
records.push(record);
|
|
3235
|
-
}
|
|
3567
|
+
let names;
|
|
3568
|
+
try {
|
|
3569
|
+
names = readdirSync(dir);
|
|
3570
|
+
} catch (error) {
|
|
3571
|
+
if (error.code === "ENOENT")
|
|
3572
|
+
return records;
|
|
3573
|
+
throw new InstallRecordError(dir, "unreadable", error.code ?? "list failed");
|
|
3574
|
+
}
|
|
3575
|
+
for (const name of names) {
|
|
3576
|
+
if (!name.startsWith("install.") || !name.endsWith(".json"))
|
|
3577
|
+
continue;
|
|
3578
|
+
const record = readRecordFile(join3(dir, name), name.slice("install.".length, -".json".length), scope);
|
|
3579
|
+
if (record !== undefined)
|
|
3580
|
+
records.push(record);
|
|
3236
3581
|
}
|
|
3237
3582
|
return records;
|
|
3238
3583
|
}
|
|
3584
|
+
function hasRecordFiles(dir) {
|
|
3585
|
+
try {
|
|
3586
|
+
return readdirSync(dir).some((name) => name.startsWith("install.") && name.endsWith(".json"));
|
|
3587
|
+
} catch {
|
|
3588
|
+
return false;
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
function readingLegacyGlobalRecords(roots) {
|
|
3592
|
+
if (platformOf(roots) !== "win32")
|
|
3593
|
+
return false;
|
|
3594
|
+
if (hasRecordFiles(stateDir(roots, "global")))
|
|
3595
|
+
return false;
|
|
3596
|
+
return hasRecordFiles(legacyGlobalStateDir(roots));
|
|
3597
|
+
}
|
|
3598
|
+
function listRecords(roots) {
|
|
3599
|
+
const global = readRecordsFromDir(stateDir(roots, "global"), "global");
|
|
3600
|
+
const project = readRecordsFromDir(stateDir(roots, "project"), "project");
|
|
3601
|
+
const legacy = readingLegacyGlobalRecords(roots) ? readRecordsFromDir(legacyGlobalStateDir(roots), "global") : [];
|
|
3602
|
+
return [...global, ...legacy, ...project];
|
|
3603
|
+
}
|
|
3239
3604
|
var keyPath, tier, operation, recordSchema, InstallRecordError;
|
|
3240
3605
|
var init_state = __esm(() => {
|
|
3241
3606
|
init_paths();
|
|
@@ -3254,7 +3619,7 @@ var init_state = __esm(() => {
|
|
|
3254
3619
|
recordSchema = z.object({
|
|
3255
3620
|
version: z.literal(1),
|
|
3256
3621
|
installerVersion: z.string().optional(),
|
|
3257
|
-
host: z.enum(["opencode", "pi", "codex"]),
|
|
3622
|
+
host: z.enum(["opencode", "pi", "codex", "claude-code"]),
|
|
3258
3623
|
scope: z.enum(["global", "project"]),
|
|
3259
3624
|
tiers: z.object({ search: tier, fetch: tier }),
|
|
3260
3625
|
createdAt: z.string().refine((value) => Number.isFinite(Date.parse(value))),
|
|
@@ -3281,8 +3646,25 @@ var init_state = __esm(() => {
|
|
|
3281
3646
|
});
|
|
3282
3647
|
|
|
3283
3648
|
// src/installer/ownership.ts
|
|
3284
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as
|
|
3649
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync9, realpathSync as realpathSync2, rmSync as rmSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
3285
3650
|
import { basename as basename2, dirname as dirname3, join as join4, resolve } from "node:path";
|
|
3651
|
+
function hasVersionSuffix(ref) {
|
|
3652
|
+
const at = ref.lastIndexOf("@");
|
|
3653
|
+
return at > 0 && /^[0-9]/.test(ref.slice(at + 1));
|
|
3654
|
+
}
|
|
3655
|
+
function stripVersionSuffix(ref) {
|
|
3656
|
+
const at = ref.lastIndexOf("@");
|
|
3657
|
+
return hasVersionSuffix(ref) ? ref.slice(0, at) : ref;
|
|
3658
|
+
}
|
|
3659
|
+
function samePackageName(a, b) {
|
|
3660
|
+
return stripVersionSuffix(a) === stripVersionSuffix(b);
|
|
3661
|
+
}
|
|
3662
|
+
function lineageMatcher(refs) {
|
|
3663
|
+
return (candidate) => refs.some((ref) => samePackageName(ref, candidate));
|
|
3664
|
+
}
|
|
3665
|
+
function pinPackageRef(ref, version) {
|
|
3666
|
+
return hasVersionSuffix(ref) ? ref : `${ref}@${version}`;
|
|
3667
|
+
}
|
|
3286
3668
|
function canonicalPath(file) {
|
|
3287
3669
|
const absolute = resolve(file);
|
|
3288
3670
|
if (existsSync6(absolute))
|
|
@@ -3304,7 +3686,7 @@ function readOwner(file) {
|
|
|
3304
3686
|
if (!existsSync6(file))
|
|
3305
3687
|
return;
|
|
3306
3688
|
try {
|
|
3307
|
-
const value = JSON.parse(
|
|
3689
|
+
const value = JSON.parse(readFileSync9(file, "utf8"));
|
|
3308
3690
|
if (isPlainObject(value) && typeof value.recordPath === "string" && typeof value.cwd === "string" && (value.scope === "project" || value.scope === "global")) {
|
|
3309
3691
|
return { recordPath: value.recordPath, cwd: value.cwd, scope: value.scope };
|
|
3310
3692
|
}
|
|
@@ -3315,6 +3697,8 @@ function assertOwner(input, claim) {
|
|
|
3315
3697
|
const owner = readOwner(claim);
|
|
3316
3698
|
if (owner === undefined || owner.recordPath === ownerOf(input).recordPath)
|
|
3317
3699
|
return;
|
|
3700
|
+
if (owner.recordPath === canonicalPath(recordReadPath(input)))
|
|
3701
|
+
return;
|
|
3318
3702
|
throw new Error(`A ${input.host} install is already recorded at the ${owner.scope} scope (${owner.recordPath}) and claims the same config file. ` + `Uninstall it first: deepseek-web-retrieval uninstall --host ${input.host} --scope ${owner.scope} --cwd ${JSON.stringify(owner.cwd)}. ` + "Two install records must not edit one file.");
|
|
3319
3703
|
}
|
|
3320
3704
|
function assertFileOwnership(input, files) {
|
|
@@ -3418,7 +3802,7 @@ var init_operation_lock = __esm(() => {
|
|
|
3418
3802
|
});
|
|
3419
3803
|
|
|
3420
3804
|
// src/installer/apply.ts
|
|
3421
|
-
import { existsSync as existsSync7, readFileSync as
|
|
3805
|
+
import { existsSync as existsSync7, readFileSync as readFileSync10, rmSync as rmSync5 } from "node:fs";
|
|
3422
3806
|
import { dirname as dirname5 } from "node:path";
|
|
3423
3807
|
function applyPlan(plan, input) {
|
|
3424
3808
|
if (plan.errors.length > 0) {
|
|
@@ -3516,7 +3900,7 @@ function snapshotTouchedFiles(plan) {
|
|
|
3516
3900
|
const snapshots = new Map;
|
|
3517
3901
|
const touched = [...plan.edits.map((edit2) => edit2.file), ...(plan.reverts ?? []).map((revert) => revert.file)];
|
|
3518
3902
|
for (const file of new Set(touched)) {
|
|
3519
|
-
snapshots.set(file, existsSync7(file) ? { exists: true, text:
|
|
3903
|
+
snapshots.set(file, existsSync7(file) ? { exists: true, text: readFileSync10(file, "utf8") } : { exists: false, text: "" });
|
|
3520
3904
|
}
|
|
3521
3905
|
return snapshots;
|
|
3522
3906
|
}
|
|
@@ -3526,10 +3910,10 @@ function rollbackApply(input, previous, snapshots) {
|
|
|
3526
3910
|
for (const [file, snapshot] of [...snapshots.entries()].reverse()) {
|
|
3527
3911
|
try {
|
|
3528
3912
|
if (snapshot.exists) {
|
|
3529
|
-
if (existsSync7(file) &&
|
|
3913
|
+
if (existsSync7(file) && readFileSync10(file, "utf8") === snapshot.text)
|
|
3530
3914
|
continue;
|
|
3531
3915
|
writeConfig(file, snapshot.text);
|
|
3532
|
-
if (
|
|
3916
|
+
if (readFileSync10(file, "utf8") !== snapshot.text)
|
|
3533
3917
|
throw new Error("restored contents differ from the snapshot");
|
|
3534
3918
|
} else {
|
|
3535
3919
|
if (!existsSync7(file))
|
|
@@ -3622,7 +4006,7 @@ function verifyEdits(edits, host) {
|
|
|
3622
4006
|
continue;
|
|
3623
4007
|
}
|
|
3624
4008
|
const codec = codecFor(edit2.format, host);
|
|
3625
|
-
const text =
|
|
4009
|
+
const text = readFileSync10(edit2.file, "utf8");
|
|
3626
4010
|
if (edit2.format === "rules") {
|
|
3627
4011
|
const good = codec.verify(text, edit2);
|
|
3628
4012
|
lines.push(`${good ? "pass" : "FAIL"} ${edit2.file}: rules block ${good ? "present" : "missing"}`);
|
|
@@ -3661,1956 +4045,2177 @@ var init_apply = __esm(() => {
|
|
|
3661
4045
|
};
|
|
3662
4046
|
});
|
|
3663
4047
|
|
|
3664
|
-
// src/core/
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
4048
|
+
// src/core/auth.ts
|
|
4049
|
+
function apiKeyFromEnv(env = process.env) {
|
|
4050
|
+
const key = env[API_KEY_ENV]?.trim();
|
|
4051
|
+
return key ? key : undefined;
|
|
4052
|
+
}
|
|
4053
|
+
var MISSING_API_KEY_MESSAGE;
|
|
4054
|
+
var init_auth = __esm(() => {
|
|
4055
|
+
init_constants();
|
|
4056
|
+
MISSING_API_KEY_MESSAGE = `DeepSeek API key not found. Set ${API_KEY_ENV} in the MCP server's env configuration.`;
|
|
3670
4057
|
});
|
|
3671
4058
|
|
|
3672
|
-
// src/
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
4059
|
+
// src/core/format.ts
|
|
4060
|
+
function truncate(value, maxChars) {
|
|
4061
|
+
if (maxChars <= 0)
|
|
4062
|
+
return "";
|
|
4063
|
+
if (value.length <= maxChars)
|
|
4064
|
+
return value;
|
|
4065
|
+
return Array.from(value).slice(0, maxChars).join("");
|
|
3676
4066
|
}
|
|
3677
|
-
function
|
|
3678
|
-
|
|
4067
|
+
function formatFetchedContent(title, content, maxChars) {
|
|
4068
|
+
const document = withTitle(title, content);
|
|
4069
|
+
const length = Array.from(document).length;
|
|
4070
|
+
if (maxChars <= 0)
|
|
4071
|
+
return { content: "", truncated: length > 0 };
|
|
4072
|
+
if (length <= maxChars)
|
|
4073
|
+
return { content: document, truncated: false };
|
|
4074
|
+
const markerLength = Array.from(FETCH_TRUNCATION_MARKER).length;
|
|
4075
|
+
if (maxChars <= markerLength) {
|
|
4076
|
+
return { content: truncate(document, maxChars), truncated: true };
|
|
4077
|
+
}
|
|
4078
|
+
return {
|
|
4079
|
+
content: `${truncate(document, maxChars - markerLength)}${FETCH_TRUNCATION_MARKER}`,
|
|
4080
|
+
truncated: true
|
|
4081
|
+
};
|
|
3679
4082
|
}
|
|
3680
|
-
function
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
const
|
|
3686
|
-
const
|
|
3687
|
-
|
|
3688
|
-
|
|
4083
|
+
function withTitle(title, content) {
|
|
4084
|
+
const heading = title.trim();
|
|
4085
|
+
if (!heading)
|
|
4086
|
+
return content;
|
|
4087
|
+
const body = content.trimStart();
|
|
4088
|
+
const lines = body.split(/\r?\n/);
|
|
4089
|
+
const firstHeading = lines[0]?.match(/^\\?#\s+(.+?)\s*$/)?.[1];
|
|
4090
|
+
if (firstHeading && normalizeHeading(firstHeading) === normalizeHeading(heading)) {
|
|
4091
|
+
lines[0] = `# ${heading}`;
|
|
4092
|
+
return lines.join(`
|
|
4093
|
+
`);
|
|
4094
|
+
}
|
|
4095
|
+
return `# ${heading}
|
|
4096
|
+
|
|
4097
|
+
${content}`;
|
|
3689
4098
|
}
|
|
3690
|
-
function
|
|
3691
|
-
return
|
|
4099
|
+
function normalizeHeading(value) {
|
|
4100
|
+
return value.trim().replace(/\s+/g, " ").replace(/\s+#+\s*$/, "").toLowerCase();
|
|
3692
4101
|
}
|
|
3693
|
-
function
|
|
3694
|
-
|
|
3695
|
-
return [];
|
|
3696
|
-
let text;
|
|
3697
|
-
try {
|
|
3698
|
-
text = readFileSync10(file, "utf8");
|
|
3699
|
-
} catch {
|
|
3700
|
-
return [];
|
|
3701
|
-
}
|
|
3702
|
-
return removeBlock(host, text).split(`
|
|
3703
|
-
`).map((line) => line.trim()).filter((line) => line.length > 0 && STEERING_HINT.test(line) && !line.includes("deepseek")).slice(0, 2);
|
|
4102
|
+
function redactSecrets(value) {
|
|
4103
|
+
return value.replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, "sk-***").replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi, "Bearer ***");
|
|
3704
4104
|
}
|
|
3705
|
-
function
|
|
3706
|
-
const
|
|
3707
|
-
|
|
4105
|
+
function formatApiError(operation2, status, body) {
|
|
4106
|
+
const detail = redactSecrets(body.trim());
|
|
4107
|
+
const detailChars = Array.from(detail);
|
|
4108
|
+
const text = detail ? detailChars.length > MAX_ERROR_BODY_CHARS ? `${detailChars.slice(0, MAX_ERROR_BODY_CHARS).join("")}…` : detail : "empty response body";
|
|
4109
|
+
const hint = status === 401 ? "invalid API key" : status === 402 ? "insufficient balance" : status === 403 ? "permission denied" : status === 429 ? "rate limited" : undefined;
|
|
4110
|
+
const label = hint ? `${operation2} failed (HTTP ${status}, ${hint})` : `${operation2} failed (HTTP ${status})`;
|
|
4111
|
+
return `${label}: ${text}`;
|
|
3708
4112
|
}
|
|
3709
|
-
function
|
|
3710
|
-
|
|
3711
|
-
const bystanders = [];
|
|
3712
|
-
for (const [name, entry] of Object.entries(servers)) {
|
|
3713
|
-
if (name === "deepseek")
|
|
3714
|
-
continue;
|
|
3715
|
-
(looksWebCapable(name, entry) ? competitors : bystanders).push(name);
|
|
3716
|
-
}
|
|
3717
|
-
return { competitors, bystanders };
|
|
4113
|
+
function formatTimeoutError(operation2, timeoutMs) {
|
|
4114
|
+
return `${operation2} timed out after ${timeoutMs} ms`;
|
|
3718
4115
|
}
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
4116
|
+
function formatSearchResults(results) {
|
|
4117
|
+
if (results.length === 0)
|
|
4118
|
+
return "No results found.";
|
|
4119
|
+
return results.map((result, index) => {
|
|
4120
|
+
const title = result.title?.trim() || result.url;
|
|
4121
|
+
const marker = formatQualityMarker(result);
|
|
4122
|
+
const lines = [`${index + 1}. [${escapeBrackets(title)}](${result.url})${marker ? ` ${marker}` : ""}`];
|
|
4123
|
+
if (result.content) {
|
|
4124
|
+
lines.push(` ${result.content.replaceAll(`
|
|
4125
|
+
`, " ")}`);
|
|
4126
|
+
}
|
|
4127
|
+
if (result.published !== undefined) {
|
|
4128
|
+
lines.push(` Published: ${formatPublished(result.published)}`);
|
|
4129
|
+
}
|
|
4130
|
+
return lines.join(`
|
|
4131
|
+
`);
|
|
4132
|
+
}).join(`
|
|
3727
4133
|
|
|
3728
|
-
|
|
3729
|
-
function rulesLines(ids, search, fetch) {
|
|
3730
|
-
const lines = ["DeepSeek retrieval guidance:"];
|
|
3731
|
-
if (search === "preferred") {
|
|
3732
|
-
lines.push(`- Use ${ids.search} when the task needs fresh external information: current docs, version numbers,`, " changelogs, or anything you cannot know from the repository alone. Do not search for facts already", " present locally.");
|
|
3733
|
-
}
|
|
3734
|
-
if (fetch === "preferred") {
|
|
3735
|
-
lines.push(`- Use ${ids.fetch} to read a specific public web page as Markdown when its content is needed.`);
|
|
3736
|
-
}
|
|
3737
|
-
lines.push("- Never send localhost or private-network URLs to the DeepSeek tools; use local tools for those (the", " server blocks private hosts by default).", "- DeepSeek requests are billed per successful call; retrieve only what the task needs.");
|
|
3738
|
-
return lines;
|
|
4134
|
+
`);
|
|
3739
4135
|
}
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
4136
|
+
function formatQualityMarker(result) {
|
|
4137
|
+
const parts = [];
|
|
4138
|
+
if (result.rerankScore !== undefined)
|
|
4139
|
+
parts.push(`rerank ${result.rerankScore}`);
|
|
4140
|
+
if (result.authority !== undefined && result.authority >= 0)
|
|
4141
|
+
parts.push(`authority ${result.authority}`);
|
|
4142
|
+
if (result.siteName)
|
|
4143
|
+
parts.push(result.siteName);
|
|
4144
|
+
return parts.length > 0 ? `[${parts.join(" · ")}]` : "";
|
|
4145
|
+
}
|
|
4146
|
+
function toStructuredResult(result) {
|
|
4147
|
+
return {
|
|
4148
|
+
url: result.url,
|
|
4149
|
+
...result.title ? { title: result.title } : {},
|
|
4150
|
+
...result.content ? { content: result.content } : {},
|
|
4151
|
+
...result.published !== undefined ? { published: result.published } : {},
|
|
4152
|
+
...result.rerankScore !== undefined ? { rerankScore: result.rerankScore } : {},
|
|
4153
|
+
...result.authority !== undefined ? { authority: result.authority } : {},
|
|
4154
|
+
...result.siteName ? { siteName: result.siteName } : {}
|
|
3745
4155
|
};
|
|
3746
|
-
|
|
4156
|
+
}
|
|
4157
|
+
function escapeBrackets(value) {
|
|
4158
|
+
return value.replaceAll("[", "\\[").replaceAll("]", "\\]");
|
|
4159
|
+
}
|
|
4160
|
+
function formatPublished(epochMs) {
|
|
4161
|
+
return new Date(epochMs).toISOString().slice(0, 10);
|
|
4162
|
+
}
|
|
4163
|
+
var init_format2 = __esm(() => {
|
|
4164
|
+
init_constants();
|
|
3747
4165
|
});
|
|
3748
4166
|
|
|
3749
|
-
// src/
|
|
3750
|
-
function
|
|
3751
|
-
const
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
4167
|
+
// src/core/url.ts
|
|
4168
|
+
function parseFetchUrl(value) {
|
|
4169
|
+
const input = value.trim();
|
|
4170
|
+
if (!input)
|
|
4171
|
+
throw new Error("URL must not be empty");
|
|
4172
|
+
if (input.length > MAX_URL_CHARS) {
|
|
4173
|
+
throw new Error(`URL must be ${MAX_URL_CHARS} characters or fewer`);
|
|
3756
4174
|
}
|
|
3757
|
-
|
|
3758
|
-
|
|
4175
|
+
let url;
|
|
4176
|
+
try {
|
|
4177
|
+
url = new URL(input);
|
|
4178
|
+
} catch {
|
|
4179
|
+
throw new Error("URL must be an absolute http:// or https:// URL");
|
|
3759
4180
|
}
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
errors.push({ message: `${paths.rules}: ${rules.parseError}` });
|
|
3763
|
-
return { notes, errors, abort: true };
|
|
4181
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
4182
|
+
throw new Error("URL must use http:// or https://");
|
|
3764
4183
|
}
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
4184
|
+
if (!url.hostname || !normalizeHostname(url.hostname)) {
|
|
4185
|
+
throw new Error("URL must include a host");
|
|
4186
|
+
}
|
|
4187
|
+
if (url.username || url.password) {
|
|
4188
|
+
throw new Error("URL must not contain credentials");
|
|
4189
|
+
}
|
|
4190
|
+
return url;
|
|
3769
4191
|
}
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
4192
|
+
function normalizeResultUrl(value) {
|
|
4193
|
+
if (typeof value !== "string")
|
|
4194
|
+
return;
|
|
4195
|
+
const input = value.trim();
|
|
4196
|
+
if (!input || input.length > MAX_URL_CHARS)
|
|
4197
|
+
return;
|
|
4198
|
+
try {
|
|
4199
|
+
const url = new URL(input);
|
|
4200
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
4201
|
+
return;
|
|
4202
|
+
if (!url.hostname || url.username || url.password)
|
|
4203
|
+
return;
|
|
4204
|
+
return url.href;
|
|
4205
|
+
} catch {
|
|
4206
|
+
return;
|
|
3782
4207
|
}
|
|
3783
|
-
return version;
|
|
3784
4208
|
}
|
|
3785
|
-
|
|
3786
|
-
|
|
4209
|
+
var init_url = __esm(() => {
|
|
4210
|
+
init_constants();
|
|
4211
|
+
init_domains();
|
|
4212
|
+
});
|
|
4213
|
+
|
|
4214
|
+
// src/core/search.ts
|
|
4215
|
+
function buildQuery(query, options) {
|
|
4216
|
+
const parts = [query.trim()];
|
|
4217
|
+
for (const domain of normalizeDomains(options.blockedDomains))
|
|
4218
|
+
parts.push(`-site:${domain}`);
|
|
4219
|
+
const include = normalizeDomains(options.includeDomains);
|
|
4220
|
+
if (include.length)
|
|
4221
|
+
parts.push(include.map((domain) => `site:${domain}`).join(" OR "));
|
|
4222
|
+
const language = options.language?.trim().replace(/^lang:/i, "");
|
|
4223
|
+
if (language)
|
|
4224
|
+
parts.push(`lang:${language}`);
|
|
4225
|
+
return parts.filter((part) => part.length > 0).join(" ");
|
|
3787
4226
|
}
|
|
3788
|
-
function
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
4227
|
+
function parsePublished(value) {
|
|
4228
|
+
if (typeof value !== "string" || !value.trim())
|
|
4229
|
+
return;
|
|
4230
|
+
const timestamp = Date.parse(value);
|
|
4231
|
+
return Number.isFinite(timestamp) ? timestamp : undefined;
|
|
3793
4232
|
}
|
|
3794
|
-
function
|
|
3795
|
-
const
|
|
3796
|
-
const
|
|
3797
|
-
const
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
message: `${paths.config} sets [tools] web_search, which is a silent no-op at Codex 0.163.0 (only the top-level ` + "web_search key controls the tool). Remove it first so the config cannot mislead."
|
|
3812
|
-
});
|
|
3813
|
-
}
|
|
3814
|
-
const servers = loaded.value.mcp_servers;
|
|
3815
|
-
if (isPlainObject(servers)) {
|
|
3816
|
-
if ("deepseek" in servers)
|
|
3817
|
-
notes.push(replacingExistingEntryNote("mcp_servers.deepseek"));
|
|
3818
|
-
const { competitors, bystanders } = splitCompetitors(servers);
|
|
3819
|
-
if ((input.search === "forced" || input.fetch === "forced") && competitors.length > 0) {
|
|
3820
|
-
errors.push({
|
|
3821
|
-
message: `Forced cannot deliver exclusive routing: [mcp_servers.${competitors.join("], [mcp_servers.")}] look(s) web-capable, ` + "and Codex offers no per-server kill switch. Remove the server(s), disable their tools by hand via their " + "disabled_tools key, or choose a lower tier."
|
|
3822
|
-
});
|
|
4233
|
+
function processResults(raw, options) {
|
|
4234
|
+
const results = [];
|
|
4235
|
+
const seen = new Set;
|
|
4236
|
+
for (const item of raw) {
|
|
4237
|
+
if (results.length >= options.maxResults)
|
|
4238
|
+
break;
|
|
4239
|
+
if (!item || typeof item !== "object")
|
|
4240
|
+
continue;
|
|
4241
|
+
const url = normalizeResultUrl(item.url);
|
|
4242
|
+
if (!url || seen.has(url))
|
|
4243
|
+
continue;
|
|
4244
|
+
const rerank = item.rerank_score;
|
|
4245
|
+
if (typeof rerank === "number" && Number.isFinite(rerank)) {
|
|
4246
|
+
if (rerank < options.minRerank)
|
|
4247
|
+
continue;
|
|
4248
|
+
} else if (options.minRerank > 0) {
|
|
4249
|
+
continue;
|
|
3823
4250
|
}
|
|
3824
|
-
const
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
ops.push({ op: "set", path: ["web_search"], value: "disabled", previous });
|
|
3848
|
-
notes.push(previous === undefined ? 'web_search is being set to "disabled" (no previous value existed; the default is "cached").' : `web_search is being set to "disabled"; the previous value "${previous}" is snapshotted for uninstall.`);
|
|
3849
|
-
}
|
|
3850
|
-
edits.push({ file: paths.config, format: "toml", shared: true, ops });
|
|
3851
|
-
notes.push(`Codex stdio children do not inherit the parent environment, so the entry forwards ${API_KEY_ENV} explicitly ` + "via env_vars; the key must be set in the shell that launches Codex.");
|
|
3852
|
-
if (input.scope === "project") {
|
|
3853
|
-
notes.push("Codex config lives in ~/.codex/config.toml only (user level), so the server registration and kill switch are " + "written there even for a project-scope install; the rules file stays project-local.");
|
|
3854
|
-
}
|
|
3855
|
-
if (loaded.lossy) {
|
|
3856
|
-
notes.push(`${paths.config} contains comments; the TOML rewrite will drop them.`);
|
|
3857
|
-
}
|
|
3858
|
-
if (input.search === "forced" || input.fetch === "forced") {
|
|
3859
|
-
notes.push(`Forced search is exclusion: the built-in web_search tool is disabled ("disabled" mode). ${FORCED_COST_NOTE}`);
|
|
3860
|
-
}
|
|
3861
|
-
if (input.search === "optional" || input.fetch === "optional") {
|
|
3862
|
-
notes.push('Note: Codex ships a built-in web_search tool (default "cached" mode, an OpenAI index) that stays available ' + "alongside the DeepSeek tools under the optional tier.");
|
|
3863
|
-
}
|
|
3864
|
-
if (input.search === "preferred" || input.fetch === "preferred") {
|
|
3865
|
-
edits.push({
|
|
3866
|
-
file: paths.rules,
|
|
3867
|
-
format: "rules",
|
|
3868
|
-
block: buildRulesBlock(input.host, rulesLines(MCP_TOOL_IDS, input.search, input.fetch))
|
|
4251
|
+
const authority = item.authority;
|
|
4252
|
+
const knownAuthority = typeof authority === "number" && Number.isFinite(authority) && authority >= 0;
|
|
4253
|
+
if (knownAuthority) {
|
|
4254
|
+
if (authority < options.minAuthority)
|
|
4255
|
+
continue;
|
|
4256
|
+
} else if (!options.allowUnknownAuthority) {
|
|
4257
|
+
continue;
|
|
4258
|
+
}
|
|
4259
|
+
seen.add(url);
|
|
4260
|
+
const content = truncate(optionalText2(item.summary) ?? optionalText2(item.snippet) ?? "", options.maxSummaryChars);
|
|
4261
|
+
const published = parsePublished(item.date);
|
|
4262
|
+
const title = optionalText2(item.title);
|
|
4263
|
+
const rerankScore = typeof rerank === "number" && Number.isFinite(rerank) ? rerank : undefined;
|
|
4264
|
+
const authorityScore = typeof authority === "number" && Number.isFinite(authority) ? authority : undefined;
|
|
4265
|
+
const siteName = optionalText2(item.site_name);
|
|
4266
|
+
results.push({
|
|
4267
|
+
url,
|
|
4268
|
+
...title ? { title } : {},
|
|
4269
|
+
...content ? { content } : {},
|
|
4270
|
+
...published !== undefined ? { published } : {},
|
|
4271
|
+
...rerankScore !== undefined ? { rerankScore } : {},
|
|
4272
|
+
...authorityScore !== undefined ? { authority: authorityScore } : {},
|
|
4273
|
+
...siteName ? { siteName } : {}
|
|
3869
4274
|
});
|
|
3870
|
-
notes.push(input.scope === "global" ? `Rules block upserted in ${paths.rules}; Codex reads ~/.codex/AGENTS.md plus every AGENTS.md from the project root to the cwd.` : `Rules block upserted in ${paths.rules}; Codex reads every AGENTS.md from the project root to the cwd.`);
|
|
3871
4275
|
}
|
|
3872
|
-
return
|
|
4276
|
+
return results;
|
|
3873
4277
|
}
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
4278
|
+
function optionalText2(value) {
|
|
4279
|
+
if (typeof value !== "string")
|
|
4280
|
+
return;
|
|
4281
|
+
const trimmed = value.trim();
|
|
4282
|
+
return trimmed ? trimmed : undefined;
|
|
4283
|
+
}
|
|
4284
|
+
var init_search = __esm(() => {
|
|
4285
|
+
init_domains();
|
|
4286
|
+
init_format2();
|
|
4287
|
+
init_url();
|
|
3884
4288
|
});
|
|
3885
4289
|
|
|
3886
|
-
// src/
|
|
3887
|
-
function
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
}
|
|
3901
|
-
const notes = [...header.notes];
|
|
3902
|
-
const errors = [...header.errors];
|
|
3903
|
-
const edits = [];
|
|
3904
|
-
if (input.search === "preferred") {
|
|
3905
|
-
errors.push({
|
|
3906
|
-
message: "OpenCode search has no preferred tier: the websearch provider is host-level config, so the model never " + "chooses between engines. Use --search optional (provider registered, built-in stays default), " + "--search forced (replace the engine), or --search off."
|
|
3907
|
-
});
|
|
3908
|
-
}
|
|
3909
|
-
const plugins = Array.isArray(loaded.value.plugins) ? loaded.value.plugins : [];
|
|
3910
|
-
const trackedPackages = new Set(appendedByPrevious(previous, ["plugins"]).filter((entry) => isPlainObject(entry) && typeof entry.package === "string").map((entry) => entry.package));
|
|
3911
|
-
const pluginPackages = plugins.filter((entry) => isPlainObject(entry) && typeof entry.package === "string");
|
|
3912
|
-
const excused = previous === undefined ? new Set : new Set([...trackedPackages, input.packageRef]);
|
|
3913
|
-
const foreignOwn = pluginPackages.filter((entry) => looksLikeOwnPackage(entry.package) && !excused.has(entry.package));
|
|
3914
|
-
if (foreignOwn.length > 0) {
|
|
3915
|
-
errors.push({
|
|
3916
|
-
message: `${paths.config} already has a plugins entry referencing deepseek-web-retrieval that this installer did not ` + "write. Remove it (or run uninstall if it came from a previous init), then retry."
|
|
3917
|
-
});
|
|
3918
|
-
}
|
|
3919
|
-
if (previous !== undefined && pluginPackages.some((entry) => excused.has(entry.package))) {
|
|
3920
|
-
notes.push("A previous init wrote the plugins entry; re-applying keeps it the only entry (a changed package ref or tier " + "options replace it, an unchanged one is a no-op) and the original snapshots carry over.");
|
|
3921
|
-
}
|
|
3922
|
-
if (errors.length > 0) {
|
|
3923
|
-
return { host: "opencode", scope: input.scope, tiers, edits, notes, errors };
|
|
3924
|
-
}
|
|
3925
|
-
const options = {};
|
|
3926
|
-
if (input.search === "forced")
|
|
3927
|
-
options.setDefaultWebsearch = true;
|
|
3928
|
-
if (input.search === "off")
|
|
3929
|
-
options.searchEnabled = false;
|
|
3930
|
-
if (input.fetch === "forced")
|
|
3931
|
-
options.fetchOverrideWebfetch = true;
|
|
3932
|
-
if (input.fetch === "off")
|
|
3933
|
-
options.fetchEnabled = false;
|
|
3934
|
-
const ops = [
|
|
3935
|
-
{
|
|
3936
|
-
op: "append-array",
|
|
3937
|
-
path: ["plugins"],
|
|
3938
|
-
entries: [{ package: input.packageRef, options }],
|
|
3939
|
-
existingCount: plugins.length
|
|
4290
|
+
// src/core/transport.ts
|
|
4291
|
+
async function postJson(endpoint, body, operation2, opts) {
|
|
4292
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
4293
|
+
const attempts = opts.retry.maxRetries + 1;
|
|
4294
|
+
let lastError;
|
|
4295
|
+
for (let attempt = 0;attempt < attempts; attempt++) {
|
|
4296
|
+
try {
|
|
4297
|
+
return await requestOnce(endpoint, body, operation2, opts, fetchImpl);
|
|
4298
|
+
} catch (error) {
|
|
4299
|
+
lastError = error;
|
|
4300
|
+
if (!(error instanceof DeepSeekApiError) || !error.retryable || attempt === attempts - 1) {
|
|
4301
|
+
throw error;
|
|
4302
|
+
}
|
|
4303
|
+
await sleep(retryDelay(error.retryAfterMs, opts.retry.baseDelayMs, attempt), opts.signal);
|
|
3940
4304
|
}
|
|
3941
|
-
];
|
|
3942
|
-
const permissions = [];
|
|
3943
|
-
if (input.search !== "off")
|
|
3944
|
-
permissions.push({ action: "websearch", resource: "*", effect: "allow" });
|
|
3945
|
-
if (input.fetch !== "off")
|
|
3946
|
-
permissions.push({ action: "deepseek_fetch", resource: "*", effect: "allow" });
|
|
3947
|
-
if (input.fetch === "forced")
|
|
3948
|
-
permissions.push({ action: "webfetch", resource: "*", effect: "allow" });
|
|
3949
|
-
const existingPermissions = Array.isArray(loaded.value.permissions) ? loaded.value.permissions.length : 0;
|
|
3950
|
-
if (permissions.length > 0) {
|
|
3951
|
-
ops.push({ op: "append-array", path: ["permissions"], entries: permissions, existingCount: existingPermissions });
|
|
3952
|
-
}
|
|
3953
|
-
if (input.search === "forced") {
|
|
3954
|
-
const currentProvider = isPlainObject(loaded.value.websearch) ? loaded.value.websearch.provider : undefined;
|
|
3955
|
-
ops.push({ op: "set", path: ["websearch", "provider"], value: "deepseek", previous: currentProvider });
|
|
3956
|
-
notes.push(currentProvider === undefined ? "websearch.provider is being set to deepseek (no previous value existed)." : `websearch.provider is being set to deepseek; the previous value "${String(currentProvider)}" is snapshotted for uninstall.`);
|
|
3957
|
-
}
|
|
3958
|
-
edits.push({ file: paths.config, format: "json", ops });
|
|
3959
|
-
notes.push("The plugin entry uses the object form so options are honored; auto-discovered plugins cannot receive options.");
|
|
3960
|
-
if (input.search === "optional") {
|
|
3961
|
-
notes.push("Search tier optional: the deepseek provider is registered but the built-in provider stays the default engine; " + "set websearch.provider to deepseek (or rerun with --search forced) to route search through DeepSeek.");
|
|
3962
|
-
}
|
|
3963
|
-
if (input.search === "forced" || input.fetch === "forced") {
|
|
3964
|
-
notes.push(`Forced tier replaces built-in routing. ${FORCED_COST_NOTE}`);
|
|
3965
|
-
}
|
|
3966
|
-
if (input.fetch === "preferred") {
|
|
3967
|
-
edits.push({
|
|
3968
|
-
file: paths.rules,
|
|
3969
|
-
format: "rules",
|
|
3970
|
-
block: buildRulesBlock(input.host, rulesLines(OPENCODE_TOOL_IDS, input.search, input.fetch))
|
|
3971
|
-
});
|
|
3972
|
-
notes.push(`Rules block upserted in ${paths.rules}; OpenCode loads AGENTS.md only (no CLAUDE.md fallback in v2).`);
|
|
3973
4305
|
}
|
|
3974
|
-
|
|
3975
|
-
}
|
|
3976
|
-
var OPENCODE_TOOL_IDS;
|
|
3977
|
-
var init_opencode = __esm(() => {
|
|
3978
|
-
init_detect();
|
|
3979
|
-
init_formats();
|
|
3980
|
-
init_rules();
|
|
3981
|
-
init_value();
|
|
3982
|
-
init_paths();
|
|
3983
|
-
init_rulesContent();
|
|
3984
|
-
init_state();
|
|
3985
|
-
init_shared();
|
|
3986
|
-
OPENCODE_TOOL_IDS = { search: "websearch (provider deepseek)", fetch: "deepseek_fetch" };
|
|
3987
|
-
});
|
|
3988
|
-
|
|
3989
|
-
// src/pi/settings.ts
|
|
3990
|
-
var SETTINGS_NAMESPACE = "deepseekWebRetrieval";
|
|
3991
|
-
var init_settings = () => {};
|
|
3992
|
-
|
|
3993
|
-
// src/installer/hosts/pi.ts
|
|
3994
|
-
function piPaths(input) {
|
|
3995
|
-
const mcp = input.scope === "global" ? join2(input.home, ".pi/agent/mcp.json") : join2(input.cwd, ".pi/mcp.json");
|
|
3996
|
-
const settings = input.scope === "global" ? join2(input.home, ".pi/agent/settings.json") : join2(input.cwd, ".pi/settings.json");
|
|
3997
|
-
const rules = input.scope === "global" ? join2(input.home, ".pi/agent/AGENTS.md") : join2(input.cwd, "AGENTS.md");
|
|
3998
|
-
return { config: input.path === "mcp" ? mcp : settings, rules };
|
|
3999
|
-
}
|
|
4000
|
-
function piSourceRef(packageRef) {
|
|
4001
|
-
return packageRef.startsWith("npm:") ? packageRef : `npm:${packageRef}`;
|
|
4002
|
-
}
|
|
4003
|
-
function planPi(input, previous) {
|
|
4004
|
-
return input.path === "mcp" ? planPiMcp(input) : planPiExtension(input, previous);
|
|
4306
|
+
throw lastError;
|
|
4005
4307
|
}
|
|
4006
|
-
function
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
const foreignOwn = sources.filter((entry) => looksLikeOwnPackage(entry) && !excused.has(entry));
|
|
4023
|
-
if (foreignOwn.length > 0) {
|
|
4024
|
-
errors.push({
|
|
4025
|
-
message: `${paths.config} already lists ${foreignOwn.join(", ")} — this package (or a fork of it) that this installer did not ` + "write. Remove it (pi remove <source>, or uninstall if it came from a previous init), then retry."
|
|
4026
|
-
});
|
|
4027
|
-
}
|
|
4028
|
-
if (previous !== undefined && sources.some((entry) => excused.has(entry))) {
|
|
4029
|
-
notes.push("A previous init wrote the packages entry; re-applying reuses it (an unchanged source deduplicates, a changed " + "--package ref replaces the old source) and the original snapshots carry over.");
|
|
4030
|
-
}
|
|
4031
|
-
const forced = input.search === "forced" || input.fetch === "forced";
|
|
4032
|
-
if (forced) {
|
|
4033
|
-
const ownSources = new Set([...trackedSources, source]);
|
|
4034
|
-
const unexcludable = [...collectSources(loaded.value.packages), ...collectSources(loaded.value.extensions)].filter((entry) => !ownSources.has(entry) && looksWebCapable(entry, undefined));
|
|
4035
|
-
if (unexcludable.length > 0) {
|
|
4036
|
-
errors.push({
|
|
4037
|
-
message: `Forced cannot deliver exclusive routing: settings sources ${unexcludable.join(", ")} look web-capable, ` + "and Pi offers no per-package kill switch. Remove them (pi remove <source>) or choose a lower tier."
|
|
4308
|
+
async function requestOnce(endpoint, body, operation2, opts, fetchImpl) {
|
|
4309
|
+
return fetchWithTimeout(endpoint, {
|
|
4310
|
+
method: "POST",
|
|
4311
|
+
headers: {
|
|
4312
|
+
Authorization: `Bearer ${opts.auth.apiKey}`,
|
|
4313
|
+
"Content-Type": "application/json"
|
|
4314
|
+
},
|
|
4315
|
+
body: JSON.stringify(body)
|
|
4316
|
+
}, opts.signal, opts.timeoutMs, operation2, fetchImpl, async (response) => {
|
|
4317
|
+
if (!response.ok) {
|
|
4318
|
+
const text2 = await readBoundedText(response, MAX_ERROR_BODY_CHARS);
|
|
4319
|
+
throw new DeepSeekApiError({
|
|
4320
|
+
operation: operation2,
|
|
4321
|
+
status: response.status,
|
|
4322
|
+
body: text2,
|
|
4323
|
+
retryAfterMs: parseRetryAfter(response.headers.get("retry-after"))
|
|
4038
4324
|
});
|
|
4039
4325
|
}
|
|
4326
|
+
const text = await readResponseText(response, MAX_RESPONSE_BYTES, operation2);
|
|
4327
|
+
if (!text.trim())
|
|
4328
|
+
throw new Error(`${operation2} returned an empty response body`);
|
|
4329
|
+
try {
|
|
4330
|
+
return JSON.parse(text);
|
|
4331
|
+
} catch {
|
|
4332
|
+
throw new Error(`${operation2} returned invalid JSON: ${truncate(redactSecrets(text), 500)}`);
|
|
4333
|
+
}
|
|
4334
|
+
});
|
|
4335
|
+
}
|
|
4336
|
+
async function fetchWithTimeout(endpoint, init, signal, timeoutMs, operation2, fetchImpl, handleResponse) {
|
|
4337
|
+
const controller = new AbortController;
|
|
4338
|
+
let timedOut = false;
|
|
4339
|
+
const forwardAbort = () => controller.abort(signal.reason);
|
|
4340
|
+
if (signal.aborted)
|
|
4341
|
+
forwardAbort();
|
|
4342
|
+
else
|
|
4343
|
+
signal.addEventListener("abort", forwardAbort, { once: true });
|
|
4344
|
+
const timer = signal.aborted ? undefined : setTimeout(() => {
|
|
4345
|
+
timedOut = true;
|
|
4346
|
+
controller.abort(new Error("timeout"));
|
|
4347
|
+
}, timeoutMs);
|
|
4348
|
+
try {
|
|
4349
|
+
const response = await fetchImpl(endpoint, { ...init, signal: controller.signal });
|
|
4350
|
+
return await handleResponse(response);
|
|
4351
|
+
} catch (error) {
|
|
4352
|
+
if (timedOut)
|
|
4353
|
+
throw new Error(formatTimeoutError(operation2, timeoutMs));
|
|
4354
|
+
if (signal.aborted)
|
|
4355
|
+
throw abortReason(signal, operation2);
|
|
4356
|
+
throw error;
|
|
4357
|
+
} finally {
|
|
4358
|
+
if (timer !== undefined)
|
|
4359
|
+
clearTimeout(timer);
|
|
4360
|
+
signal.removeEventListener("abort", forwardAbort);
|
|
4040
4361
|
}
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
if (
|
|
4049
|
-
|
|
4050
|
-
op: "set",
|
|
4051
|
-
path: [SETTINGS_NAMESPACE, "enableSearch"],
|
|
4052
|
-
value: false,
|
|
4053
|
-
previous: typeof namespace.enableSearch === "boolean" ? namespace.enableSearch : undefined
|
|
4054
|
-
});
|
|
4055
|
-
}
|
|
4056
|
-
if (input.fetch === "off") {
|
|
4057
|
-
ops.push({
|
|
4058
|
-
op: "set",
|
|
4059
|
-
path: [SETTINGS_NAMESPACE, "enableFetch"],
|
|
4060
|
-
value: false,
|
|
4061
|
-
previous: typeof namespace.enableFetch === "boolean" ? namespace.enableFetch : undefined
|
|
4062
|
-
});
|
|
4063
|
-
}
|
|
4064
|
-
edits.push({ file: paths.config, format: "json", ops });
|
|
4065
|
-
notes.push("Pi resolves the packages source at session start; the first launch after apply fetches the package from npm " + "(pi update --extensions refreshes it later).");
|
|
4066
|
-
notes.push(`The extension reads ${API_KEY_ENV} from the Pi process environment; no env forwarding is needed. ` + "Set it in the shell that launches Pi.");
|
|
4067
|
-
if (input.search === "off" || input.fetch === "off") {
|
|
4068
|
-
notes.push(`Off capability switches are written into the "${SETTINGS_NAMESPACE}" settings namespace; the extension ` + "reads them at load (they outrank the DEEPSEEK_ENABLE_* environment variables).");
|
|
4069
|
-
}
|
|
4070
|
-
notes.push(`Extension tools are declared to the model with exposure "direct" by Pi's own default; no exposure key is needed.`);
|
|
4071
|
-
if (forced) {
|
|
4072
|
-
edits.push(...planMcpCompetitorDisables(input, notes, errors));
|
|
4073
|
-
}
|
|
4074
|
-
if (errors.length > 0) {
|
|
4075
|
-
return { host: "pi", scope: input.scope, tiers, edits: [], notes, errors };
|
|
4076
|
-
}
|
|
4077
|
-
if (input.search === "preferred" || input.fetch === "preferred") {
|
|
4078
|
-
edits.push({
|
|
4079
|
-
file: paths.rules,
|
|
4080
|
-
format: "rules",
|
|
4081
|
-
block: buildRulesBlock(input.host, rulesLines(RAW_MCP_TOOL_IDS, input.search, input.fetch))
|
|
4082
|
-
});
|
|
4083
|
-
notes.push(`Rules block upserted in ${paths.rules}; Pi loads AGENTS.md context files from the working directory and its parents.`);
|
|
4084
|
-
}
|
|
4085
|
-
if (forced) {
|
|
4086
|
-
notes.push("Forced tier: Pi 1.3.0 ships no built-in web tool, so the registered tools are the only web path (de-facto). " + FORCED_COST_NOTE);
|
|
4362
|
+
}
|
|
4363
|
+
function parseRetryAfter(value) {
|
|
4364
|
+
if (!value)
|
|
4365
|
+
return;
|
|
4366
|
+
const trimmed = value.trim();
|
|
4367
|
+
if (!trimmed)
|
|
4368
|
+
return;
|
|
4369
|
+
if (/^\d+$/.test(trimmed)) {
|
|
4370
|
+
return Math.min(MAX_RETRY_DELAY_MS, Number(trimmed) * 1000);
|
|
4087
4371
|
}
|
|
4088
|
-
|
|
4372
|
+
const timestamp = Date.parse(trimmed);
|
|
4373
|
+
if (!Number.isFinite(timestamp))
|
|
4374
|
+
return;
|
|
4375
|
+
return Math.min(MAX_RETRY_DELAY_MS, Math.max(0, timestamp - Date.now()));
|
|
4089
4376
|
}
|
|
4090
|
-
function
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4377
|
+
function retryDelay(retryAfterMs, baseDelayMs, attempt) {
|
|
4378
|
+
if (retryAfterMs !== undefined)
|
|
4379
|
+
return retryAfterMs;
|
|
4380
|
+
return Math.min(MAX_RETRY_DELAY_MS, baseDelayMs * 2 ** attempt);
|
|
4381
|
+
}
|
|
4382
|
+
function sleep(ms, signal) {
|
|
4383
|
+
if (ms <= 0)
|
|
4384
|
+
return Promise.resolve();
|
|
4385
|
+
return new Promise((resolve2, reject) => {
|
|
4386
|
+
const timer = setTimeout(() => {
|
|
4387
|
+
signal.removeEventListener("abort", onAbort);
|
|
4388
|
+
resolve2();
|
|
4389
|
+
}, ms);
|
|
4390
|
+
const onAbort = () => {
|
|
4391
|
+
clearTimeout(timer);
|
|
4392
|
+
reject(abortReason(signal, "DeepSeek request"));
|
|
4393
|
+
};
|
|
4394
|
+
if (signal.aborted) {
|
|
4395
|
+
clearTimeout(timer);
|
|
4396
|
+
reject(abortReason(signal, "DeepSeek request"));
|
|
4397
|
+
return;
|
|
4099
4398
|
}
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
if (
|
|
4108
|
-
|
|
4109
|
-
|
|
4399
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
4400
|
+
});
|
|
4401
|
+
}
|
|
4402
|
+
function abortReason(signal, operation2) {
|
|
4403
|
+
return signal.reason instanceof Error ? signal.reason : new Error(`${operation2} request aborted`);
|
|
4404
|
+
}
|
|
4405
|
+
async function readBoundedText(response, maxChars) {
|
|
4406
|
+
if (!response.body)
|
|
4407
|
+
return truncate(await response.text(), maxChars + 1);
|
|
4408
|
+
const reader = response.body.getReader();
|
|
4409
|
+
const decoder = new TextDecoder;
|
|
4410
|
+
const limit = maxChars + 1;
|
|
4411
|
+
let text = "";
|
|
4412
|
+
let chars = 0;
|
|
4413
|
+
try {
|
|
4414
|
+
while (chars < limit) {
|
|
4415
|
+
const { done, value } = await reader.read();
|
|
4416
|
+
if (done)
|
|
4417
|
+
break;
|
|
4418
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
4419
|
+
text += chunk;
|
|
4420
|
+
chars += Array.from(chunk).length;
|
|
4421
|
+
}
|
|
4422
|
+
text += decoder.decode();
|
|
4423
|
+
} finally {
|
|
4424
|
+
await reader.cancel().catch(() => {
|
|
4425
|
+
return;
|
|
4110
4426
|
});
|
|
4427
|
+
reader.releaseLock();
|
|
4111
4428
|
}
|
|
4112
|
-
return
|
|
4429
|
+
return truncate(text, limit);
|
|
4113
4430
|
}
|
|
4114
|
-
function
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4431
|
+
async function readResponseText(response, maxBytes, operation2) {
|
|
4432
|
+
if (!response.body) {
|
|
4433
|
+
const text2 = await response.text();
|
|
4434
|
+
if (new TextEncoder().encode(text2).byteLength > maxBytes) {
|
|
4435
|
+
throw new Error(`${operation2} response exceeded ${maxBytes} bytes`);
|
|
4436
|
+
}
|
|
4437
|
+
return text2;
|
|
4120
4438
|
}
|
|
4121
|
-
const
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4439
|
+
const reader = response.body.getReader();
|
|
4440
|
+
const decoder = new TextDecoder;
|
|
4441
|
+
let text = "";
|
|
4442
|
+
let bytes = 0;
|
|
4443
|
+
try {
|
|
4444
|
+
while (true) {
|
|
4445
|
+
const { done, value } = await reader.read();
|
|
4446
|
+
if (done)
|
|
4447
|
+
break;
|
|
4448
|
+
bytes += value.byteLength;
|
|
4449
|
+
if (bytes > maxBytes) {
|
|
4450
|
+
throw new Error(`${operation2} response exceeded ${maxBytes} bytes`);
|
|
4451
|
+
}
|
|
4452
|
+
text += decoder.decode(value, { stream: true });
|
|
4453
|
+
}
|
|
4454
|
+
text += decoder.decode();
|
|
4455
|
+
} finally {
|
|
4456
|
+
await reader.cancel().catch(() => {
|
|
4457
|
+
return;
|
|
4458
|
+
});
|
|
4459
|
+
reader.releaseLock();
|
|
4130
4460
|
}
|
|
4131
|
-
|
|
4132
|
-
notes.push(othersStayRegisteredNote(bystanders));
|
|
4133
|
-
return edits;
|
|
4461
|
+
return text;
|
|
4134
4462
|
}
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
env,
|
|
4155
|
-
exposure: "direct",
|
|
4156
|
-
enabled: true
|
|
4157
|
-
};
|
|
4158
|
-
const servers = loaded.value.mcpServers;
|
|
4159
|
-
const existingEntry = isPlainObject(servers) ? servers.deepseek : undefined;
|
|
4160
|
-
const ops = [{ op: "set", path: ["mcpServers", "deepseek"], value: entry, previous: existingEntry }];
|
|
4161
|
-
edits.push({ file: paths.config, format: "json", ops });
|
|
4162
|
-
if (isPlainObject(servers)) {
|
|
4163
|
-
if (servers.deepseek !== undefined)
|
|
4164
|
-
notes.push(replacingExistingEntryNote("mcpServers.deepseek"));
|
|
4165
|
-
const { competitors, bystanders } = splitCompetitors(servers);
|
|
4166
|
-
const forced = input.search === "forced" || input.fetch === "forced";
|
|
4167
|
-
if (forced) {
|
|
4168
|
-
ops.push(...planCompetitorDisables(servers, competitors, notes, errors));
|
|
4463
|
+
var DeepSeekApiError;
|
|
4464
|
+
var init_transport = __esm(() => {
|
|
4465
|
+
init_constants();
|
|
4466
|
+
init_format2();
|
|
4467
|
+
DeepSeekApiError = class DeepSeekApiError extends Error {
|
|
4468
|
+
operation;
|
|
4469
|
+
status;
|
|
4470
|
+
body;
|
|
4471
|
+
retryable;
|
|
4472
|
+
retryAfterMs;
|
|
4473
|
+
constructor(input) {
|
|
4474
|
+
const body = redactSecrets(input.body);
|
|
4475
|
+
super(formatApiError(input.operation, input.status, body));
|
|
4476
|
+
this.name = "DeepSeekApiError";
|
|
4477
|
+
this.operation = input.operation;
|
|
4478
|
+
this.status = input.status;
|
|
4479
|
+
this.body = body;
|
|
4480
|
+
this.retryable = input.status === 408 || input.status === 425 || input.status === 429 || input.status >= 500 && input.status <= 599;
|
|
4481
|
+
this.retryAfterMs = input.retryAfterMs;
|
|
4169
4482
|
}
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4483
|
+
};
|
|
4484
|
+
});
|
|
4485
|
+
|
|
4486
|
+
// src/core/client.ts
|
|
4487
|
+
function effectiveTimeout(configuredMs, perCallMs) {
|
|
4488
|
+
return perCallMs === undefined ? configuredMs : Math.min(configuredMs, perCallMs);
|
|
4489
|
+
}
|
|
4490
|
+
function oriQuestionValue(oriQuestion) {
|
|
4491
|
+
const trimmed = oriQuestion?.trim();
|
|
4492
|
+
return trimmed ? truncate(trimmed, MAX_ORI_QUESTION_CHARS) : undefined;
|
|
4493
|
+
}
|
|
4494
|
+
async function searchWeb(query, options, request) {
|
|
4495
|
+
const trimmed = query.trim();
|
|
4496
|
+
if (!trimmed)
|
|
4497
|
+
throw new Error("DeepSeek search query must not be empty");
|
|
4498
|
+
if (Array.from(trimmed).length > MAX_QUERY_CHARS) {
|
|
4499
|
+
throw new Error(`DeepSeek search query must be ${MAX_QUERY_CHARS} characters or fewer`);
|
|
4173
4500
|
}
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
if (
|
|
4177
|
-
|
|
4178
|
-
file: paths.rules,
|
|
4179
|
-
format: "rules",
|
|
4180
|
-
block: buildRulesBlock(input.host, rulesLines(MCP_TOOL_IDS, input.search, input.fetch))
|
|
4181
|
-
});
|
|
4182
|
-
notes.push(`Rules block upserted in ${paths.rules}; Pi loads AGENTS.md context files from the working directory and its parents.`);
|
|
4501
|
+
const builtQuery = buildQuery(trimmed, options);
|
|
4502
|
+
const builtQueryLength = Array.from(builtQuery).length;
|
|
4503
|
+
if (builtQueryLength > MAX_QUERY_CHARS) {
|
|
4504
|
+
throw new Error(`DeepSeek search query must be ${MAX_QUERY_CHARS} characters or fewer after applying ` + `blockedDomains, includeDomains, and language ` + `(built query is ${builtQueryLength} characters)`);
|
|
4183
4505
|
}
|
|
4184
|
-
|
|
4185
|
-
|
|
4506
|
+
const timeout = effectiveTimeout(options.timeoutMs, request.timeoutMs);
|
|
4507
|
+
const oriQuestion = oriQuestionValue(request.oriQuestion);
|
|
4508
|
+
const payload = await postJson(SEARCH_ENDPOINT, {
|
|
4509
|
+
query: builtQuery,
|
|
4510
|
+
count: options.count,
|
|
4511
|
+
need_content: false,
|
|
4512
|
+
...oriQuestion === undefined ? {} : { ori_question: oriQuestion },
|
|
4513
|
+
...options.localization === undefined ? {} : { localization: options.localization }
|
|
4514
|
+
}, "DeepSeek Search", { ...request, timeoutMs: timeout });
|
|
4515
|
+
if (!Array.isArray(payload.results)) {
|
|
4516
|
+
throw new Error("DeepSeek Search returned a response without a results array");
|
|
4186
4517
|
}
|
|
4187
|
-
return
|
|
4188
|
-
}
|
|
4189
|
-
function collectSources(value) {
|
|
4190
|
-
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
4518
|
+
return processResults(payload.results, options);
|
|
4191
4519
|
}
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
// src/installer/hosts/index.ts
|
|
4206
|
-
function planForHost(host, input) {
|
|
4207
|
-
try {
|
|
4208
|
-
const previous = readRecord(input);
|
|
4209
|
-
const planned = withCrossScopeGuard(HOST_PLANNERS[host](input, previous), input);
|
|
4210
|
-
return {
|
|
4211
|
-
...withSecretGuard(withOrphanReverts(withLegacySharedConfigGuard(planned, previous), previous)),
|
|
4212
|
-
previousRecord: previous
|
|
4213
|
-
};
|
|
4214
|
-
} catch (error) {
|
|
4215
|
-
if (!(error instanceof InstallRecordError))
|
|
4216
|
-
throw error;
|
|
4217
|
-
return {
|
|
4218
|
-
host,
|
|
4219
|
-
scope: input.scope,
|
|
4220
|
-
tiers: { search: input.search, fetch: input.fetch },
|
|
4221
|
-
edits: [],
|
|
4222
|
-
notes: [],
|
|
4223
|
-
errors: [{ message: error.message }]
|
|
4224
|
-
};
|
|
4520
|
+
async function fetchPage(url, options, request) {
|
|
4521
|
+
const target = parseFetchUrl(url.href);
|
|
4522
|
+
assertFetchHostPolicy(target, options);
|
|
4523
|
+
const timeout = effectiveTimeout(options.timeoutMs, request.timeoutMs);
|
|
4524
|
+
const payload = await postJson(FETCH_ENDPOINT, { url: target.href }, "DeepSeek Fetch", {
|
|
4525
|
+
...request,
|
|
4526
|
+
timeoutMs: timeout
|
|
4527
|
+
});
|
|
4528
|
+
const title = typeof payload.title === "string" ? payload.title.trim() : "";
|
|
4529
|
+
const content = typeof payload.content === "string" ? payload.content : "";
|
|
4530
|
+
if (!content.trim()) {
|
|
4531
|
+
throw new Error("DeepSeek Fetch returned no page content (the page may be empty or unavailable)");
|
|
4225
4532
|
}
|
|
4533
|
+
return { url: target.href, title, content };
|
|
4226
4534
|
}
|
|
4227
|
-
function
|
|
4228
|
-
if (
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
} catch (error) {
|
|
4233
|
-
return { ...plan, edits: [], reverts: undefined, errors: [{ message: String(error) }] };
|
|
4234
|
-
}
|
|
4235
|
-
const other = input.scope === "global" ? "project" : "global";
|
|
4236
|
-
const overlap = claimedByOtherScope(input, plan.edits.map((edit2) => edit2.file));
|
|
4237
|
-
if (overlap.length === 0)
|
|
4238
|
-
return { ...plan, notes: [...plan.notes, ...fileOwnershipNotes(input, plan.edits)] };
|
|
4535
|
+
async function fetchDocument(url, options, request) {
|
|
4536
|
+
if (typeof url !== "string" && !(url instanceof URL))
|
|
4537
|
+
throw new Error('"url" must be a string or URL');
|
|
4538
|
+
const page = await fetchPage(url instanceof URL ? url : parseFetchUrl(url), options, request);
|
|
4539
|
+
const formatted = formatFetchedContent(page.title, page.content, options.maxChars);
|
|
4239
4540
|
return {
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
{
|
|
4245
|
-
message: `A ${plan.host} install is already recorded at the ${other} scope and claims the same file(s): ${overlap.join(", ")}. ` + `Uninstall it first (deepseek-web-retrieval uninstall --host ${plan.host} --scope ${other}), ` + `or re-run with --scope ${other} to update it in place — two scopes must not edit one file.`
|
|
4246
|
-
}
|
|
4247
|
-
]
|
|
4541
|
+
url: page.url,
|
|
4542
|
+
title: page.title,
|
|
4543
|
+
content: formatted.content,
|
|
4544
|
+
truncated: formatted.truncated
|
|
4248
4545
|
};
|
|
4249
4546
|
}
|
|
4250
|
-
function
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
const old = op.previous;
|
|
4258
|
-
if (op.op !== "set" || !isPlainObject(old) || old.command !== "npx" || !Array.isArray(old.args) || old.args[0] !== "-y" || !Array.isArray(old.env_vars) || !old.env_vars.includes(API_KEY_ENV))
|
|
4259
|
-
continue;
|
|
4260
|
-
return {
|
|
4261
|
-
...plan,
|
|
4262
|
-
edits: [],
|
|
4263
|
-
reverts: undefined,
|
|
4264
|
-
errors: [
|
|
4265
|
-
{
|
|
4266
|
-
message: `${edit2.file} contains an installer-style registration without an install record for this location. ` + "It may belong to an older install in another project. Re-run init or uninstall from that original project and scope first; " + "if the registration was created manually, remove it before retrying. No existing registration was adopted."
|
|
4267
|
-
}
|
|
4268
|
-
]
|
|
4269
|
-
};
|
|
4270
|
-
}
|
|
4271
|
-
}
|
|
4272
|
-
return plan;
|
|
4547
|
+
function createRetrievalClient(dependencies = {}) {
|
|
4548
|
+
const bind = (request) => ({ ...request, ...dependencies });
|
|
4549
|
+
return {
|
|
4550
|
+
searchWeb: (query, options, request) => searchWeb(query, options, bind(request)),
|
|
4551
|
+
fetchPage: (url, options, request) => fetchPage(url, options, bind(request)),
|
|
4552
|
+
fetchDocument: (url, options, request) => fetchDocument(url, options, bind(request))
|
|
4553
|
+
};
|
|
4273
4554
|
}
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4555
|
+
var retrievalClient;
|
|
4556
|
+
var init_client = __esm(() => {
|
|
4557
|
+
init_constants();
|
|
4558
|
+
init_domains();
|
|
4559
|
+
init_format2();
|
|
4560
|
+
init_search();
|
|
4561
|
+
init_transport();
|
|
4562
|
+
init_url();
|
|
4563
|
+
init_transport();
|
|
4564
|
+
retrievalClient = createRetrievalClient();
|
|
4565
|
+
});
|
|
4566
|
+
|
|
4567
|
+
// src/core/env.ts
|
|
4568
|
+
function resolveEnvOptions(env = process.env, fileOptions) {
|
|
4569
|
+
return resolvePluginOptions({ ...fileOptions, ...pluginOptionsFromEnv(env) });
|
|
4570
|
+
}
|
|
4571
|
+
function envOptionsReport(env = process.env) {
|
|
4572
|
+
const options = {};
|
|
4573
|
+
const warnings = [];
|
|
4574
|
+
putNumber(options, "count", env, warnings);
|
|
4575
|
+
putNumber(options, "maxResults", env, warnings);
|
|
4576
|
+
putNumber(options, "minRerank", env, warnings);
|
|
4577
|
+
putNumber(options, "minAuthority", env, warnings);
|
|
4578
|
+
putFalseOnly(options, "allowUnknownAuthority", env, warnings);
|
|
4579
|
+
putDomains(options, "blockedDomains", env, warnings);
|
|
4580
|
+
putDomains(options, "includeDomains", env, warnings);
|
|
4581
|
+
putText(options, "language", env);
|
|
4582
|
+
putText(options, "searchLocalization", env);
|
|
4583
|
+
putNumber(options, "maxSummaryChars", env, warnings);
|
|
4584
|
+
putNumber(options, "searchTimeoutMs", env, warnings);
|
|
4585
|
+
putNumber(options, "fetchMaxChars", env, warnings);
|
|
4586
|
+
putDomains(options, "fetchAllowedDomains", env, warnings);
|
|
4587
|
+
putDomains(options, "fetchBlockedDomains", env, warnings);
|
|
4588
|
+
putNumber(options, "fetchTimeoutMs", env, warnings);
|
|
4589
|
+
putFalseOnly(options, "fetchBlockPrivateHosts", env, warnings);
|
|
4590
|
+
putFalseOnly(options, "searchEnabled", env, warnings);
|
|
4591
|
+
putFalseOnly(options, "fetchEnabled", env, warnings);
|
|
4592
|
+
putNumber(options, "maxRetries", env, warnings);
|
|
4593
|
+
putNumber(options, "retryBaseDelayMs", env, warnings);
|
|
4594
|
+
const known = new Set(OPTION_ENV_VARS);
|
|
4595
|
+
for (const key of Object.keys(env)) {
|
|
4596
|
+
if (!key.startsWith("DEEPSEEK_") || key === API_KEY_ENV || known.has(key))
|
|
4277
4597
|
continue;
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
const offender = findSecretLike(op.previous, op.path.join("."));
|
|
4282
|
-
if (offender !== undefined) {
|
|
4283
|
-
return {
|
|
4284
|
-
...plan,
|
|
4285
|
-
edits: [],
|
|
4286
|
-
reverts: undefined,
|
|
4287
|
-
errors: [
|
|
4288
|
-
{
|
|
4289
|
-
message: `${edit2.file}: the existing value at ${op.path.join(".")} contains a credential-looking key ` + `(${offender}). The installer refuses to snapshot secrets into its state file. Move the key to ` + `environment indirection ("\${${API_KEY_ENV}}" expansion or env_vars) and re-run.`
|
|
4290
|
-
}
|
|
4291
|
-
]
|
|
4292
|
-
};
|
|
4293
|
-
}
|
|
4294
|
-
}
|
|
4598
|
+
if (!(env[key] ?? "").trim())
|
|
4599
|
+
continue;
|
|
4600
|
+
warnings.push(`unknown DEEPSEEK_* variable ${key} — ignored`);
|
|
4295
4601
|
}
|
|
4296
|
-
return
|
|
4602
|
+
return { options, warnings };
|
|
4297
4603
|
}
|
|
4298
|
-
function
|
|
4299
|
-
|
|
4604
|
+
function pluginOptionsFromEnv(env) {
|
|
4605
|
+
return envOptionsReport(env).options;
|
|
4606
|
+
}
|
|
4607
|
+
function putNumber(target, key, env, warnings) {
|
|
4608
|
+
const value = env[NUMBER_ENV[key]];
|
|
4609
|
+
if (value === undefined)
|
|
4300
4610
|
return;
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
}
|
|
4310
|
-
return;
|
|
4611
|
+
const trimmed = value.trim();
|
|
4612
|
+
if (!trimmed)
|
|
4613
|
+
return;
|
|
4614
|
+
const parsed = Number(trimmed);
|
|
4615
|
+
if (Number.isFinite(parsed))
|
|
4616
|
+
target[key] = parsed;
|
|
4617
|
+
else
|
|
4618
|
+
warnings.push(`${NUMBER_ENV[key]} is not a number ("${trimmed}") — ignored`);
|
|
4311
4619
|
}
|
|
4312
|
-
function
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4620
|
+
function putText(target, key, env) {
|
|
4621
|
+
const value = env[TEXT_ENV[key]];
|
|
4622
|
+
if (value === undefined)
|
|
4623
|
+
return;
|
|
4624
|
+
const trimmed = value.trim();
|
|
4625
|
+
if (trimmed)
|
|
4626
|
+
target[key] = trimmed;
|
|
4627
|
+
}
|
|
4628
|
+
function putFalseOnly(target, key, env, warnings) {
|
|
4629
|
+
const name = FALSE_ONLY_ENV[key];
|
|
4630
|
+
const value = env[name];
|
|
4631
|
+
if (value === undefined || !value.trim())
|
|
4632
|
+
return;
|
|
4633
|
+
if (isFalseValue(value)) {
|
|
4634
|
+
target[key] = false;
|
|
4635
|
+
return;
|
|
4636
|
+
}
|
|
4637
|
+
warnings.push(`${name}="${value.trim()}" changes nothing — only false/0/no/off turns the switch off`);
|
|
4638
|
+
}
|
|
4639
|
+
function putDomains(target, key, env, warnings) {
|
|
4640
|
+
const name = DOMAINS_ENV[key];
|
|
4641
|
+
const value = env[name];
|
|
4642
|
+
if (value === undefined || !value.trim())
|
|
4643
|
+
return;
|
|
4644
|
+
const domains = value.split(",").map((domain) => domain.trim()).filter((domain) => domain.length > 0);
|
|
4645
|
+
for (const domain of domains) {
|
|
4646
|
+
if (normalizeDomain(domain) === undefined) {
|
|
4647
|
+
warnings.push(`${name} entry "${domain}" is not a domain — ignored`);
|
|
4323
4648
|
}
|
|
4324
|
-
if (edit2.format === "rules" || file.format === "rules")
|
|
4325
|
-
continue;
|
|
4326
|
-
const orphanOps = file.ops.flatMap((op) => {
|
|
4327
|
-
const claimed = edit2.ops.find((candidate) => candidate.op === op.op && candidate.path.join("\x00") === op.path.join("\x00"));
|
|
4328
|
-
if (op.op !== "append-array" || claimed === undefined || claimed.op !== "append-array") {
|
|
4329
|
-
return claimed === undefined ? [op] : [];
|
|
4330
|
-
}
|
|
4331
|
-
const dropped = (Array.isArray(op.applied) ? op.applied : []).filter((entry) => !(claimed.entries ?? []).some((wanted) => deepEqual(wanted, entry)));
|
|
4332
|
-
return dropped.length > 0 ? [{ ...op, applied: dropped }] : [];
|
|
4333
|
-
});
|
|
4334
|
-
if (orphanOps.length > 0)
|
|
4335
|
-
reverts.push({ ...file, ops: orphanOps });
|
|
4336
4649
|
}
|
|
4337
|
-
if (
|
|
4338
|
-
|
|
4339
|
-
|
|
4650
|
+
if (domains.length > 0)
|
|
4651
|
+
target[key] = domains;
|
|
4652
|
+
else
|
|
4653
|
+
warnings.push(`${name} contains no domains — ignored`);
|
|
4340
4654
|
}
|
|
4341
|
-
|
|
4342
|
-
|
|
4655
|
+
function isFalseValue(value) {
|
|
4656
|
+
const trimmed = value?.trim().toLowerCase();
|
|
4657
|
+
return trimmed === "false" || trimmed === "0" || trimmed === "no" || trimmed === "off";
|
|
4658
|
+
}
|
|
4659
|
+
var NUMBER_ENV, TEXT_ENV, DOMAINS_ENV, FALSE_ONLY_ENV, OPTION_ENV_BY_KEY, OPTION_ENV_VARS;
|
|
4660
|
+
var init_env = __esm(() => {
|
|
4343
4661
|
init_constants();
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4662
|
+
init_domains();
|
|
4663
|
+
init_options();
|
|
4664
|
+
NUMBER_ENV = {
|
|
4665
|
+
count: "DEEPSEEK_SEARCH_COUNT",
|
|
4666
|
+
maxResults: "DEEPSEEK_SEARCH_MAX_RESULTS",
|
|
4667
|
+
minRerank: "DEEPSEEK_SEARCH_MIN_RERANK",
|
|
4668
|
+
minAuthority: "DEEPSEEK_SEARCH_MIN_AUTHORITY",
|
|
4669
|
+
maxSummaryChars: "DEEPSEEK_SEARCH_MAX_SUMMARY_CHARS",
|
|
4670
|
+
searchTimeoutMs: "DEEPSEEK_SEARCH_TIMEOUT_MS",
|
|
4671
|
+
fetchMaxChars: "DEEPSEEK_FETCH_MAX_CHARS",
|
|
4672
|
+
fetchTimeoutMs: "DEEPSEEK_FETCH_TIMEOUT_MS",
|
|
4673
|
+
maxRetries: "DEEPSEEK_MAX_RETRIES",
|
|
4674
|
+
retryBaseDelayMs: "DEEPSEEK_RETRY_BASE_DELAY_MS"
|
|
4354
4675
|
};
|
|
4355
|
-
|
|
4356
|
-
|
|
4676
|
+
TEXT_ENV = {
|
|
4677
|
+
language: "DEEPSEEK_SEARCH_LANGUAGE",
|
|
4678
|
+
searchLocalization: SEARCH_LOCALIZATION_ENV
|
|
4679
|
+
};
|
|
4680
|
+
DOMAINS_ENV = {
|
|
4681
|
+
blockedDomains: "DEEPSEEK_SEARCH_BLOCKED_DOMAINS",
|
|
4682
|
+
includeDomains: "DEEPSEEK_SEARCH_INCLUDE_DOMAINS",
|
|
4683
|
+
fetchAllowedDomains: "DEEPSEEK_FETCH_ALLOWED_DOMAINS",
|
|
4684
|
+
fetchBlockedDomains: "DEEPSEEK_FETCH_BLOCKED_DOMAINS"
|
|
4685
|
+
};
|
|
4686
|
+
FALSE_ONLY_ENV = {
|
|
4687
|
+
allowUnknownAuthority: "DEEPSEEK_SEARCH_ALLOW_UNKNOWN_AUTHORITY",
|
|
4688
|
+
fetchBlockPrivateHosts: "DEEPSEEK_FETCH_BLOCK_PRIVATE_HOSTS",
|
|
4689
|
+
searchEnabled: ENABLE_SEARCH_ENV,
|
|
4690
|
+
fetchEnabled: ENABLE_FETCH_ENV
|
|
4691
|
+
};
|
|
4692
|
+
OPTION_ENV_BY_KEY = {
|
|
4693
|
+
...NUMBER_ENV,
|
|
4694
|
+
...TEXT_ENV,
|
|
4695
|
+
...DOMAINS_ENV,
|
|
4696
|
+
...FALSE_ONLY_ENV
|
|
4357
4697
|
};
|
|
4698
|
+
OPTION_ENV_VARS = Object.values(OPTION_ENV_BY_KEY);
|
|
4358
4699
|
});
|
|
4359
4700
|
|
|
4360
|
-
// src/installer/
|
|
4361
|
-
function
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
lines.push("");
|
|
4366
|
-
lines.push("Files:");
|
|
4367
|
-
for (const edit2 of plan.edits) {
|
|
4368
|
-
lines.push(` ${edit2.file}`);
|
|
4369
|
-
if (edit2.format === "rules") {
|
|
4370
|
-
lines.push(" upsert marked rules block");
|
|
4371
|
-
continue;
|
|
4372
|
-
}
|
|
4373
|
-
for (const op of edit2.ops) {
|
|
4374
|
-
if (op.op === "set") {
|
|
4375
|
-
const was = op.previous === undefined ? "(absent)" : preview(op.previous);
|
|
4376
|
-
lines.push(` set ${op.path.join(".")} = ${preview(op.value)} (was ${was})`);
|
|
4377
|
-
} else {
|
|
4378
|
-
const count = (op.entries ?? []).length;
|
|
4379
|
-
const kept = op.existingCount !== undefined ? ` (${op.existingCount} existing kept)` : "";
|
|
4380
|
-
lines.push(` append ${count} entr${count === 1 ? "y" : "ies"} to ${op.path.join(".")}${kept}`);
|
|
4381
|
-
for (const entry of op.entries ?? [])
|
|
4382
|
-
lines.push(` + ${preview(entry)}`);
|
|
4383
|
-
}
|
|
4384
|
-
}
|
|
4701
|
+
// src/installer/config-command.ts
|
|
4702
|
+
async function runConfigCommand(query, io, deps = {}) {
|
|
4703
|
+
if (query !== undefined && !query.trim()) {
|
|
4704
|
+
io.err('error: --search requires a query (for example: config --search "deepseek mcp")');
|
|
4705
|
+
return EXIT_USAGE;
|
|
4385
4706
|
}
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
} else {
|
|
4399
|
-
const count = Array.isArray(op.applied) ? op.applied.length : 0;
|
|
4400
|
-
lines.push(` remove ${count} appended entr${count === 1 ? "y" : "ies"} from ${op.path.join(".")}`);
|
|
4401
|
-
}
|
|
4402
|
-
}
|
|
4403
|
-
}
|
|
4707
|
+
const env = deps.env ?? process.env;
|
|
4708
|
+
const file = configFilePath({ env });
|
|
4709
|
+
const report = readFileOptionsReport(file);
|
|
4710
|
+
for (const warning of report.warnings)
|
|
4711
|
+
io.err(`warning: ${warning}`);
|
|
4712
|
+
const envReport = envOptionsReport(env);
|
|
4713
|
+
for (const warning of envReport.warnings)
|
|
4714
|
+
io.err(`warning: ${warning}`);
|
|
4715
|
+
const resolved = resolveEnvOptions(env, report.options);
|
|
4716
|
+
if (query === undefined) {
|
|
4717
|
+
printConfigReport(file, report.exists, report.options, envReport.options, resolved, io);
|
|
4718
|
+
return report.warnings.length > 0 || envReport.warnings.length > 0 ? EXIT_CONFIG_PROBLEMS : 0;
|
|
4404
4719
|
}
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4720
|
+
const apiKey = apiKeyFromEnv(env);
|
|
4721
|
+
if (!apiKey) {
|
|
4722
|
+
io.err(MISSING_API_KEY_MESSAGE);
|
|
4723
|
+
return EXIT_CONFIG_PROBLEMS;
|
|
4724
|
+
}
|
|
4725
|
+
const client = deps.client ?? createRetrievalClient();
|
|
4726
|
+
const startedAt = Date.now();
|
|
4727
|
+
try {
|
|
4728
|
+
const results = await client.searchWeb(query, resolved.search, {
|
|
4729
|
+
signal: new AbortController().signal,
|
|
4730
|
+
auth: { apiKey },
|
|
4731
|
+
retry: resolved.retry
|
|
4732
|
+
});
|
|
4733
|
+
const elapsedMs = Date.now() - startedAt;
|
|
4734
|
+
io.out(`search "${query.trim()}" — ${results.length} ${results.length === 1 ? "result" : "results"} in ${elapsedMs} ms`);
|
|
4735
|
+
io.out("");
|
|
4736
|
+
for (const line of formatSearchResults(results).split(`
|
|
4737
|
+
`))
|
|
4738
|
+
io.out(line);
|
|
4739
|
+
return 0;
|
|
4740
|
+
} catch (error) {
|
|
4741
|
+
io.err(error instanceof Error ? error.message : String(error));
|
|
4742
|
+
return EXIT_CONFIG_PROBLEMS;
|
|
4410
4743
|
}
|
|
4411
|
-
return lines.join(`
|
|
4412
|
-
`);
|
|
4413
4744
|
}
|
|
4414
|
-
function
|
|
4415
|
-
|
|
4416
|
-
|
|
4745
|
+
function printConfigReport(file, exists, fileOptions, envOptions, resolved, io) {
|
|
4746
|
+
io.out(`Config file: ${file} (${exists ? "exists" : "missing"})`);
|
|
4747
|
+
io.out("");
|
|
4748
|
+
const effective = configSummaryValues(resolved);
|
|
4749
|
+
const fileKeys = new Set(Object.keys(fileOptions));
|
|
4750
|
+
const envKeys = new Set(Object.keys(envOptions));
|
|
4751
|
+
for (const name of Object.keys(effective)) {
|
|
4752
|
+
let source = envKeys.has(name) ? "env" : fileKeys.has(name) ? "file" : "default";
|
|
4753
|
+
if (name === "maxResults" && source === "default" && (envKeys.has("count") || fileKeys.has("count"))) {
|
|
4754
|
+
source = "count";
|
|
4755
|
+
}
|
|
4756
|
+
io.out(`${name} = ${formatConfigValue(effective[name], "join")} (${source})`);
|
|
4757
|
+
}
|
|
4758
|
+
io.out("");
|
|
4759
|
+
io.out(HOST_OVERRIDE_NOTE);
|
|
4417
4760
|
}
|
|
4761
|
+
var EXIT_CONFIG_PROBLEMS = 1, EXIT_USAGE = 2, HOST_OVERRIDE_NOTE = "Host-native layers (OpenCode options, Pi settings) may override file and env values per host.";
|
|
4762
|
+
var init_config_command = __esm(() => {
|
|
4763
|
+
init_auth();
|
|
4764
|
+
init_client();
|
|
4765
|
+
init_config_file();
|
|
4766
|
+
init_config_summary();
|
|
4767
|
+
init_env();
|
|
4768
|
+
init_format2();
|
|
4769
|
+
});
|
|
4418
4770
|
|
|
4419
|
-
// src/installer/
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
if (record === undefined) {
|
|
4424
|
-
throw new Error(`No ${input.host} install is recorded for the ${input.scope} scope.`);
|
|
4425
|
-
}
|
|
4426
|
-
return withOperationLock(record.files.map((file) => file.file), () => uninstallLocked(input, record));
|
|
4427
|
-
});
|
|
4771
|
+
// src/installer/detect.ts
|
|
4772
|
+
import { existsSync as existsSync8, readFileSync as readFileSync11 } from "node:fs";
|
|
4773
|
+
function replacingExistingEntryNote(path) {
|
|
4774
|
+
return `An existing ${path} entry is being replaced; its previous form is recorded for uninstall.`;
|
|
4428
4775
|
}
|
|
4429
|
-
function
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
if (
|
|
4434
|
-
|
|
4776
|
+
function othersStayRegisteredNote(others) {
|
|
4777
|
+
return `Other MCP servers stay registered (${others.join(", ")}); routing to DeepSeek is not exclusive while they offer web tools.`;
|
|
4778
|
+
}
|
|
4779
|
+
function looksWebCapable(name, entry) {
|
|
4780
|
+
if (WEB_TOOL_HINT.test(name))
|
|
4781
|
+
return true;
|
|
4782
|
+
if (!isPlainObject(entry))
|
|
4783
|
+
return false;
|
|
4784
|
+
const command = typeof entry.command === "string" ? entry.command : "";
|
|
4785
|
+
const url = typeof entry.url === "string" ? entry.url : "";
|
|
4786
|
+
const args = Array.isArray(entry.args) ? entry.args.filter((arg) => typeof arg === "string").join(" ") : "";
|
|
4787
|
+
return WEB_TOOL_HINT.test(`${command} ${args} ${url}`);
|
|
4788
|
+
}
|
|
4789
|
+
function looksLikeOwnPackage(source) {
|
|
4790
|
+
return /(^|[/:@])deepseek-web-retrieval($|[@/]|-\d)/.test(source);
|
|
4791
|
+
}
|
|
4792
|
+
function findCompetitorSteering(host, file) {
|
|
4793
|
+
if (!existsSync8(file))
|
|
4794
|
+
return [];
|
|
4795
|
+
let text;
|
|
4796
|
+
try {
|
|
4797
|
+
text = readFileSync11(file, "utf8");
|
|
4798
|
+
} catch {
|
|
4799
|
+
return [];
|
|
4435
4800
|
}
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4801
|
+
return removeBlock(host, text).split(`
|
|
4802
|
+
`).map((line) => line.trim()).filter((line) => line.length > 0 && STEERING_HINT.test(line) && !line.includes("deepseek")).slice(0, 2);
|
|
4803
|
+
}
|
|
4804
|
+
function competitorSteeringNote(file, lines) {
|
|
4805
|
+
const quoted = lines.map((line) => `"${line}"`).join(", ");
|
|
4806
|
+
return `Existing rules in ${file} steer toward other web tools (${quoted}); the plan surfaces this conflict — it does not resolve it.`;
|
|
4807
|
+
}
|
|
4808
|
+
function splitCompetitors(servers) {
|
|
4809
|
+
const competitors = [];
|
|
4810
|
+
const bystanders = [];
|
|
4811
|
+
for (const [name, entry] of Object.entries(servers)) {
|
|
4812
|
+
if (name === "deepseek")
|
|
4439
4813
|
continue;
|
|
4440
|
-
|
|
4441
|
-
if (loaded.parseError)
|
|
4442
|
-
throw new Error(`${file.file}: ${loaded.parseError}`);
|
|
4814
|
+
(looksWebCapable(name, entry) ? competitors : bystanders).push(name);
|
|
4443
4815
|
}
|
|
4444
|
-
|
|
4445
|
-
|
|
4816
|
+
return { competitors, bystanders };
|
|
4817
|
+
}
|
|
4818
|
+
var BOTH_CAPABILITIES_OFF = "Both capabilities are off; nothing would be installed.", FORCED_COST_NOTE, WEB_TOOL_HINT, STEERING_HINT;
|
|
4819
|
+
var init_detect = __esm(() => {
|
|
4820
|
+
init_rules();
|
|
4821
|
+
init_value();
|
|
4822
|
+
FORCED_COST_NOTE = "Forced routing is deterministic, and Search and Fetch are billed per successful request — " + `see the README's "Privacy and cost" section before applying.`;
|
|
4823
|
+
WEB_TOOL_HINT = /(web[_ -]?(search|fetch)|search|fetch|browse|scrape|crawl|tavily|brave|exa|perplexity|firecrawl|jina|serp|duckduckgo|reader)/i;
|
|
4824
|
+
STEERING_HINT = /(web[_ ]?(search|fetch)|websearch|webfetch|tavily|exa|brave|perplexity|firecrawl|jina|serp|duckduckgo|google|bing)/i;
|
|
4825
|
+
});
|
|
4826
|
+
|
|
4827
|
+
// src/installer/rulesContent.ts
|
|
4828
|
+
function rulesLines(ids, search, fetch) {
|
|
4829
|
+
const lines = ["DeepSeek retrieval guidance:"];
|
|
4830
|
+
if (search === "preferred") {
|
|
4831
|
+
lines.push(`- Use ${ids.search} when the task needs fresh external information: current docs, version numbers,`, " changelogs, or anything you cannot know from the repository alone. Do not search for facts already", " present locally.");
|
|
4446
4832
|
}
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
lines.push(
|
|
4833
|
+
if (fetch === "preferred") {
|
|
4834
|
+
lines.push(`- Use ${ids.fetch} to read a specific public web page as Markdown when its content is needed.`);
|
|
4835
|
+
}
|
|
4836
|
+
lines.push("- Never send localhost or private-network URLs to the DeepSeek tools; use local tools for those (the", " server blocks private hosts by default).", "- DeepSeek requests are billed per successful call; retrieve only what the task needs.");
|
|
4451
4837
|
return lines;
|
|
4452
4838
|
}
|
|
4453
|
-
var
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4839
|
+
var MCP_TOOL_IDS, RAW_MCP_TOOL_IDS;
|
|
4840
|
+
var init_rulesContent = __esm(() => {
|
|
4841
|
+
MCP_TOOL_IDS = {
|
|
4842
|
+
search: "mcp__deepseek__deepseek_web_search",
|
|
4843
|
+
fetch: "mcp__deepseek__deepseek_fetch"
|
|
4844
|
+
};
|
|
4845
|
+
RAW_MCP_TOOL_IDS = { search: "deepseek_web_search", fetch: "deepseek_fetch" };
|
|
4460
4846
|
});
|
|
4461
4847
|
|
|
4462
|
-
// src/installer/
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4848
|
+
// src/installer/hosts/shared.ts
|
|
4849
|
+
function planHeader(input, paths, loaded, dialect) {
|
|
4850
|
+
const notes = [];
|
|
4851
|
+
const errors = [];
|
|
4852
|
+
if (loaded.parseError) {
|
|
4853
|
+
errors.push({ message: `${paths.config} exists but does not parse as ${dialect}: ${loaded.parseError}` });
|
|
4854
|
+
return { notes, errors, abort: true };
|
|
4855
|
+
}
|
|
4856
|
+
if (input.search === "off" && input.fetch === "off") {
|
|
4857
|
+
errors.push({ message: BOTH_CAPABILITIES_OFF });
|
|
4858
|
+
}
|
|
4859
|
+
const rules = codecFor("rules", input.host).load(paths.rules);
|
|
4860
|
+
if (rules.parseError) {
|
|
4861
|
+
errors.push({ message: `${paths.rules}: ${rules.parseError}` });
|
|
4862
|
+
return { notes, errors, abort: true };
|
|
4863
|
+
}
|
|
4864
|
+
const steering = findCompetitorSteering(input.host, paths.rules);
|
|
4865
|
+
if (steering.length > 0)
|
|
4866
|
+
notes.push(competitorSteeringNote(paths.rules, steering));
|
|
4867
|
+
return { notes, errors, abort: false };
|
|
4868
|
+
}
|
|
4869
|
+
var init_shared = __esm(() => {
|
|
4870
|
+
init_detect();
|
|
4871
|
+
init_formats();
|
|
4467
4872
|
});
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
const eq = arg.indexOf("=");
|
|
4481
|
-
const name = eq === -1 ? arg.slice(2) : arg.slice(2, eq);
|
|
4482
|
-
if (BOOLEAN_FLAGS.has(name)) {
|
|
4483
|
-
if (eq !== -1) {
|
|
4484
|
-
problems.push(`--${name} does not accept a value; use --${name} to enable it or omit it`);
|
|
4485
|
-
continue;
|
|
4486
|
-
}
|
|
4487
|
-
if (booleans.has(name)) {
|
|
4488
|
-
problems.push(`--${name} was given more than once`);
|
|
4489
|
-
continue;
|
|
4490
|
-
}
|
|
4491
|
-
booleans.add(name);
|
|
4492
|
-
continue;
|
|
4493
|
-
}
|
|
4494
|
-
if (!VALUE_FLAGS.has(name)) {
|
|
4495
|
-
problems.push(`unknown flag --${name}`);
|
|
4496
|
-
continue;
|
|
4497
|
-
}
|
|
4498
|
-
if (values.has(name)) {
|
|
4499
|
-
problems.push(`--${name} was given more than once`);
|
|
4500
|
-
continue;
|
|
4501
|
-
}
|
|
4502
|
-
if (eq !== -1) {
|
|
4503
|
-
values.set(name, arg.slice(eq + 1));
|
|
4504
|
-
continue;
|
|
4505
|
-
}
|
|
4506
|
-
const next = args[i + 1];
|
|
4507
|
-
if (next === undefined || next.startsWith("--")) {
|
|
4508
|
-
problems.push(`--${name} requires a value`);
|
|
4509
|
-
continue;
|
|
4510
|
-
}
|
|
4511
|
-
values.set(name, next);
|
|
4512
|
-
i += 1;
|
|
4873
|
+
|
|
4874
|
+
// src/installer/hosts/claude-code.ts
|
|
4875
|
+
import { spawnSync } from "node:child_process";
|
|
4876
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
4877
|
+
function valueKind(value) {
|
|
4878
|
+
return Array.isArray(value) ? "an array" : `a ${typeof value}`;
|
|
4879
|
+
}
|
|
4880
|
+
function probeClaudeCodeVersion(err) {
|
|
4881
|
+
const result = spawnSync("claude", ["--version"], { timeout: 5000, encoding: "utf8", env: process.env, shell: true });
|
|
4882
|
+
const version = result.status === 0 ? parseClaudeCodeVersion(result.stdout ?? "") : undefined;
|
|
4883
|
+
if (version === undefined) {
|
|
4884
|
+
err("note: claude was not found on PATH or printed no version; the version-drift check was skipped");
|
|
4513
4885
|
}
|
|
4514
|
-
return
|
|
4886
|
+
return version;
|
|
4515
4887
|
}
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4888
|
+
function parseClaudeCodeVersion(output) {
|
|
4889
|
+
return [...output.matchAll(/\b\d+\.\d+\.\d+\b/g)].map((match) => match[0]).at(-1);
|
|
4890
|
+
}
|
|
4891
|
+
function claudeCodePaths(input) {
|
|
4892
|
+
return input.scope === "global" ? {
|
|
4893
|
+
config: join2(input.home, ".claude.json"),
|
|
4894
|
+
settings: join2(input.home, ".claude", "settings.json"),
|
|
4895
|
+
rules: join2(input.home, ".claude", "CLAUDE.md")
|
|
4896
|
+
} : {
|
|
4897
|
+
config: join2(input.cwd, ".mcp.json"),
|
|
4898
|
+
settings: join2(input.cwd, ".claude", "settings.json"),
|
|
4899
|
+
rules: join2(input.cwd, "CLAUDE.md")
|
|
4900
|
+
};
|
|
4901
|
+
}
|
|
4902
|
+
function managedSettingsPath(input) {
|
|
4903
|
+
const platform = input.platform ?? process.platform;
|
|
4904
|
+
if (platform === "darwin")
|
|
4905
|
+
return "/Library/Application Support/ClaudeCode/managed-settings.json";
|
|
4906
|
+
if (platform === "win32")
|
|
4907
|
+
return "C:\\Program Files\\ClaudeCode\\managed-settings.json";
|
|
4908
|
+
return "/etc/claude-code/managed-settings.json";
|
|
4909
|
+
}
|
|
4910
|
+
function planClaudeCode(input, _previous) {
|
|
4911
|
+
const paths = claudeCodePaths(input);
|
|
4912
|
+
const loaded = codecFor("json", input.host).load(paths.config);
|
|
4913
|
+
const tiers = { search: input.search, fetch: input.fetch };
|
|
4914
|
+
const header = planHeader(input, paths, loaded, "JSON");
|
|
4915
|
+
if (header.abort) {
|
|
4916
|
+
return { host: "claude-code", scope: input.scope, tiers, edits: [], notes: header.notes, errors: header.errors };
|
|
4523
4917
|
}
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4918
|
+
const notes = [...header.notes];
|
|
4919
|
+
const errors = [...header.errors];
|
|
4920
|
+
const edits = [];
|
|
4921
|
+
if (input.hostVersion !== undefined && input.hostVersion !== CLAUDE_CODE_TESTED_VERSION) {
|
|
4922
|
+
notes.push(`Claude Code ${input.hostVersion} is installed, but this installer's Claude Code facts are verified against ` + `${CLAUDE_CODE_TESTED_VERSION} (the docs/hosts.jsonc lock). Deny-rule and tool-visibility behavior may have ` + "drifted between the two — re-verify the tiers if routing misbehaves.");
|
|
4527
4923
|
}
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4924
|
+
const managed = managedSettingsPath(input);
|
|
4925
|
+
if (existsSync9(managed)) {
|
|
4926
|
+
notes.push(`${managed} exists: the managed policy layer outranks every settings file this plan writes, so a deny or allow ` + "there overrides these changes. Check with your administrator if routing does not take effect.");
|
|
4531
4927
|
}
|
|
4532
|
-
const
|
|
4533
|
-
if (
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
${USAGE}`);
|
|
4538
|
-
return EXIT_USAGE;
|
|
4928
|
+
const servers = loaded.value.mcpServers;
|
|
4929
|
+
if (servers !== undefined && !isPlainObject(servers)) {
|
|
4930
|
+
errors.push({
|
|
4931
|
+
message: `${paths.config}: mcpServers is ${valueKind(servers)}, not an object; fix it before installing.`
|
|
4932
|
+
});
|
|
4539
4933
|
}
|
|
4540
|
-
const
|
|
4541
|
-
const
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4934
|
+
const existingEntry = isPlainObject(servers) ? servers.deepseek : undefined;
|
|
4935
|
+
const entry = {
|
|
4936
|
+
command: "npx",
|
|
4937
|
+
args: ["-y", input.packageRef],
|
|
4938
|
+
env: { [API_KEY_ENV]: `\${${API_KEY_ENV}}` }
|
|
4939
|
+
};
|
|
4940
|
+
const ops = [{ op: "set", path: ["mcpServers", "deepseek"], value: entry, previous: existingEntry }];
|
|
4941
|
+
edits.push({
|
|
4942
|
+
file: paths.config,
|
|
4943
|
+
format: "json",
|
|
4944
|
+
...input.scope === "global" ? { shared: true } : {},
|
|
4945
|
+
ops
|
|
4946
|
+
});
|
|
4947
|
+
if (isPlainObject(servers)) {
|
|
4948
|
+
if ("deepseek" in servers)
|
|
4949
|
+
notes.push(replacingExistingEntryNote("mcpServers.deepseek"));
|
|
4950
|
+
}
|
|
4951
|
+
notes.push(`\`${API_KEY_ENV}\` is written as "\${${API_KEY_ENV}}" in the server's env block, so the key expands from the ` + "environment that launches Claude Code and stays out of the file.");
|
|
4952
|
+
if (input.scope === "project") {
|
|
4953
|
+
notes.push("Claude Code asks once per project whether to use servers from .mcp.json; approve the deepseek server when " + "prompted (or via the /mcp menu), or its tools will not load in that project.");
|
|
4954
|
+
} else {
|
|
4955
|
+
notes.push(`User-scope servers live in the mcpServers key of ${paths.config} — Claude Code's own user-level store, where ` + "`claude mcp add --scope user` writes too; every other key in the file is left untouched.");
|
|
4956
|
+
}
|
|
4957
|
+
const forced = input.search === "forced" || input.fetch === "forced";
|
|
4958
|
+
const scanned = {};
|
|
4959
|
+
if (isPlainObject(servers))
|
|
4960
|
+
Object.assign(scanned, servers);
|
|
4961
|
+
const otherFile = input.scope === "project" ? join2(input.home, ".claude.json") : join2(input.cwd, ".mcp.json");
|
|
4962
|
+
if (forced && existsSync9(otherFile)) {
|
|
4963
|
+
let otherLoaded;
|
|
4545
4964
|
try {
|
|
4546
|
-
|
|
4965
|
+
otherLoaded = codecFor("json", input.host).load(otherFile);
|
|
4547
4966
|
} catch (error) {
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
if (records.length === 0) {
|
|
4552
|
-
out("No installs recorded.");
|
|
4553
|
-
return 0;
|
|
4967
|
+
errors.push({
|
|
4968
|
+
message: `${otherFile} exists but cannot be read (${error instanceof Error ? error.message : String(error)}); the forced-tier competitor scan requires it. Fix the file or its permissions, or choose a lower tier.`
|
|
4969
|
+
});
|
|
4554
4970
|
}
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4971
|
+
if (otherLoaded?.parseError) {
|
|
4972
|
+
errors.push({ message: `${otherFile} exists but does not parse as JSON: ${otherLoaded.parseError}` });
|
|
4973
|
+
} else if (otherLoaded !== undefined && otherLoaded.value.mcpServers !== undefined && !isPlainObject(otherLoaded.value.mcpServers)) {
|
|
4974
|
+
errors.push({
|
|
4975
|
+
message: `${otherFile}: mcpServers is ${valueKind(otherLoaded.value.mcpServers)}, not an object; the forced-tier competitor scan cannot read it. Fix the file or choose a lower tier.`
|
|
4976
|
+
});
|
|
4977
|
+
} else if (otherLoaded !== undefined && isPlainObject(otherLoaded.value.mcpServers)) {
|
|
4978
|
+
const otherServers = otherLoaded.value.mcpServers;
|
|
4979
|
+
Object.assign(scanned, otherServers);
|
|
4980
|
+
if ("deepseek" in otherServers) {
|
|
4981
|
+
notes.push(`A deepseek server is also registered in ${otherFile}; Claude Code resolves the name local first, then ` + "project, then user scope, so consider uninstalling the other scope to keep one registration.");
|
|
4982
|
+
}
|
|
4560
4983
|
}
|
|
4561
|
-
return 0;
|
|
4562
4984
|
}
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4985
|
+
const denyEntries = [];
|
|
4986
|
+
const { competitors, bystanders } = splitCompetitors(scanned);
|
|
4987
|
+
if (forced && competitors.length > 0) {
|
|
4988
|
+
const rules = competitors.map((name) => `mcp__${name}`);
|
|
4989
|
+
denyEntries.push(...rules);
|
|
4990
|
+
notes.push(`Forced tier: web-capable MCP servers are denied by server-level rules (${rules.join(", ")}), so DeepSeek ` + "routing is exclusive while the entries stay." + (input.scope === "global" ? " These denies apply in every project on this machine and match by server name, so a same-named server in another project is denied too." : ""));
|
|
4991
|
+
}
|
|
4992
|
+
const stayRegistered = forced ? bystanders : [...competitors, ...bystanders];
|
|
4993
|
+
if (stayRegistered.length > 0)
|
|
4994
|
+
notes.push(othersStayRegisteredNote(stayRegistered));
|
|
4995
|
+
if (input.search === "forced")
|
|
4996
|
+
denyEntries.push(BUILTIN_TOOL_IDS.search);
|
|
4997
|
+
if (input.fetch === "forced")
|
|
4998
|
+
denyEntries.push(BUILTIN_TOOL_IDS.fetch);
|
|
4999
|
+
if (input.search === "off")
|
|
5000
|
+
denyEntries.push(MCP_TOOL_IDS.search);
|
|
5001
|
+
if (input.fetch === "off")
|
|
5002
|
+
denyEntries.push(MCP_TOOL_IDS.fetch);
|
|
5003
|
+
if (denyEntries.length > 0) {
|
|
5004
|
+
const settingsLoaded = codecFor("json", input.host).load(paths.settings);
|
|
5005
|
+
if (settingsLoaded.parseError) {
|
|
5006
|
+
errors.push({ message: `${paths.settings} exists but does not parse as JSON: ${settingsLoaded.parseError}` });
|
|
5007
|
+
} else {
|
|
5008
|
+
const permissions = settingsLoaded.value.permissions;
|
|
5009
|
+
const deny = isPlainObject(permissions) ? permissions.deny : undefined;
|
|
5010
|
+
if (permissions !== undefined && !isPlainObject(permissions)) {
|
|
5011
|
+
errors.push({
|
|
5012
|
+
message: `${paths.settings}: the permissions key is ${valueKind(permissions)}, not an object; fix it before installing.`
|
|
5013
|
+
});
|
|
5014
|
+
} else if (deny !== undefined && !Array.isArray(deny)) {
|
|
5015
|
+
errors.push({
|
|
5016
|
+
message: `${paths.settings}: permissions.deny is ${valueKind(deny)}, not an array; fix it before installing.`
|
|
5017
|
+
});
|
|
5018
|
+
} else {
|
|
5019
|
+
const existingCount = Array.isArray(deny) ? deny.length : 0;
|
|
5020
|
+
const existingDeny = Array.isArray(deny) ? deny.filter((item) => typeof item === "string") : [];
|
|
5021
|
+
const denyOps = [
|
|
5022
|
+
{
|
|
5023
|
+
op: "append-array",
|
|
5024
|
+
path: ["permissions", "deny"],
|
|
5025
|
+
entries: denyEntries,
|
|
5026
|
+
existingCount
|
|
5027
|
+
}
|
|
5028
|
+
];
|
|
5029
|
+
edits.push({
|
|
5030
|
+
file: paths.settings,
|
|
5031
|
+
format: "json",
|
|
5032
|
+
...input.scope === "global" ? { shared: true } : {},
|
|
5033
|
+
ops: denyOps
|
|
5034
|
+
});
|
|
5035
|
+
if (existingCount > 0) {
|
|
5036
|
+
notes.push(`permissions.deny already holds ${existingCount} ${existingCount === 1 ? "entry" : "entries"}; ` + "the plan appends only its own and never removes yours.");
|
|
5037
|
+
}
|
|
5038
|
+
const preexisting = denyEntries.filter((name) => existingDeny.includes(name));
|
|
5039
|
+
if (preexisting.length > 0) {
|
|
5040
|
+
notes.push(`${preexisting.join(", ")} ${preexisting.length === 1 ? "is" : "are"} already present in permissions.deny — present entries are not duplicated.`);
|
|
5041
|
+
}
|
|
5042
|
+
}
|
|
4574
5043
|
}
|
|
4575
5044
|
}
|
|
4576
|
-
if (
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
${USAGE}`);
|
|
4580
|
-
return EXIT_USAGE;
|
|
5045
|
+
if (errors.length > 0) {
|
|
5046
|
+
return { host: "claude-code", scope: input.scope, tiers, edits: [], notes, errors };
|
|
4581
5047
|
}
|
|
4582
|
-
const
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
const plan = planForHost(input.host, hostVersion === undefined ? input : { ...input, hostVersion });
|
|
4589
|
-
if (plan.errors.length > 0) {
|
|
4590
|
-
for (const error of plan.errors)
|
|
4591
|
-
err(`error: ${error.message}`);
|
|
4592
|
-
err("Nothing was written.");
|
|
4593
|
-
return 1;
|
|
5048
|
+
const forcedIds = [
|
|
5049
|
+
...input.search === "forced" ? [BUILTIN_TOOL_IDS.search] : [],
|
|
5050
|
+
...input.fetch === "forced" ? [BUILTIN_TOOL_IDS.fetch] : []
|
|
5051
|
+
];
|
|
5052
|
+
if (forcedIds.length > 0) {
|
|
5053
|
+
notes.push(`Forced is exclusion: bare deny rules (${forcedIds.join(", ")}) in permissions.deny remove the built-in tool ` + "from the model's context entirely — deny outranks allow at every settings level and cannot be carved out, so " + `the exclusion also applies to every other workflow that relied on the tool ${input.scope === "global" ? "in every project on this machine" : "in this project"}. ` + FORCED_COST_NOTE);
|
|
4594
5054
|
}
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
5055
|
+
const offIds = [
|
|
5056
|
+
...input.search === "off" ? [MCP_TOOL_IDS.search] : [],
|
|
5057
|
+
...input.fetch === "off" ? [MCP_TOOL_IDS.fetch] : []
|
|
5058
|
+
];
|
|
5059
|
+
if (offIds.length > 0) {
|
|
5060
|
+
notes.push(`Off capabilities are written as their full tool ids (${offIds.join(", ")}) in permissions.deny: the tool ` + "leaves the model's context while the server keeps running; delete the entry to re-enable it.");
|
|
4600
5061
|
}
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
for (const line of result.lines)
|
|
4604
|
-
out(line);
|
|
4605
|
-
return 0;
|
|
4606
|
-
} catch (error) {
|
|
4607
|
-
err(error instanceof Error ? error.message : String(error));
|
|
4608
|
-
return exitCodeForError(error);
|
|
5062
|
+
if (input.search === "optional" || input.fetch === "optional") {
|
|
5063
|
+
notes.push("Claude Code ships built-in WebSearch and WebFetch tools; under the optional tier they stay available " + "alongside the DeepSeek tools.");
|
|
4609
5064
|
}
|
|
5065
|
+
if (input.search === "preferred" || input.fetch === "preferred") {
|
|
5066
|
+
if (input.scope === "project" && !existsSync9(paths.rules) && existsSync9(join2(input.cwd, "AGENTS.md"))) {
|
|
5067
|
+
notes.push(`This plan creates ${paths.rules} where none exists, and ${join2(input.cwd, "AGENTS.md")} is present: Claude ` + "Code reads AGENTS.md only when no CLAUDE.md exists above the cwd, so the new CLAUDE.md takes precedence — " + "merge anything you rely on from AGENTS.md before applying.");
|
|
5068
|
+
}
|
|
5069
|
+
edits.push({
|
|
5070
|
+
file: paths.rules,
|
|
5071
|
+
format: "rules",
|
|
5072
|
+
block: buildRulesBlock(input.host, rulesLines(MCP_TOOL_IDS, input.search, input.fetch))
|
|
5073
|
+
});
|
|
5074
|
+
notes.push(input.scope === "global" ? `Rules block upserted in ${paths.rules}; Claude Code loads the user CLAUDE.md plus every CLAUDE.md from the cwd up the directory tree, concatenating the levels.` : `Rules block upserted in ${paths.rules}; Claude Code loads every CLAUDE.md from the cwd up the directory tree, concatenating the levels.`);
|
|
5075
|
+
}
|
|
5076
|
+
return { host: "claude-code", scope: input.scope, tiers, edits, notes, errors };
|
|
4610
5077
|
}
|
|
4611
|
-
|
|
4612
|
-
|
|
5078
|
+
var CLAUDE_CODE_TESTED_VERSION = "2.1.303", BUILTIN_TOOL_IDS;
|
|
5079
|
+
var init_claude_code = __esm(() => {
|
|
5080
|
+
init_constants();
|
|
5081
|
+
init_detect();
|
|
5082
|
+
init_formats();
|
|
5083
|
+
init_rules();
|
|
5084
|
+
init_value();
|
|
5085
|
+
init_paths();
|
|
5086
|
+
init_rulesContent();
|
|
5087
|
+
init_shared();
|
|
5088
|
+
BUILTIN_TOOL_IDS = { search: "WebSearch", fetch: "WebFetch" };
|
|
5089
|
+
});
|
|
5090
|
+
|
|
5091
|
+
// src/installer/hosts/codex.ts
|
|
5092
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
5093
|
+
function probeCodexVersion(err) {
|
|
5094
|
+
const result = spawnSync2("codex", ["--version"], { timeout: 5000, encoding: "utf8", env: process.env, shell: true });
|
|
5095
|
+
const version = result.status === 0 ? parseCodexVersion(result.stdout ?? "") : undefined;
|
|
5096
|
+
if (version === undefined) {
|
|
5097
|
+
err("note: codex was not found on PATH or printed no version; the version-drift check was skipped");
|
|
5098
|
+
}
|
|
5099
|
+
return version;
|
|
4613
5100
|
}
|
|
4614
|
-
function
|
|
4615
|
-
|
|
4616
|
-
err(`error: ${problem}`);
|
|
4617
|
-
err(`
|
|
4618
|
-
${USAGE}`);
|
|
4619
|
-
return EXIT_USAGE;
|
|
5101
|
+
function parseCodexVersion(output) {
|
|
5102
|
+
return /\bcodex(?:-cli)?\s+(\d+\.\d+\.\d+)\b/.exec(output)?.[1];
|
|
4620
5103
|
}
|
|
4621
|
-
function
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
5104
|
+
function codexPaths(input) {
|
|
5105
|
+
return {
|
|
5106
|
+
config: join2(input.home, ".codex/config.toml"),
|
|
5107
|
+
rules: input.scope === "global" ? join2(input.home, ".codex/AGENTS.md") : join2(input.cwd, "AGENTS.md")
|
|
5108
|
+
};
|
|
4626
5109
|
}
|
|
4627
|
-
function
|
|
4628
|
-
const
|
|
4629
|
-
const
|
|
4630
|
-
const
|
|
4631
|
-
const
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
if (host === undefined)
|
|
4635
|
-
problems.push(`--host is required (one of: ${HOSTS.join(", ")})`);
|
|
4636
|
-
else if (!HOSTS.includes(host))
|
|
4637
|
-
problems.push(`invalid --host value "${host}" (one of: ${HOSTS.join(", ")})`);
|
|
4638
|
-
if (scope === undefined)
|
|
4639
|
-
problems.push(`--scope is required (one of: ${SCOPES.join(", ")})`);
|
|
4640
|
-
else if (!SCOPES.includes(scope))
|
|
4641
|
-
problems.push(`invalid --scope value "${scope}" (one of: ${SCOPES.join(", ")})`);
|
|
4642
|
-
if (path !== undefined) {
|
|
4643
|
-
if (host !== "pi") {
|
|
4644
|
-
problems.push(`--path is only supported for host pi (${host ?? "no host"} has a single integration path)`);
|
|
4645
|
-
} else if (!PATH_CHOICES.includes(path)) {
|
|
4646
|
-
problems.push(`invalid --path value "${path}" (one of: ${PATH_CHOICES.join(", ")})`);
|
|
4647
|
-
}
|
|
5110
|
+
function planCodex(input, _previous) {
|
|
5111
|
+
const paths = codexPaths(input);
|
|
5112
|
+
const loaded = codecFor("toml", input.host).load(paths.config);
|
|
5113
|
+
const tiers = { search: input.search, fetch: input.fetch };
|
|
5114
|
+
const header = planHeader(input, paths, loaded, "TOML");
|
|
5115
|
+
if (header.abort) {
|
|
5116
|
+
return { host: "codex", scope: input.scope, tiers, edits: [], notes: header.notes, errors: header.errors };
|
|
4648
5117
|
}
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
problems.push(flags.values.has("fetch") ? `invalid --fetch value "${fetch}" (one of: ${TIERS.join(", ")})` : `--fetch is required (one of: ${TIERS.join(", ")})`);
|
|
4655
|
-
}
|
|
5118
|
+
const notes = [...header.notes];
|
|
5119
|
+
const errors = [...header.errors];
|
|
5120
|
+
const edits = [];
|
|
5121
|
+
if (input.hostVersion !== undefined && input.hostVersion !== CODEX_TESTED_VERSION) {
|
|
5122
|
+
notes.push(`Codex ${input.hostVersion} is installed, but this installer's Codex facts are verified against ` + `${CODEX_TESTED_VERSION} (the docs/hosts.jsonc lock). Kill-switch and trap behavior may have drifted ` + "between the two — re-verify the tiers if routing misbehaves.");
|
|
4656
5123
|
}
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
...path !== undefined ? { path } : {}
|
|
5124
|
+
const tools = loaded.value.tools;
|
|
5125
|
+
if (isPlainObject(tools) && "web_search" in tools) {
|
|
5126
|
+
errors.push({
|
|
5127
|
+
message: `${paths.config} sets [tools] web_search, which is a silent no-op at Codex 0.163.0 (only the top-level ` + "web_search key controls the tool). Remove it first so the config cannot mislead."
|
|
5128
|
+
});
|
|
5129
|
+
}
|
|
5130
|
+
const servers = loaded.value.mcp_servers;
|
|
5131
|
+
if (isPlainObject(servers)) {
|
|
5132
|
+
if ("deepseek" in servers)
|
|
5133
|
+
notes.push(replacingExistingEntryNote("mcp_servers.deepseek"));
|
|
5134
|
+
const { competitors, bystanders } = splitCompetitors(servers);
|
|
5135
|
+
if ((input.search === "forced" || input.fetch === "forced") && competitors.length > 0) {
|
|
5136
|
+
errors.push({
|
|
5137
|
+
message: `Forced cannot deliver exclusive routing: [mcp_servers.${competitors.join("], [mcp_servers.")}] look(s) web-capable, ` + "and Codex offers no per-server kill switch. Remove the server(s), disable their tools by hand via their " + "disabled_tools key, or choose a lower tier."
|
|
5138
|
+
});
|
|
4673
5139
|
}
|
|
5140
|
+
const stayRegistered = input.search === "forced" || input.fetch === "forced" ? bystanders : [...competitors, ...bystanders];
|
|
5141
|
+
if (stayRegistered.length > 0)
|
|
5142
|
+
notes.push(othersStayRegisteredNote(stayRegistered));
|
|
5143
|
+
}
|
|
5144
|
+
const existingEntry = isPlainObject(servers) ? servers.deepseek : undefined;
|
|
5145
|
+
const rawToolIds = Object.values(RAW_MCP_TOOL_IDS);
|
|
5146
|
+
const keptDisabled = isPlainObject(existingEntry) && Array.isArray(existingEntry.disabled_tools) ? existingEntry.disabled_tools.filter((name) => typeof name === "string" && !rawToolIds.includes(name)) : [];
|
|
5147
|
+
const offDisabled = [
|
|
5148
|
+
...input.search === "off" ? [RAW_MCP_TOOL_IDS.search] : [],
|
|
5149
|
+
...input.fetch === "off" ? [RAW_MCP_TOOL_IDS.fetch] : []
|
|
5150
|
+
];
|
|
5151
|
+
const entry = {
|
|
5152
|
+
command: "npx",
|
|
5153
|
+
args: ["-y", input.packageRef],
|
|
5154
|
+
...keptDisabled.length > 0 || offDisabled.length > 0 ? { disabled_tools: [...keptDisabled, ...offDisabled] } : {},
|
|
5155
|
+
env_vars: [API_KEY_ENV, ...OPTION_ENV_VARS]
|
|
4674
5156
|
};
|
|
5157
|
+
const ops = [{ op: "set", path: ["mcp_servers", "deepseek"], value: entry, previous: existingEntry }];
|
|
5158
|
+
if (offDisabled.length > 0) {
|
|
5159
|
+
notes.push(`Off capabilities use Codex's own disabled_tools key (${offDisabled.join(", ")}): the tools are removed from ` + "the model's context while the server keeps running and stays re-enableable by deleting the entries.");
|
|
5160
|
+
}
|
|
5161
|
+
if (input.search === "forced") {
|
|
5162
|
+
const previous = typeof loaded.value.web_search === "string" ? loaded.value.web_search : undefined;
|
|
5163
|
+
ops.push({ op: "set", path: ["web_search"], value: "disabled", previous });
|
|
5164
|
+
notes.push(previous === undefined ? 'web_search is being set to "disabled" (no previous value existed; the default is "cached").' : `web_search is being set to "disabled"; the previous value "${previous}" is snapshotted for uninstall.`);
|
|
5165
|
+
}
|
|
5166
|
+
edits.push({ file: paths.config, format: "toml", shared: true, ops });
|
|
5167
|
+
notes.push(`Codex stdio children do not inherit the parent environment, so the entry forwards ${API_KEY_ENV} and every ` + "DEEPSEEK_* option variable explicitly via env_vars; shell-set knobs reach the server without hand-editing " + "config.toml (the shared config file works without any forwarding).");
|
|
5168
|
+
if (input.scope === "project") {
|
|
5169
|
+
notes.push("Codex config lives in ~/.codex/config.toml only (user level), so the server registration and kill switch are " + "written there even for a project-scope install; the rules file stays project-local.");
|
|
5170
|
+
}
|
|
5171
|
+
if (loaded.lossy) {
|
|
5172
|
+
notes.push(`${paths.config} contains comments; the TOML rewrite will drop them.`);
|
|
5173
|
+
}
|
|
5174
|
+
if (input.search === "forced" || input.fetch === "forced") {
|
|
5175
|
+
notes.push(`Forced search is exclusion: the built-in web_search tool is disabled ("disabled" mode). ${FORCED_COST_NOTE}`);
|
|
5176
|
+
}
|
|
5177
|
+
if (input.search === "optional" || input.fetch === "optional") {
|
|
5178
|
+
notes.push('Note: Codex ships a built-in web_search tool (default "cached" mode, an OpenAI index) that stays available ' + "alongside the DeepSeek tools under the optional tier.");
|
|
5179
|
+
}
|
|
5180
|
+
if (input.search === "preferred" || input.fetch === "preferred") {
|
|
5181
|
+
edits.push({
|
|
5182
|
+
file: paths.rules,
|
|
5183
|
+
format: "rules",
|
|
5184
|
+
block: buildRulesBlock(input.host, rulesLines(MCP_TOOL_IDS, input.search, input.fetch))
|
|
5185
|
+
});
|
|
5186
|
+
notes.push(input.scope === "global" ? `Rules block upserted in ${paths.rules}; Codex reads ~/.codex/AGENTS.md plus every AGENTS.md from the project root to the cwd.` : `Rules block upserted in ${paths.rules}; Codex reads every AGENTS.md from the project root to the cwd.`);
|
|
5187
|
+
}
|
|
5188
|
+
return { host: "codex", scope: input.scope, tiers, edits, notes, errors };
|
|
4675
5189
|
}
|
|
4676
|
-
var
|
|
4677
|
-
var
|
|
4678
|
-
init_version();
|
|
4679
|
-
init_apply();
|
|
4680
|
-
init_hosts();
|
|
4681
|
-
init_state();
|
|
4682
|
-
init_uninstall();
|
|
4683
|
-
HOSTS = Object.keys(HOST_PLANNERS);
|
|
4684
|
-
HOST_CHOICES = `<${HOSTS.join("|")}>`;
|
|
4685
|
-
SCOPES = ["project", "global"];
|
|
4686
|
-
TIERS = ["optional", "preferred", "forced", "off"];
|
|
4687
|
-
USAGE = `Usage:
|
|
4688
|
-
deepseek-web-retrieval init --host ${HOST_CHOICES} --scope <project|global> --search <optional|preferred|forced|off> --fetch <optional|preferred|forced|off> [--path <extension|mcp>] [--package <npm ref>] [--cwd <dir>] [--apply]
|
|
4689
|
-
deepseek-web-retrieval uninstall --host ${HOST_CHOICES} --scope <project|global> [--cwd <dir>]
|
|
4690
|
-
deepseek-web-retrieval list [--cwd <dir>]
|
|
4691
|
-
deepseek-web-retrieval version
|
|
4692
|
-
|
|
4693
|
-
init prints the full plan (every file, old value → new value) and writes nothing
|
|
4694
|
-
until --apply is passed. uninstall restores what the plan applied. --path picks
|
|
4695
|
-
the integration mechanism for hosts with more than one (Pi: extension by
|
|
4696
|
-
default, mcp for the stdio MCP server). --package overrides the package ref
|
|
4697
|
-
written into host config; the Pi extension path turns it into the packages
|
|
4698
|
-
source (a bare name is prefixed with npm:, a ref that already starts with npm:
|
|
4699
|
-
is written as-is).
|
|
4700
|
-
|
|
4701
|
-
Exit codes: 0 success; 1 refused or failed with nothing written (a failed apply
|
|
4702
|
-
is fully rolled back); 2 invalid command line; 3 recovery required — a failed
|
|
4703
|
-
apply could not roll back completely, follow the reported guidance before
|
|
4704
|
-
re-running.`;
|
|
4705
|
-
VALUE_FLAGS = new Set(["host", "scope", "search", "fetch", "path", "package", "cwd"]);
|
|
4706
|
-
BOOLEAN_FLAGS = new Set(["apply"]);
|
|
4707
|
-
PATH_CHOICES = ["extension", "mcp"];
|
|
4708
|
-
});
|
|
4709
|
-
|
|
4710
|
-
// src/core/auth.ts
|
|
4711
|
-
function apiKeyFromEnv(env = process.env) {
|
|
4712
|
-
const key = env[API_KEY_ENV]?.trim();
|
|
4713
|
-
return key ? key : undefined;
|
|
4714
|
-
}
|
|
4715
|
-
var init_auth = __esm(() => {
|
|
5190
|
+
var CODEX_TESTED_VERSION = "0.163.0";
|
|
5191
|
+
var init_codex = __esm(() => {
|
|
4716
5192
|
init_constants();
|
|
5193
|
+
init_env();
|
|
5194
|
+
init_detect();
|
|
5195
|
+
init_formats();
|
|
5196
|
+
init_rules();
|
|
5197
|
+
init_value();
|
|
5198
|
+
init_paths();
|
|
5199
|
+
init_rulesContent();
|
|
5200
|
+
init_shared();
|
|
4717
5201
|
});
|
|
4718
5202
|
|
|
4719
|
-
// src/
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed);
|
|
4726
|
-
if (hasScheme && !/^https?:\/\//i.test(trimmed))
|
|
4727
|
-
return;
|
|
4728
|
-
let hostname;
|
|
4729
|
-
try {
|
|
4730
|
-
hostname = new URL(hasScheme ? trimmed : `http://${trimmed}`).hostname;
|
|
4731
|
-
} catch {
|
|
4732
|
-
return;
|
|
4733
|
-
}
|
|
4734
|
-
return normalizeHostname(hostname);
|
|
4735
|
-
}
|
|
4736
|
-
function normalizeHostname(value) {
|
|
4737
|
-
return value.trim().toLowerCase().replace(/^\[/, "").replace(/\]$/, "").replace(/\.+$/, "");
|
|
4738
|
-
}
|
|
4739
|
-
function domainMatches(hostname, domain) {
|
|
4740
|
-
const host = normalizeHostname(hostname);
|
|
4741
|
-
const target = normalizeHostname(domain);
|
|
4742
|
-
if (!host || !target)
|
|
4743
|
-
return false;
|
|
4744
|
-
return host === target || host.endsWith(`.${target}`);
|
|
4745
|
-
}
|
|
4746
|
-
function normalizeDomains(values) {
|
|
4747
|
-
if (!Array.isArray(values))
|
|
4748
|
-
return [];
|
|
4749
|
-
const domains = new Set;
|
|
4750
|
-
for (const value of values) {
|
|
4751
|
-
if (typeof value !== "string")
|
|
4752
|
-
continue;
|
|
4753
|
-
const domain = normalizeDomain(value);
|
|
4754
|
-
if (domain)
|
|
4755
|
-
domains.add(domain);
|
|
4756
|
-
}
|
|
4757
|
-
return [...domains];
|
|
5203
|
+
// src/installer/hosts/opencode.ts
|
|
5204
|
+
function opencodePaths(input) {
|
|
5205
|
+
return input.scope === "global" ? {
|
|
5206
|
+
config: join2(configHome(input), "opencode/opencode.jsonc"),
|
|
5207
|
+
rules: join2(configHome(input), "opencode/AGENTS.md")
|
|
5208
|
+
} : { config: join2(input.cwd, "opencode.jsonc"), rules: join2(input.cwd, "AGENTS.md") };
|
|
4758
5209
|
}
|
|
4759
|
-
function
|
|
4760
|
-
const
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
return true;
|
|
5210
|
+
function planOpenCode(input, previous) {
|
|
5211
|
+
const paths = opencodePaths(input);
|
|
5212
|
+
const loaded = codecFor("json", input.host).load(paths.config);
|
|
5213
|
+
const tiers = { search: input.search, fetch: input.fetch };
|
|
5214
|
+
const header = planHeader(input, paths, loaded, "JSONC");
|
|
5215
|
+
if (header.abort) {
|
|
5216
|
+
return { host: "opencode", scope: input.scope, tiers, edits: [], notes: header.notes, errors: header.errors };
|
|
4767
5217
|
}
|
|
4768
|
-
const
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
if (
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
}
|
|
4775
|
-
function assertFetchHostPolicy(url, options) {
|
|
4776
|
-
const host = normalizeHostname(url.hostname);
|
|
4777
|
-
if (options.blockPrivateHosts && isPrivateHostname(host)) {
|
|
4778
|
-
throw new Error(`Fetch blocked for ${host}: local/private addresses are not reachable by DeepSeek. ` + "Set fetchBlockPrivateHosts=false only if you really need to send the URL.");
|
|
5218
|
+
const notes = [...header.notes];
|
|
5219
|
+
const errors = [...header.errors];
|
|
5220
|
+
const edits = [];
|
|
5221
|
+
if (input.search === "preferred") {
|
|
5222
|
+
errors.push({
|
|
5223
|
+
message: "OpenCode search has no preferred tier: the websearch provider is host-level config, so the model never " + "chooses between engines. Use --search optional (provider registered, built-in stays default), " + "--search forced (replace the engine), or --search off."
|
|
5224
|
+
});
|
|
4779
5225
|
}
|
|
4780
|
-
const
|
|
4781
|
-
|
|
4782
|
-
|
|
5226
|
+
const plugins = Array.isArray(loaded.value.plugins) ? loaded.value.plugins : [];
|
|
5227
|
+
const trackedPackages = new Set(appendedByPrevious(previous, ["plugins"]).filter((entry) => isPlainObject(entry) && typeof entry.package === "string").map((entry) => entry.package));
|
|
5228
|
+
const pluginPackages = plugins.filter((entry) => isPlainObject(entry) && typeof entry.package === "string");
|
|
5229
|
+
const excusedRefs = previous === undefined ? [] : [...trackedPackages, input.packageRef];
|
|
5230
|
+
const isExcusedRef = lineageMatcher(excusedRefs);
|
|
5231
|
+
const foreignOwn = pluginPackages.filter((entry) => looksLikeOwnPackage(entry.package) && !isExcusedRef(entry.package));
|
|
5232
|
+
if (foreignOwn.length > 0) {
|
|
5233
|
+
errors.push({
|
|
5234
|
+
message: `${paths.config} already has a plugins entry referencing deepseek-web-retrieval that this installer did not ` + "write. Remove it (or run uninstall if it came from a previous init), then retry."
|
|
5235
|
+
});
|
|
4783
5236
|
}
|
|
4784
|
-
if (
|
|
4785
|
-
|
|
5237
|
+
if (previous !== undefined && pluginPackages.some((entry) => isExcusedRef(entry.package))) {
|
|
5238
|
+
notes.push("A previous init wrote the plugins entry; re-applying keeps it the only entry (a changed package ref or tier " + "options replace it, an unchanged one is a no-op) and the original snapshots carry over.");
|
|
4786
5239
|
}
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
const parts = host.split(".").map((part) => Number(part));
|
|
4790
|
-
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
|
4791
|
-
return true;
|
|
5240
|
+
if (errors.length > 0) {
|
|
5241
|
+
return { host: "opencode", scope: input.scope, tiers, edits, notes, errors };
|
|
4792
5242
|
}
|
|
4793
|
-
const [a, b] = parts;
|
|
4794
|
-
return a === 0 || a === 10 || a === 127 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 198 && (b === 18 || b === 19) || a >= 224;
|
|
4795
|
-
}
|
|
4796
|
-
function isPrivateIpv6(host) {
|
|
4797
|
-
const value = host.toLowerCase();
|
|
4798
|
-
if (value === "::" || value === "::1")
|
|
4799
|
-
return true;
|
|
4800
|
-
if (value.startsWith("fc") || value.startsWith("fd"))
|
|
4801
|
-
return true;
|
|
4802
|
-
if (/^fe[89ab]/.test(value))
|
|
4803
|
-
return true;
|
|
4804
|
-
const dottedMapped = value.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/)?.[1];
|
|
4805
|
-
if (dottedMapped)
|
|
4806
|
-
return isPrivateIpv4(dottedMapped);
|
|
4807
|
-
const mapped = ipv4FromIpv6Hextets(value, /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
|
|
4808
|
-
if (mapped)
|
|
4809
|
-
return isPrivateIpv4(mapped);
|
|
4810
|
-
const compatible = ipv4FromIpv6Hextets(value, /^::([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
|
|
4811
|
-
if (compatible)
|
|
4812
|
-
return isPrivateIpv4(compatible);
|
|
4813
|
-
return false;
|
|
4814
|
-
}
|
|
4815
|
-
function ipv4FromIpv6Hextets(value, pattern) {
|
|
4816
|
-
const match = value.match(pattern);
|
|
4817
|
-
if (!match)
|
|
4818
|
-
return;
|
|
4819
|
-
const high = Number.parseInt(match[1], 16);
|
|
4820
|
-
const low = Number.parseInt(match[2], 16);
|
|
4821
|
-
if (!Number.isFinite(high) || !Number.isFinite(low))
|
|
4822
|
-
return;
|
|
4823
|
-
return `${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`;
|
|
4824
|
-
}
|
|
4825
|
-
var init_domains = () => {};
|
|
4826
|
-
|
|
4827
|
-
// src/core/options.ts
|
|
4828
|
-
function resolvePluginOptions(input) {
|
|
4829
|
-
return {
|
|
4830
|
-
search: resolveSearchOptions(input),
|
|
4831
|
-
fetch: resolveFetchOptions(input),
|
|
4832
|
-
auth: resolveAuthOptions(input),
|
|
4833
|
-
retry: resolveRetryOptions(input)
|
|
4834
|
-
};
|
|
4835
|
-
}
|
|
4836
|
-
function resolveSearchOptions(input) {
|
|
4837
|
-
return {
|
|
4838
|
-
count: clampInt(input?.count, DEFAULT_SEARCH_COUNT, 1, 50),
|
|
4839
|
-
maxResults: clampInt(input?.maxResults, DEFAULT_MAX_RESULTS, 0, 50),
|
|
4840
|
-
minRerank: finiteNumber(input?.minRerank, DEFAULT_MIN_RERANK),
|
|
4841
|
-
minAuthority: finiteNumber(input?.minAuthority, DEFAULT_MIN_AUTHORITY),
|
|
4842
|
-
allowUnknownAuthority: input?.allowUnknownAuthority !== false,
|
|
4843
|
-
blockedDomains: normalizeDomains(input?.blockedDomains),
|
|
4844
|
-
includeDomains: normalizeDomains(input?.includeDomains),
|
|
4845
|
-
language: optionalText(input?.language),
|
|
4846
|
-
maxSummaryChars: clampInt(input?.maxSummaryChars, DEFAULT_MAX_SUMMARY_CHARS, 0, 1e6),
|
|
4847
|
-
timeoutMs: clampInt(input?.searchTimeoutMs, DEFAULT_TIMEOUT_MS, MIN_TIMEOUT_MS, MAX_TIMEOUT_MS),
|
|
4848
|
-
enabled: input?.searchEnabled !== false
|
|
4849
|
-
};
|
|
4850
|
-
}
|
|
4851
|
-
function resolveFetchOptions(input) {
|
|
4852
|
-
return {
|
|
4853
|
-
maxChars: clampInt(input?.fetchMaxChars, DEFAULT_FETCH_MAX_CHARS, 0, 1e6),
|
|
4854
|
-
allowedDomains: normalizeDomains(input?.fetchAllowedDomains),
|
|
4855
|
-
blockedDomains: normalizeDomains(input?.fetchBlockedDomains),
|
|
4856
|
-
timeoutMs: clampInt(input?.fetchTimeoutMs, DEFAULT_TIMEOUT_MS, MIN_TIMEOUT_MS, MAX_TIMEOUT_MS),
|
|
4857
|
-
blockPrivateHosts: input?.fetchBlockPrivateHosts !== false,
|
|
4858
|
-
enabled: input?.fetchEnabled !== false
|
|
4859
|
-
};
|
|
4860
|
-
}
|
|
4861
|
-
function resolveAuthOptions(input) {
|
|
4862
|
-
const value = input?.apiKeySource;
|
|
4863
|
-
return {
|
|
4864
|
-
source: value === "opencode" || value === "env" ? value : DEFAULT_API_KEY_SOURCE
|
|
4865
|
-
};
|
|
4866
|
-
}
|
|
4867
|
-
function resolveRetryOptions(input) {
|
|
4868
|
-
return {
|
|
4869
|
-
maxRetries: clampInt(input?.maxRetries, DEFAULT_MAX_RETRIES, 0, MAX_RETRIES),
|
|
4870
|
-
baseDelayMs: clampInt(input?.retryBaseDelayMs, DEFAULT_RETRY_BASE_DELAY_MS, 0, MAX_RETRY_DELAY_MS)
|
|
4871
|
-
};
|
|
4872
|
-
}
|
|
4873
|
-
function optionalText(value) {
|
|
4874
|
-
if (typeof value !== "string")
|
|
4875
|
-
return;
|
|
4876
|
-
const trimmed = value.trim();
|
|
4877
|
-
return trimmed ? trimmed : undefined;
|
|
4878
|
-
}
|
|
4879
|
-
function finiteNumber(value, fallback) {
|
|
4880
|
-
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
4881
|
-
}
|
|
4882
|
-
function clampInt(value, fallback, min, max) {
|
|
4883
|
-
const number = typeof value === "number" && Number.isFinite(value) ? Math.trunc(value) : fallback;
|
|
4884
|
-
return Math.min(max, Math.max(min, number));
|
|
4885
|
-
}
|
|
4886
|
-
var init_options = __esm(() => {
|
|
4887
|
-
init_constants();
|
|
4888
|
-
init_domains();
|
|
4889
|
-
});
|
|
4890
|
-
|
|
4891
|
-
// src/core/env.ts
|
|
4892
|
-
function resolveEnvOptions(env = process.env) {
|
|
4893
|
-
return resolvePluginOptions(pluginOptionsFromEnv(env));
|
|
4894
|
-
}
|
|
4895
|
-
function pluginOptionsFromEnv(env) {
|
|
4896
5243
|
const options = {};
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
}
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
}
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
if (
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
if (
|
|
4935
|
-
|
|
4936
|
-
}
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
if (domains.length > 0)
|
|
4947
|
-
target[key] = domains;
|
|
4948
|
-
}
|
|
4949
|
-
function isFalseValue(value) {
|
|
4950
|
-
const trimmed = value?.trim().toLowerCase();
|
|
4951
|
-
return trimmed === "false" || trimmed === "0" || trimmed === "no" || trimmed === "off";
|
|
5244
|
+
if (input.search === "forced")
|
|
5245
|
+
options.setDefaultWebsearch = true;
|
|
5246
|
+
if (input.search === "off")
|
|
5247
|
+
options.searchEnabled = false;
|
|
5248
|
+
if (input.fetch === "forced")
|
|
5249
|
+
options.fetchOverrideWebfetch = true;
|
|
5250
|
+
if (input.fetch === "off")
|
|
5251
|
+
options.fetchEnabled = false;
|
|
5252
|
+
const ops = [
|
|
5253
|
+
{
|
|
5254
|
+
op: "append-array",
|
|
5255
|
+
path: ["plugins"],
|
|
5256
|
+
entries: [{ package: input.packageRef, options }],
|
|
5257
|
+
existingCount: plugins.length
|
|
5258
|
+
}
|
|
5259
|
+
];
|
|
5260
|
+
const permissions = [];
|
|
5261
|
+
if (input.search !== "off")
|
|
5262
|
+
permissions.push({ action: "websearch", resource: "*", effect: "allow" });
|
|
5263
|
+
if (input.fetch !== "off")
|
|
5264
|
+
permissions.push({ action: "deepseek_fetch", resource: "*", effect: "allow" });
|
|
5265
|
+
if (input.fetch === "forced")
|
|
5266
|
+
permissions.push({ action: "webfetch", resource: "*", effect: "allow" });
|
|
5267
|
+
const existingPermissions = Array.isArray(loaded.value.permissions) ? loaded.value.permissions.length : 0;
|
|
5268
|
+
if (permissions.length > 0) {
|
|
5269
|
+
ops.push({ op: "append-array", path: ["permissions"], entries: permissions, existingCount: existingPermissions });
|
|
5270
|
+
}
|
|
5271
|
+
if (input.search === "forced") {
|
|
5272
|
+
const currentProvider = isPlainObject(loaded.value.websearch) ? loaded.value.websearch.provider : undefined;
|
|
5273
|
+
ops.push({ op: "set", path: ["websearch", "provider"], value: "deepseek", previous: currentProvider });
|
|
5274
|
+
notes.push(currentProvider === undefined ? "websearch.provider is being set to deepseek (no previous value existed)." : `websearch.provider is being set to deepseek; the previous value "${String(currentProvider)}" is snapshotted for uninstall.`);
|
|
5275
|
+
}
|
|
5276
|
+
edits.push({ file: paths.config, format: "json", ops });
|
|
5277
|
+
notes.push("The plugin entry uses the object form so options are honored; auto-discovered plugins cannot receive options.");
|
|
5278
|
+
if (input.search === "optional") {
|
|
5279
|
+
notes.push("Search tier optional: the deepseek provider is registered but the built-in provider stays the default engine; " + "set websearch.provider to deepseek (or rerun with --search forced) to route search through DeepSeek.");
|
|
5280
|
+
}
|
|
5281
|
+
if (input.search === "forced" || input.fetch === "forced") {
|
|
5282
|
+
notes.push(`Forced tier replaces built-in routing. ${FORCED_COST_NOTE}`);
|
|
5283
|
+
}
|
|
5284
|
+
if (input.fetch === "preferred") {
|
|
5285
|
+
edits.push({
|
|
5286
|
+
file: paths.rules,
|
|
5287
|
+
format: "rules",
|
|
5288
|
+
block: buildRulesBlock(input.host, rulesLines(OPENCODE_TOOL_IDS, input.search, input.fetch))
|
|
5289
|
+
});
|
|
5290
|
+
notes.push(`Rules block upserted in ${paths.rules}; OpenCode loads AGENTS.md only (no CLAUDE.md fallback in v2).`);
|
|
5291
|
+
}
|
|
5292
|
+
return { host: "opencode", scope: input.scope, tiers, edits, notes, errors };
|
|
4952
5293
|
}
|
|
4953
|
-
var
|
|
4954
|
-
var
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
fetchTimeoutMs: "DEEPSEEK_FETCH_TIMEOUT_MS",
|
|
4966
|
-
maxRetries: "DEEPSEEK_MAX_RETRIES",
|
|
4967
|
-
retryBaseDelayMs: "DEEPSEEK_RETRY_BASE_DELAY_MS"
|
|
4968
|
-
};
|
|
4969
|
-
TEXT_ENV = {
|
|
4970
|
-
language: "DEEPSEEK_SEARCH_LANGUAGE"
|
|
4971
|
-
};
|
|
4972
|
-
DOMAINS_ENV = {
|
|
4973
|
-
blockedDomains: "DEEPSEEK_SEARCH_BLOCKED_DOMAINS",
|
|
4974
|
-
includeDomains: "DEEPSEEK_SEARCH_INCLUDE_DOMAINS",
|
|
4975
|
-
fetchAllowedDomains: "DEEPSEEK_FETCH_ALLOWED_DOMAINS",
|
|
4976
|
-
fetchBlockedDomains: "DEEPSEEK_FETCH_BLOCKED_DOMAINS"
|
|
4977
|
-
};
|
|
4978
|
-
FALSE_ONLY_ENV = {
|
|
4979
|
-
allowUnknownAuthority: "DEEPSEEK_SEARCH_ALLOW_UNKNOWN_AUTHORITY",
|
|
4980
|
-
fetchBlockPrivateHosts: "DEEPSEEK_FETCH_BLOCK_PRIVATE_HOSTS",
|
|
4981
|
-
searchEnabled: ENABLE_SEARCH_ENV,
|
|
4982
|
-
fetchEnabled: ENABLE_FETCH_ENV
|
|
4983
|
-
};
|
|
5294
|
+
var OPENCODE_TOOL_IDS;
|
|
5295
|
+
var init_opencode = __esm(() => {
|
|
5296
|
+
init_detect();
|
|
5297
|
+
init_formats();
|
|
5298
|
+
init_rules();
|
|
5299
|
+
init_value();
|
|
5300
|
+
init_ownership();
|
|
5301
|
+
init_paths();
|
|
5302
|
+
init_rulesContent();
|
|
5303
|
+
init_state();
|
|
5304
|
+
init_shared();
|
|
5305
|
+
OPENCODE_TOOL_IDS = { search: "websearch (provider deepseek)", fetch: "deepseek_fetch" };
|
|
4984
5306
|
});
|
|
4985
5307
|
|
|
4986
|
-
// src/
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
if (!apiKey)
|
|
4990
|
-
throw new Error(MISSING_API_KEY_MESSAGE);
|
|
4991
|
-
return apiKey;
|
|
4992
|
-
}
|
|
4993
|
-
var MISSING_API_KEY_MESSAGE;
|
|
4994
|
-
var init_auth2 = __esm(() => {
|
|
4995
|
-
init_auth();
|
|
4996
|
-
init_constants();
|
|
4997
|
-
MISSING_API_KEY_MESSAGE = `DeepSeek API key not found. Set ${API_KEY_ENV} in the MCP server's env configuration.`;
|
|
4998
|
-
});
|
|
5308
|
+
// src/pi/settings.ts
|
|
5309
|
+
var SETTINGS_NAMESPACE = "deepseekWebRetrieval";
|
|
5310
|
+
var init_settings = () => {};
|
|
4999
5311
|
|
|
5000
|
-
// src/
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
const
|
|
5004
|
-
const
|
|
5005
|
-
|
|
5006
|
-
return name.name;
|
|
5007
|
-
}
|
|
5008
|
-
return server.server.getClientVersion()?.name;
|
|
5312
|
+
// src/installer/hosts/pi.ts
|
|
5313
|
+
function piPaths(input) {
|
|
5314
|
+
const mcp = input.scope === "global" ? join2(input.home, ".pi/agent/mcp.json") : join2(input.cwd, ".pi/mcp.json");
|
|
5315
|
+
const settings = input.scope === "global" ? join2(input.home, ".pi/agent/settings.json") : join2(input.cwd, ".pi/settings.json");
|
|
5316
|
+
const rules = input.scope === "global" ? join2(input.home, ".pi/agent/AGENTS.md") : join2(input.cwd, "AGENTS.md");
|
|
5317
|
+
return { config: input.path === "mcp" ? mcp : settings, rules };
|
|
5009
5318
|
}
|
|
5010
|
-
function
|
|
5011
|
-
return
|
|
5319
|
+
function piSourceRef(packageRef) {
|
|
5320
|
+
return packageRef.startsWith("npm:") ? packageRef : `npm:${packageRef}`;
|
|
5012
5321
|
}
|
|
5013
|
-
function
|
|
5014
|
-
return
|
|
5322
|
+
function planPi(input, previous) {
|
|
5323
|
+
return input.path === "mcp" ? planPiMcp(input) : planPiExtension(input, previous);
|
|
5015
5324
|
}
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
}
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
"./core": "./src/core/index.ts",
|
|
5051
|
-
"./mcp": "./src/mcp/server.ts",
|
|
5052
|
-
"./pi": "./src/pi/extension.ts"
|
|
5053
|
-
},
|
|
5054
|
-
pi: {
|
|
5055
|
-
extensions: [
|
|
5056
|
-
"./src/pi/extension.ts"
|
|
5057
|
-
]
|
|
5058
|
-
},
|
|
5059
|
-
bin: {
|
|
5060
|
-
"deepseek-web-retrieval": "dist/cli.js"
|
|
5061
|
-
},
|
|
5062
|
-
files: [
|
|
5063
|
-
"index.ts",
|
|
5064
|
-
"src",
|
|
5065
|
-
"dist",
|
|
5066
|
-
"docs",
|
|
5067
|
-
"README.md",
|
|
5068
|
-
"LICENSE",
|
|
5069
|
-
"CHANGELOG.md",
|
|
5070
|
-
"examples"
|
|
5071
|
-
],
|
|
5072
|
-
scripts: {
|
|
5073
|
-
lint: "biome check --error-on-warnings .",
|
|
5074
|
-
"lint:ci": "biome ci --error-on-warnings .",
|
|
5075
|
-
"lint:fix": "biome check --write --error-on-warnings .",
|
|
5076
|
-
format: "biome format --write .",
|
|
5077
|
-
typecheck: "tsc --noEmit",
|
|
5078
|
-
build: "bun build src/mcp/cli.ts --target=node --format=esm --outdir=dist --external @modelcontextprotocol/server --external @modelcontextprotocol/server/stdio --external zod --external zod/v4",
|
|
5079
|
-
prepublishOnly: "bun run check:release",
|
|
5080
|
-
test: "bun test",
|
|
5081
|
-
"test:package": "node scripts/test-package.mjs",
|
|
5082
|
-
"test:e2e:keyless": "bun run test:e2e pi pi-extension installer reinit upgrade pkg capability-off",
|
|
5083
|
-
"check:release": "bun run check && bun run test:package && bun run test:e2e:keyless",
|
|
5084
|
-
"test:e2e": "bun run test/e2e/run.ts",
|
|
5085
|
-
"docs:matrix": "bun run scripts/render-matrix.ts",
|
|
5086
|
-
"test:coverage": "bun test --coverage",
|
|
5087
|
-
check: "bun run lint && bun run typecheck && bun run docs:matrix --check && bun run build && bun test"
|
|
5088
|
-
},
|
|
5089
|
-
peerDependencies: {
|
|
5090
|
-
"@earendil-works/pi-ai": "^1.3.0",
|
|
5091
|
-
"@earendil-works/pi-coding-agent": "^1.3.0",
|
|
5092
|
-
"@opencode/plugin": "^2.0.32"
|
|
5093
|
-
},
|
|
5094
|
-
peerDependenciesMeta: {
|
|
5095
|
-
"@earendil-works/pi-ai": {
|
|
5096
|
-
optional: true
|
|
5097
|
-
},
|
|
5098
|
-
"@earendil-works/pi-coding-agent": {
|
|
5099
|
-
optional: true
|
|
5100
|
-
},
|
|
5101
|
-
"@opencode/plugin": {
|
|
5102
|
-
optional: true
|
|
5103
|
-
}
|
|
5104
|
-
},
|
|
5105
|
-
devDependencies: {
|
|
5106
|
-
"@biomejs/biome": "2.5.15",
|
|
5107
|
-
"@earendil-works/pi-ai": "1.3.0",
|
|
5108
|
-
"@earendil-works/pi-coding-agent": "1.3.0",
|
|
5109
|
-
"@modelcontextprotocol/client": "^2.3.1",
|
|
5110
|
-
"@opencode/plugin": "2.0.32",
|
|
5111
|
-
"@types/bun": "1.4.0",
|
|
5112
|
-
"@types/node": "^26.6.5",
|
|
5113
|
-
typescript: "5.9.3"
|
|
5114
|
-
},
|
|
5115
|
-
engines: {
|
|
5116
|
-
bun: ">=1.3.0",
|
|
5117
|
-
node: ">=20"
|
|
5118
|
-
},
|
|
5119
|
-
dependencies: {
|
|
5120
|
-
"@modelcontextprotocol/server": "^2.3.1",
|
|
5121
|
-
"jsonc-parser": "^3.3.1",
|
|
5122
|
-
"smol-toml": "^1.9.1",
|
|
5123
|
-
zod: "^4"
|
|
5325
|
+
function planPiExtension(input, previous) {
|
|
5326
|
+
const paths = piPaths(input);
|
|
5327
|
+
const loaded = codecFor("json", input.host).load(paths.config);
|
|
5328
|
+
const tiers = { search: input.search, fetch: input.fetch };
|
|
5329
|
+
const header = planHeader(input, paths, loaded, "JSON");
|
|
5330
|
+
if (header.abort) {
|
|
5331
|
+
return { host: "pi", scope: input.scope, tiers, edits: [], notes: header.notes, errors: header.errors };
|
|
5332
|
+
}
|
|
5333
|
+
const notes = [...header.notes];
|
|
5334
|
+
const errors = [...header.errors];
|
|
5335
|
+
const edits = [];
|
|
5336
|
+
const packages = Array.isArray(loaded.value.packages) ? loaded.value.packages : [];
|
|
5337
|
+
const source = piSourceRef(input.packageRef);
|
|
5338
|
+
const trackedSources = appendedByPrevious(previous, ["packages"]).filter((entry) => typeof entry === "string");
|
|
5339
|
+
const sources = packages.filter((entry) => typeof entry === "string");
|
|
5340
|
+
const excusedSources = previous === undefined ? [] : [...trackedSources, source];
|
|
5341
|
+
const isExcusedSource = lineageMatcher(excusedSources);
|
|
5342
|
+
const foreignOwn = sources.filter((entry) => looksLikeOwnPackage(entry) && !isExcusedSource(entry));
|
|
5343
|
+
if (foreignOwn.length > 0) {
|
|
5344
|
+
errors.push({
|
|
5345
|
+
message: `${paths.config} already lists ${foreignOwn.join(", ")} — this package (or a fork of it) that this installer did not ` + "write. Remove it (pi remove <source>, or uninstall if it came from a previous init), then retry."
|
|
5346
|
+
});
|
|
5347
|
+
}
|
|
5348
|
+
if (previous !== undefined && sources.some((entry) => isExcusedSource(entry))) {
|
|
5349
|
+
notes.push("A previous init wrote the packages entry; re-applying reuses it (an unchanged source deduplicates, a changed " + "--package ref replaces the old source) and the original snapshots carry over.");
|
|
5350
|
+
}
|
|
5351
|
+
const forced = input.search === "forced" || input.fetch === "forced";
|
|
5352
|
+
if (forced) {
|
|
5353
|
+
const isOwnSource = lineageMatcher([...trackedSources, source]);
|
|
5354
|
+
const unexcludable = [...collectSources(loaded.value.packages), ...collectSources(loaded.value.extensions)].filter((entry) => !isOwnSource(entry) && looksWebCapable(entry, undefined));
|
|
5355
|
+
if (unexcludable.length > 0) {
|
|
5356
|
+
errors.push({
|
|
5357
|
+
message: `Forced cannot deliver exclusive routing: settings sources ${unexcludable.join(", ")} look web-capable, ` + "and Pi offers no per-package kill switch. Remove them (pi remove <source>) or choose a lower tier."
|
|
5358
|
+
});
|
|
5124
5359
|
}
|
|
5360
|
+
}
|
|
5361
|
+
if (errors.length > 0) {
|
|
5362
|
+
return { host: "pi", scope: input.scope, tiers, edits: [], notes, errors };
|
|
5363
|
+
}
|
|
5364
|
+
const ops = [
|
|
5365
|
+
{ op: "append-array", path: ["packages"], entries: [source], existingCount: packages.length }
|
|
5366
|
+
];
|
|
5367
|
+
const namespace = isPlainObject(loaded.value[SETTINGS_NAMESPACE]) ? loaded.value[SETTINGS_NAMESPACE] : {};
|
|
5368
|
+
if (input.search === "off") {
|
|
5369
|
+
ops.push({
|
|
5370
|
+
op: "set",
|
|
5371
|
+
path: [SETTINGS_NAMESPACE, "enableSearch"],
|
|
5372
|
+
value: false,
|
|
5373
|
+
previous: typeof namespace.enableSearch === "boolean" ? namespace.enableSearch : undefined
|
|
5374
|
+
});
|
|
5375
|
+
}
|
|
5376
|
+
if (input.fetch === "off") {
|
|
5377
|
+
ops.push({
|
|
5378
|
+
op: "set",
|
|
5379
|
+
path: [SETTINGS_NAMESPACE, "enableFetch"],
|
|
5380
|
+
value: false,
|
|
5381
|
+
previous: typeof namespace.enableFetch === "boolean" ? namespace.enableFetch : undefined
|
|
5382
|
+
});
|
|
5383
|
+
}
|
|
5384
|
+
edits.push({
|
|
5385
|
+
file: paths.config,
|
|
5386
|
+
format: "json",
|
|
5387
|
+
...input.scope === "global" ? { shared: true } : {},
|
|
5388
|
+
ops
|
|
5389
|
+
});
|
|
5390
|
+
notes.push("Pi resolves the packages source at session start; the first launch after apply fetches the package from npm " + "(pi update --extensions refreshes it later).");
|
|
5391
|
+
notes.push(`The extension reads ${API_KEY_ENV} from the Pi process environment; no env forwarding is needed. ` + "Set it in the shell that launches Pi.");
|
|
5392
|
+
if (input.search === "off" || input.fetch === "off") {
|
|
5393
|
+
notes.push(`Off capability switches are written into the "${SETTINGS_NAMESPACE}" settings namespace; the extension ` + "reads them at load (they outrank the DEEPSEEK_ENABLE_* environment variables).");
|
|
5394
|
+
}
|
|
5395
|
+
notes.push(`Extension tools are declared to the model with exposure "direct" by Pi's own default; no exposure key is needed.`);
|
|
5396
|
+
if (forced) {
|
|
5397
|
+
edits.push(...planMcpCompetitorDisables(input, notes, errors));
|
|
5398
|
+
}
|
|
5399
|
+
if (errors.length > 0) {
|
|
5400
|
+
return { host: "pi", scope: input.scope, tiers, edits: [], notes, errors };
|
|
5401
|
+
}
|
|
5402
|
+
if (input.search === "preferred" || input.fetch === "preferred") {
|
|
5403
|
+
edits.push({
|
|
5404
|
+
file: paths.rules,
|
|
5405
|
+
format: "rules",
|
|
5406
|
+
block: buildRulesBlock(input.host, rulesLines(RAW_MCP_TOOL_IDS, input.search, input.fetch))
|
|
5407
|
+
});
|
|
5408
|
+
notes.push(`Rules block upserted in ${paths.rules}; Pi loads AGENTS.md context files from the working directory and its parents.`);
|
|
5409
|
+
}
|
|
5410
|
+
if (forced) {
|
|
5411
|
+
notes.push("Forced tier: Pi 1.3.0 ships no built-in web tool, so the registered tools are the only web path (de-facto). " + FORCED_COST_NOTE);
|
|
5412
|
+
}
|
|
5413
|
+
return { host: "pi", scope: input.scope, tiers, edits, notes, errors };
|
|
5414
|
+
}
|
|
5415
|
+
function planCompetitorDisables(servers, competitors, notes, errors) {
|
|
5416
|
+
const ops = [];
|
|
5417
|
+
const excluded = [];
|
|
5418
|
+
const unexcludable = [];
|
|
5419
|
+
for (const name of competitors) {
|
|
5420
|
+
const current = servers[name];
|
|
5421
|
+
if (!isPlainObject(current)) {
|
|
5422
|
+
unexcludable.push(name);
|
|
5423
|
+
continue;
|
|
5424
|
+
}
|
|
5425
|
+
const previous = typeof current.enabled === "boolean" ? current.enabled : undefined;
|
|
5426
|
+
ops.push({ op: "set", path: ["mcpServers", name, "enabled"], value: false, previous });
|
|
5427
|
+
excluded.push(name);
|
|
5428
|
+
}
|
|
5429
|
+
if (excluded.length > 0) {
|
|
5430
|
+
notes.push(`Forced tier: web-capable MCP servers are disabled so DeepSeek routing is exclusive (${excluded.join(", ")}; ` + "prior enabled values are snapshotted for uninstall).");
|
|
5431
|
+
}
|
|
5432
|
+
if (unexcludable.length > 0) {
|
|
5433
|
+
errors.push({
|
|
5434
|
+
message: `Forced cannot deliver exclusive routing: mcpServers entries ${unexcludable.join(", ")} look web-capable ` + "but are not objects, so the plan cannot set their enabled key. Fix or remove them, or choose a lower tier."
|
|
5435
|
+
});
|
|
5436
|
+
}
|
|
5437
|
+
return ops;
|
|
5438
|
+
}
|
|
5439
|
+
function planMcpCompetitorDisables(input, notes, errors) {
|
|
5440
|
+
const mcpFile = input.scope === "global" ? join2(input.home, ".pi/agent/mcp.json") : join2(input.cwd, ".pi/mcp.json");
|
|
5441
|
+
const loaded = codecFor("json", input.host).load(mcpFile);
|
|
5442
|
+
if (loaded.parseError) {
|
|
5443
|
+
errors.push({ message: `${mcpFile} does not parse: ${loaded.parseError}` });
|
|
5444
|
+
return [];
|
|
5445
|
+
}
|
|
5446
|
+
const servers = isPlainObject(loaded.value.mcpServers) ? loaded.value.mcpServers : undefined;
|
|
5447
|
+
if (!isPlainObject(servers))
|
|
5448
|
+
return [];
|
|
5449
|
+
const { competitors, bystanders } = splitCompetitors(servers);
|
|
5450
|
+
const edits = [];
|
|
5451
|
+
if (competitors.length > 0) {
|
|
5452
|
+
const ops = planCompetitorDisables(servers, competitors, notes, errors);
|
|
5453
|
+
if (ops.length > 0) {
|
|
5454
|
+
edits.push({
|
|
5455
|
+
file: mcpFile,
|
|
5456
|
+
format: "json",
|
|
5457
|
+
...input.scope === "global" ? { shared: true } : {},
|
|
5458
|
+
ops
|
|
5459
|
+
});
|
|
5460
|
+
}
|
|
5461
|
+
}
|
|
5462
|
+
if (bystanders.length > 0)
|
|
5463
|
+
notes.push(othersStayRegisteredNote(bystanders));
|
|
5464
|
+
return edits;
|
|
5465
|
+
}
|
|
5466
|
+
function planPiMcp(input) {
|
|
5467
|
+
const paths = piPaths(input);
|
|
5468
|
+
const loaded = codecFor("json", input.host).load(paths.config);
|
|
5469
|
+
const tiers = { search: input.search, fetch: input.fetch };
|
|
5470
|
+
const header = planHeader(input, paths, loaded, "JSON");
|
|
5471
|
+
if (header.abort) {
|
|
5472
|
+
return { host: "pi", scope: input.scope, tiers, edits: [], notes: header.notes, errors: header.errors };
|
|
5473
|
+
}
|
|
5474
|
+
const notes = [...header.notes];
|
|
5475
|
+
const errors = [...header.errors];
|
|
5476
|
+
const edits = [];
|
|
5477
|
+
const env = { [API_KEY_ENV]: `\${${API_KEY_ENV}}` };
|
|
5478
|
+
if (input.search === "off")
|
|
5479
|
+
env[ENABLE_SEARCH_ENV] = "false";
|
|
5480
|
+
if (input.fetch === "off")
|
|
5481
|
+
env[ENABLE_FETCH_ENV] = "false";
|
|
5482
|
+
const entry = {
|
|
5483
|
+
command: "npx",
|
|
5484
|
+
args: ["-y", input.packageRef],
|
|
5485
|
+
env,
|
|
5486
|
+
exposure: "direct",
|
|
5487
|
+
enabled: true
|
|
5125
5488
|
};
|
|
5489
|
+
const servers = loaded.value.mcpServers;
|
|
5490
|
+
const existingEntry = isPlainObject(servers) ? servers.deepseek : undefined;
|
|
5491
|
+
const ops = [{ op: "set", path: ["mcpServers", "deepseek"], value: entry, previous: existingEntry }];
|
|
5492
|
+
edits.push({
|
|
5493
|
+
file: paths.config,
|
|
5494
|
+
format: "json",
|
|
5495
|
+
...input.scope === "global" ? { shared: true } : {},
|
|
5496
|
+
ops
|
|
5497
|
+
});
|
|
5498
|
+
if (isPlainObject(servers)) {
|
|
5499
|
+
if (servers.deepseek !== undefined)
|
|
5500
|
+
notes.push(replacingExistingEntryNote("mcpServers.deepseek"));
|
|
5501
|
+
const { competitors, bystanders } = splitCompetitors(servers);
|
|
5502
|
+
const forced = input.search === "forced" || input.fetch === "forced";
|
|
5503
|
+
if (forced) {
|
|
5504
|
+
ops.push(...planCompetitorDisables(servers, competitors, notes, errors));
|
|
5505
|
+
}
|
|
5506
|
+
const stayRegistered = forced ? bystanders : [...competitors, ...bystanders];
|
|
5507
|
+
if (stayRegistered.length > 0)
|
|
5508
|
+
notes.push(othersStayRegisteredNote(stayRegistered));
|
|
5509
|
+
}
|
|
5510
|
+
notes.push('"exposure": "direct" is required: Pi 1.3.0 defaults MCP tools to codemode, which hides them from the model.');
|
|
5511
|
+
notes.push(`\`${API_KEY_ENV}\` is written as "\${${API_KEY_ENV}}" so the key expands from the shell that launches Pi and stays out of the file.`);
|
|
5512
|
+
if (input.search === "preferred" || input.fetch === "preferred") {
|
|
5513
|
+
edits.push({
|
|
5514
|
+
file: paths.rules,
|
|
5515
|
+
format: "rules",
|
|
5516
|
+
block: buildRulesBlock(input.host, rulesLines(MCP_TOOL_IDS, input.search, input.fetch))
|
|
5517
|
+
});
|
|
5518
|
+
notes.push(`Rules block upserted in ${paths.rules}; Pi loads AGENTS.md context files from the working directory and its parents.`);
|
|
5519
|
+
}
|
|
5520
|
+
if (input.search === "forced" || input.fetch === "forced") {
|
|
5521
|
+
notes.push("Forced tier: Pi 1.3.0 ships no built-in web tool, so the registered tools are the only web path (de-facto). " + FORCED_COST_NOTE);
|
|
5522
|
+
}
|
|
5523
|
+
return { host: "pi", scope: input.scope, tiers, edits, notes, errors };
|
|
5524
|
+
}
|
|
5525
|
+
function collectSources(value) {
|
|
5526
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
5527
|
+
}
|
|
5528
|
+
var init_pi = __esm(() => {
|
|
5529
|
+
init_constants();
|
|
5530
|
+
init_settings();
|
|
5531
|
+
init_detect();
|
|
5532
|
+
init_formats();
|
|
5533
|
+
init_rules();
|
|
5534
|
+
init_value();
|
|
5535
|
+
init_ownership();
|
|
5536
|
+
init_paths();
|
|
5537
|
+
init_rulesContent();
|
|
5538
|
+
init_state();
|
|
5539
|
+
init_shared();
|
|
5126
5540
|
});
|
|
5127
5541
|
|
|
5128
|
-
// src/
|
|
5129
|
-
function
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
return
|
|
5134
|
-
|
|
5542
|
+
// src/installer/hosts/index.ts
|
|
5543
|
+
function planForHost(host, input) {
|
|
5544
|
+
try {
|
|
5545
|
+
const previous = readRecord(input);
|
|
5546
|
+
const planned = withCrossScopeGuard(HOST_PLANNERS[host](input, previous), input);
|
|
5547
|
+
return {
|
|
5548
|
+
...withSecretGuard(withOrphanReverts(withLegacySharedConfigGuard(planned, previous), previous)),
|
|
5549
|
+
previousRecord: previous
|
|
5550
|
+
};
|
|
5551
|
+
} catch (error) {
|
|
5552
|
+
if (!(error instanceof InstallRecordError))
|
|
5553
|
+
throw error;
|
|
5554
|
+
return {
|
|
5555
|
+
host,
|
|
5556
|
+
scope: input.scope,
|
|
5557
|
+
tiers: { search: input.search, fetch: input.fetch },
|
|
5558
|
+
edits: [],
|
|
5559
|
+
notes: [],
|
|
5560
|
+
errors: [{ message: error.message }]
|
|
5561
|
+
};
|
|
5562
|
+
}
|
|
5135
5563
|
}
|
|
5136
|
-
function
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
return {
|
|
5143
|
-
const markerLength = Array.from(FETCH_TRUNCATION_MARKER).length;
|
|
5144
|
-
if (maxChars <= markerLength) {
|
|
5145
|
-
return { content: truncate(document, maxChars), truncated: true };
|
|
5564
|
+
function withCrossScopeGuard(plan, input) {
|
|
5565
|
+
if (plan.errors.length > 0)
|
|
5566
|
+
return plan;
|
|
5567
|
+
try {
|
|
5568
|
+
assertFileOwnership(input, plan.edits);
|
|
5569
|
+
} catch (error) {
|
|
5570
|
+
return { ...plan, edits: [], reverts: undefined, errors: [{ message: String(error) }] };
|
|
5146
5571
|
}
|
|
5572
|
+
const other = input.scope === "global" ? "project" : "global";
|
|
5573
|
+
const overlap = claimedByOtherScope(input, plan.edits.map((edit2) => edit2.file));
|
|
5574
|
+
if (overlap.length === 0)
|
|
5575
|
+
return { ...plan, notes: [...plan.notes, ...fileOwnershipNotes(input, plan.edits)] };
|
|
5147
5576
|
return {
|
|
5148
|
-
|
|
5149
|
-
|
|
5577
|
+
...plan,
|
|
5578
|
+
edits: [],
|
|
5579
|
+
reverts: undefined,
|
|
5580
|
+
errors: [
|
|
5581
|
+
{
|
|
5582
|
+
message: `A ${plan.host} install is already recorded at the ${other} scope and claims the same file(s): ${overlap.join(", ")}. ` + `Uninstall it first (deepseek-web-retrieval uninstall --host ${plan.host} --scope ${other}), ` + `or re-run with --scope ${other} to update it in place — two scopes must not edit one file.`
|
|
5583
|
+
}
|
|
5584
|
+
]
|
|
5150
5585
|
};
|
|
5151
5586
|
}
|
|
5152
|
-
function
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5587
|
+
function withLegacySharedConfigGuard(plan, previous) {
|
|
5588
|
+
if (plan.errors.length > 0 || previous !== undefined)
|
|
5589
|
+
return plan;
|
|
5590
|
+
for (const edit2 of plan.edits) {
|
|
5591
|
+
if (edit2.format === "rules" || !edit2.shared)
|
|
5592
|
+
continue;
|
|
5593
|
+
for (const op of edit2.ops) {
|
|
5594
|
+
const old = op.previous;
|
|
5595
|
+
if (op.op !== "set" || !isPlainObject(old) || old.command !== "npx" || !Array.isArray(old.args) || old.args[0] !== "-y" || !Array.isArray(old.env_vars) || !old.env_vars.includes(API_KEY_ENV))
|
|
5596
|
+
continue;
|
|
5597
|
+
return {
|
|
5598
|
+
...plan,
|
|
5599
|
+
edits: [],
|
|
5600
|
+
reverts: undefined,
|
|
5601
|
+
errors: [
|
|
5602
|
+
{
|
|
5603
|
+
message: `${edit2.file} contains an installer-style registration without an install record for this location. ` + "It may belong to an older install in another project. Re-run init or uninstall from that original project and scope first; " + "if the registration was created manually, remove it before retrying. No existing registration was adopted."
|
|
5604
|
+
}
|
|
5605
|
+
]
|
|
5606
|
+
};
|
|
5607
|
+
}
|
|
5163
5608
|
}
|
|
5164
|
-
return
|
|
5165
|
-
|
|
5166
|
-
${content}`;
|
|
5167
|
-
}
|
|
5168
|
-
function normalizeHeading(value) {
|
|
5169
|
-
return value.trim().replace(/\s+/g, " ").replace(/\s+#+\s*$/, "").toLowerCase();
|
|
5609
|
+
return plan;
|
|
5170
5610
|
}
|
|
5171
|
-
function
|
|
5172
|
-
|
|
5611
|
+
function withSecretGuard(plan) {
|
|
5612
|
+
for (const edit2 of plan.edits) {
|
|
5613
|
+
if (edit2.format === "rules")
|
|
5614
|
+
continue;
|
|
5615
|
+
for (const op of edit2.ops) {
|
|
5616
|
+
if (op.op !== "set" || op.previous === undefined)
|
|
5617
|
+
continue;
|
|
5618
|
+
const offender = findSecretLike(op.previous, op.path.join("."));
|
|
5619
|
+
if (offender !== undefined) {
|
|
5620
|
+
return {
|
|
5621
|
+
...plan,
|
|
5622
|
+
edits: [],
|
|
5623
|
+
reverts: undefined,
|
|
5624
|
+
errors: [
|
|
5625
|
+
{
|
|
5626
|
+
message: `${edit2.file}: the existing value at ${op.path.join(".")} contains a credential-looking key ` + `(${offender}). The installer refuses to snapshot secrets into its state file. Move the key to ` + `environment indirection ("\${${API_KEY_ENV}}" expansion or env_vars) and re-run.`
|
|
5627
|
+
}
|
|
5628
|
+
]
|
|
5629
|
+
};
|
|
5630
|
+
}
|
|
5631
|
+
}
|
|
5632
|
+
}
|
|
5633
|
+
return plan;
|
|
5173
5634
|
}
|
|
5174
|
-
function
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
const
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5635
|
+
function findSecretLike(value, at) {
|
|
5636
|
+
if (typeof value !== "object" || value === null)
|
|
5637
|
+
return;
|
|
5638
|
+
for (const [key, child] of Object.entries(value)) {
|
|
5639
|
+
const path = `${at}.${key}`;
|
|
5640
|
+
if (/(api[_-]?key|token|secret|password|credential)/i.test(key) && typeof child === "string" && child.length > 0 && !/^\$\{[^}]+\}$/.test(child)) {
|
|
5641
|
+
return path;
|
|
5642
|
+
}
|
|
5643
|
+
const nested = findSecretLike(child, path);
|
|
5644
|
+
if (nested !== undefined)
|
|
5645
|
+
return nested;
|
|
5646
|
+
}
|
|
5647
|
+
return;
|
|
5181
5648
|
}
|
|
5182
|
-
function
|
|
5183
|
-
|
|
5649
|
+
function withOrphanReverts(plan, previous) {
|
|
5650
|
+
if (plan.errors.length > 0)
|
|
5651
|
+
return plan;
|
|
5652
|
+
if (previous === undefined)
|
|
5653
|
+
return plan;
|
|
5654
|
+
const reverts = [];
|
|
5655
|
+
for (const file of previous.files) {
|
|
5656
|
+
const edit2 = plan.edits.find((candidate) => candidate.file === file.file);
|
|
5657
|
+
if (edit2 === undefined) {
|
|
5658
|
+
reverts.push(file);
|
|
5659
|
+
continue;
|
|
5660
|
+
}
|
|
5661
|
+
if (edit2.format === "rules" || file.format === "rules")
|
|
5662
|
+
continue;
|
|
5663
|
+
const orphanOps = file.ops.flatMap((op) => {
|
|
5664
|
+
const claimed = edit2.ops.find((candidate) => candidate.op === op.op && candidate.path.join("\x00") === op.path.join("\x00"));
|
|
5665
|
+
if (op.op !== "append-array" || claimed === undefined || claimed.op !== "append-array") {
|
|
5666
|
+
return claimed === undefined ? [op] : [];
|
|
5667
|
+
}
|
|
5668
|
+
const dropped = (Array.isArray(op.applied) ? op.applied : []).filter((entry) => !(claimed.entries ?? []).some((wanted) => deepEqual(wanted, entry)));
|
|
5669
|
+
return dropped.length > 0 ? [{ ...op, applied: dropped }] : [];
|
|
5670
|
+
});
|
|
5671
|
+
if (orphanOps.length > 0)
|
|
5672
|
+
reverts.push({ ...file, ops: orphanOps });
|
|
5673
|
+
}
|
|
5674
|
+
if (reverts.length === 0)
|
|
5675
|
+
return plan;
|
|
5676
|
+
return { ...plan, reverts };
|
|
5184
5677
|
}
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5678
|
+
var HOST_PLANNERS, HOST_VERSION_PROBES;
|
|
5679
|
+
var init_hosts = __esm(() => {
|
|
5680
|
+
init_constants();
|
|
5681
|
+
init_value();
|
|
5682
|
+
init_ownership();
|
|
5683
|
+
init_state();
|
|
5684
|
+
init_claude_code();
|
|
5685
|
+
init_codex();
|
|
5686
|
+
init_opencode();
|
|
5687
|
+
init_pi();
|
|
5688
|
+
HOST_PLANNERS = {
|
|
5689
|
+
opencode: planOpenCode,
|
|
5690
|
+
pi: planPi,
|
|
5691
|
+
codex: planCodex,
|
|
5692
|
+
"claude-code": planClaudeCode
|
|
5693
|
+
};
|
|
5694
|
+
HOST_VERSION_PROBES = {
|
|
5695
|
+
codex: probeCodexVersion,
|
|
5696
|
+
"claude-code": probeClaudeCodeVersion
|
|
5697
|
+
};
|
|
5698
|
+
});
|
|
5699
|
+
|
|
5700
|
+
// src/installer/render.ts
|
|
5701
|
+
function renderPlan(plan) {
|
|
5702
|
+
const lines = [];
|
|
5703
|
+
lines.push(`Install plan: ${plan.host} (${plan.scope} scope)`);
|
|
5704
|
+
lines.push(`Tiers: search=${plan.tiers.search}, fetch=${plan.tiers.fetch}`);
|
|
5705
|
+
lines.push("");
|
|
5706
|
+
lines.push("Files:");
|
|
5707
|
+
for (const edit2 of plan.edits) {
|
|
5708
|
+
lines.push(` ${edit2.file}`);
|
|
5709
|
+
if (edit2.format === "rules") {
|
|
5710
|
+
lines.push(" upsert marked rules block");
|
|
5711
|
+
continue;
|
|
5194
5712
|
}
|
|
5195
|
-
|
|
5196
|
-
|
|
5713
|
+
for (const op of edit2.ops) {
|
|
5714
|
+
if (op.op === "set") {
|
|
5715
|
+
const was = op.previous === undefined ? "(absent)" : preview(op.previous);
|
|
5716
|
+
lines.push(` set ${op.path.join(".")} = ${preview(op.value)} (was ${was})`);
|
|
5717
|
+
} else {
|
|
5718
|
+
const count = (op.entries ?? []).length;
|
|
5719
|
+
const kept = op.existingCount !== undefined ? ` (${op.existingCount} existing kept)` : "";
|
|
5720
|
+
lines.push(` append ${count} entr${count === 1 ? "y" : "ies"} to ${op.path.join(".")}${kept}`);
|
|
5721
|
+
for (const entry of op.entries ?? [])
|
|
5722
|
+
lines.push(` + ${preview(entry)}`);
|
|
5723
|
+
}
|
|
5724
|
+
}
|
|
5725
|
+
}
|
|
5726
|
+
if (plan.reverts && plan.reverts.length > 0) {
|
|
5727
|
+
lines.push("");
|
|
5728
|
+
lines.push("Reverts (artifacts from the previous init that this plan no longer wants):");
|
|
5729
|
+
for (const revert of plan.reverts) {
|
|
5730
|
+
lines.push(` ${revert.file}`);
|
|
5731
|
+
if (revert.format === "rules") {
|
|
5732
|
+
lines.push(" remove rules block");
|
|
5733
|
+
continue;
|
|
5734
|
+
}
|
|
5735
|
+
for (const op of revert.ops) {
|
|
5736
|
+
if (op.op === "set") {
|
|
5737
|
+
lines.push(op.previous === undefined ? ` remove ${op.path.join(".")} (was not set before this install)` : ` restore ${op.path.join(".")} = ${preview(op.previous)}`);
|
|
5738
|
+
} else {
|
|
5739
|
+
const count = Array.isArray(op.applied) ? op.applied.length : 0;
|
|
5740
|
+
lines.push(` remove ${count} appended entr${count === 1 ? "y" : "ies"} from ${op.path.join(".")}`);
|
|
5741
|
+
}
|
|
5742
|
+
}
|
|
5197
5743
|
}
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5744
|
+
}
|
|
5745
|
+
if (plan.notes.length > 0) {
|
|
5746
|
+
lines.push("");
|
|
5747
|
+
lines.push("Notes:");
|
|
5748
|
+
for (const note of plan.notes)
|
|
5749
|
+
lines.push(` - ${note}`);
|
|
5750
|
+
}
|
|
5751
|
+
return lines.join(`
|
|
5202
5752
|
`);
|
|
5203
5753
|
}
|
|
5204
|
-
function
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
...result.title ? { title: result.title } : {},
|
|
5208
|
-
...result.content ? { content: result.content } : {},
|
|
5209
|
-
...result.published !== undefined ? { published: result.published } : {}
|
|
5210
|
-
};
|
|
5211
|
-
}
|
|
5212
|
-
function escapeBrackets(value) {
|
|
5213
|
-
return value.replaceAll("[", "\\[").replaceAll("]", "\\]");
|
|
5214
|
-
}
|
|
5215
|
-
function formatPublished(epochMs) {
|
|
5216
|
-
return new Date(epochMs).toISOString().slice(0, 10);
|
|
5754
|
+
function preview(value) {
|
|
5755
|
+
const text = JSON.stringify(value) ?? String(value);
|
|
5756
|
+
return text.length > 100 ? `${text.slice(0, 97)}...` : text;
|
|
5217
5757
|
}
|
|
5218
|
-
var init_format2 = __esm(() => {
|
|
5219
|
-
init_constants();
|
|
5220
|
-
});
|
|
5221
5758
|
|
|
5222
|
-
// src/
|
|
5223
|
-
function
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
if (
|
|
5237
|
-
throw new Error(
|
|
5238
|
-
}
|
|
5239
|
-
if (!url.hostname || !normalizeHostname(url.hostname)) {
|
|
5240
|
-
throw new Error("URL must include a host");
|
|
5759
|
+
// src/installer/uninstall.ts
|
|
5760
|
+
function uninstall(input) {
|
|
5761
|
+
return withOperationLock([statePathFor(input)], () => {
|
|
5762
|
+
const record = readRecord(input);
|
|
5763
|
+
if (record === undefined) {
|
|
5764
|
+
throw new Error(`No ${input.host} install is recorded for the ${input.scope} scope.`);
|
|
5765
|
+
}
|
|
5766
|
+
return withOperationLock(record.files.map((file) => file.file), () => uninstallLocked(input, record));
|
|
5767
|
+
});
|
|
5768
|
+
}
|
|
5769
|
+
function uninstallLocked(input, record) {
|
|
5770
|
+
assertFileOwnership(input, record.files);
|
|
5771
|
+
const other = input.scope === "global" ? "project" : "global";
|
|
5772
|
+
const overlap = claimedByOtherScope(input, record.files.map((file) => file.file));
|
|
5773
|
+
if (overlap.length > 0) {
|
|
5774
|
+
throw new Error(`A ${input.host} install is also recorded at the ${other} scope and claims the same file(s): ${overlap.join(", ")}. ` + `Uninstall the ${other}-scope install first so this rollback cannot clobber its entries.`);
|
|
5241
5775
|
}
|
|
5242
|
-
|
|
5243
|
-
|
|
5776
|
+
const lines = [];
|
|
5777
|
+
for (const file of record.files) {
|
|
5778
|
+
if (file.format !== "rules")
|
|
5779
|
+
continue;
|
|
5780
|
+
const loaded = codecFor("rules", record.host).load(file.file);
|
|
5781
|
+
if (loaded.parseError)
|
|
5782
|
+
throw new Error(`${file.file}: ${loaded.parseError}`);
|
|
5244
5783
|
}
|
|
5245
|
-
|
|
5246
|
-
}
|
|
5247
|
-
function normalizeResultUrl(value) {
|
|
5248
|
-
if (typeof value !== "string")
|
|
5249
|
-
return;
|
|
5250
|
-
const input = value.trim();
|
|
5251
|
-
if (!input || input.length > MAX_URL_CHARS)
|
|
5252
|
-
return;
|
|
5253
|
-
try {
|
|
5254
|
-
const url = new URL(input);
|
|
5255
|
-
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
5256
|
-
return;
|
|
5257
|
-
if (!url.hostname || url.username || url.password)
|
|
5258
|
-
return;
|
|
5259
|
-
return url.href;
|
|
5260
|
-
} catch {
|
|
5261
|
-
return;
|
|
5784
|
+
for (const file of [...record.files].reverse()) {
|
|
5785
|
+
lines.push(...revertRecordedFile(file, record.host, { label: "", whole: true }));
|
|
5262
5786
|
}
|
|
5787
|
+
releaseFileOwnership(input, record.files);
|
|
5788
|
+
deleteRecord(input);
|
|
5789
|
+
pruneEmptyDir(stateDir(input, input.scope));
|
|
5790
|
+
lines.push(`removed install record (${input.host}, ${input.scope})`);
|
|
5791
|
+
return lines;
|
|
5263
5792
|
}
|
|
5264
|
-
var
|
|
5265
|
-
|
|
5266
|
-
|
|
5793
|
+
var init_uninstall = __esm(() => {
|
|
5794
|
+
init_formats();
|
|
5795
|
+
init_operation_lock();
|
|
5796
|
+
init_ownership();
|
|
5797
|
+
init_paths();
|
|
5798
|
+
init_revert();
|
|
5799
|
+
init_state();
|
|
5267
5800
|
});
|
|
5268
5801
|
|
|
5269
|
-
// src/
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
return Number.isFinite(timestamp) ? timestamp : undefined;
|
|
5287
|
-
}
|
|
5288
|
-
function processResults(raw, options) {
|
|
5289
|
-
const results = [];
|
|
5290
|
-
const seen = new Set;
|
|
5291
|
-
for (const item of raw) {
|
|
5292
|
-
if (results.length >= options.maxResults)
|
|
5293
|
-
break;
|
|
5294
|
-
if (!item || typeof item !== "object")
|
|
5295
|
-
continue;
|
|
5296
|
-
const url = normalizeResultUrl(item.url);
|
|
5297
|
-
if (!url || seen.has(url))
|
|
5802
|
+
// src/installer/cli.ts
|
|
5803
|
+
var exports_cli = {};
|
|
5804
|
+
__export(exports_cli, {
|
|
5805
|
+
runInstallerCli: () => runInstallerCli,
|
|
5806
|
+
resolveConfigHome: () => resolveConfigHome,
|
|
5807
|
+
exitCodeForError: () => exitCodeForError
|
|
5808
|
+
});
|
|
5809
|
+
import { homedir as homedir2 } from "node:os";
|
|
5810
|
+
import { resolve as resolve2 } from "node:path";
|
|
5811
|
+
function parseFlags(args) {
|
|
5812
|
+
const values = new Map;
|
|
5813
|
+
const booleans = new Set;
|
|
5814
|
+
const problems = [];
|
|
5815
|
+
for (let i = 0;i < args.length; i += 1) {
|
|
5816
|
+
const arg = args[i];
|
|
5817
|
+
if (!arg.startsWith("--")) {
|
|
5818
|
+
problems.push(`unexpected argument "${arg}" (flags only)`);
|
|
5298
5819
|
continue;
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5820
|
+
}
|
|
5821
|
+
const eq = arg.indexOf("=");
|
|
5822
|
+
const name = eq === -1 ? arg.slice(2) : arg.slice(2, eq);
|
|
5823
|
+
if (BOOLEAN_FLAGS.has(name)) {
|
|
5824
|
+
if (eq !== -1) {
|
|
5825
|
+
problems.push(`--${name} does not accept a value; use --${name} to enable it or omit it`);
|
|
5302
5826
|
continue;
|
|
5303
|
-
|
|
5827
|
+
}
|
|
5828
|
+
if (booleans.has(name)) {
|
|
5829
|
+
problems.push(`--${name} was given more than once`);
|
|
5830
|
+
continue;
|
|
5831
|
+
}
|
|
5832
|
+
booleans.add(name);
|
|
5304
5833
|
continue;
|
|
5305
5834
|
}
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
if (knownAuthority) {
|
|
5309
|
-
if (authority < options.minAuthority)
|
|
5310
|
-
continue;
|
|
5311
|
-
} else if (!options.allowUnknownAuthority) {
|
|
5835
|
+
if (!VALUE_FLAGS.has(name)) {
|
|
5836
|
+
problems.push(`unknown flag --${name}`);
|
|
5312
5837
|
continue;
|
|
5313
5838
|
}
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5839
|
+
if (values.has(name)) {
|
|
5840
|
+
problems.push(`--${name} was given more than once`);
|
|
5841
|
+
continue;
|
|
5842
|
+
}
|
|
5843
|
+
if (eq !== -1) {
|
|
5844
|
+
values.set(name, arg.slice(eq + 1));
|
|
5845
|
+
continue;
|
|
5846
|
+
}
|
|
5847
|
+
const next = args[i + 1];
|
|
5848
|
+
if (next === undefined || next.startsWith("--")) {
|
|
5849
|
+
problems.push(`--${name} requires a value`);
|
|
5850
|
+
continue;
|
|
5851
|
+
}
|
|
5852
|
+
values.set(name, next);
|
|
5853
|
+
i += 1;
|
|
5324
5854
|
}
|
|
5325
|
-
return
|
|
5326
|
-
}
|
|
5327
|
-
function optionalText2(value) {
|
|
5328
|
-
if (typeof value !== "string")
|
|
5329
|
-
return;
|
|
5330
|
-
const trimmed = value.trim();
|
|
5331
|
-
return trimmed ? trimmed : undefined;
|
|
5855
|
+
return { values, booleans, problems };
|
|
5332
5856
|
}
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5857
|
+
async function runInstallerCli(argv, io = {}) {
|
|
5858
|
+
const out = io.out ?? ((line) => console.log(line));
|
|
5859
|
+
const err = io.err ?? ((line) => console.error(line));
|
|
5860
|
+
const [command, ...rest] = argv;
|
|
5861
|
+
if (command === "version" || command === "--version") {
|
|
5862
|
+
out(TOOL_VERSION);
|
|
5863
|
+
return 0;
|
|
5864
|
+
}
|
|
5865
|
+
if (command === undefined) {
|
|
5866
|
+
err(USAGE);
|
|
5867
|
+
return EXIT_USAGE2;
|
|
5868
|
+
}
|
|
5869
|
+
if (command === "help" || command === "--help") {
|
|
5870
|
+
out(USAGE);
|
|
5871
|
+
return 0;
|
|
5872
|
+
}
|
|
5873
|
+
const flags = parseFlags(rest);
|
|
5874
|
+
if (flags.problems.length > 0) {
|
|
5875
|
+
for (const problem of flags.problems)
|
|
5876
|
+
err(`error: ${problem}`);
|
|
5877
|
+
err(`
|
|
5878
|
+
${USAGE}`);
|
|
5879
|
+
return EXIT_USAGE2;
|
|
5880
|
+
}
|
|
5881
|
+
const cwd = resolve2(flags.values.get("cwd") ?? process.cwd());
|
|
5882
|
+
const home = process.env.HOME ?? homedir2();
|
|
5883
|
+
const platform = io.platform ?? process.platform;
|
|
5884
|
+
const roots = { home, cwd, platform };
|
|
5885
|
+
const configHome2 = resolveConfigHome(process.env, home, platform);
|
|
5886
|
+
if (command === "list") {
|
|
5887
|
+
let records;
|
|
5345
5888
|
try {
|
|
5346
|
-
|
|
5889
|
+
records = listRecords({ ...roots, configHome: configHome2 });
|
|
5347
5890
|
} catch (error) {
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
throw error;
|
|
5351
|
-
}
|
|
5352
|
-
await sleep(retryDelay(error.retryAfterMs, opts.retry.baseDelayMs, attempt), opts.signal);
|
|
5891
|
+
err(error instanceof Error ? error.message : String(error));
|
|
5892
|
+
return 1;
|
|
5353
5893
|
}
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
async function requestOnce(endpoint, body, operation2, opts, fetchImpl) {
|
|
5358
|
-
return fetchWithTimeout(endpoint, {
|
|
5359
|
-
method: "POST",
|
|
5360
|
-
headers: {
|
|
5361
|
-
Authorization: `Bearer ${opts.auth.apiKey}`,
|
|
5362
|
-
"Content-Type": "application/json"
|
|
5363
|
-
},
|
|
5364
|
-
body: JSON.stringify(body)
|
|
5365
|
-
}, opts.signal, opts.timeoutMs, operation2, fetchImpl, async (response) => {
|
|
5366
|
-
if (!response.ok) {
|
|
5367
|
-
const text2 = await readBoundedText(response, MAX_ERROR_BODY_CHARS);
|
|
5368
|
-
throw new DeepSeekApiError({
|
|
5369
|
-
operation: operation2,
|
|
5370
|
-
status: response.status,
|
|
5371
|
-
body: text2,
|
|
5372
|
-
retryAfterMs: parseRetryAfter(response.headers.get("retry-after"))
|
|
5373
|
-
});
|
|
5894
|
+
if (records.length === 0) {
|
|
5895
|
+
out("No installs recorded.");
|
|
5896
|
+
return 0;
|
|
5374
5897
|
}
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5898
|
+
if (readingLegacyGlobalRecords({ ...roots, configHome: configHome2 })) {
|
|
5899
|
+
out(`Global install records are being read from the legacy ${legacyGlobalStateDir(roots)} ` + "(written before the Windows config home moved to %APPDATA%).");
|
|
5900
|
+
}
|
|
5901
|
+
for (const record of records) {
|
|
5902
|
+
const by = record.installerVersion ? ` by v${record.installerVersion}` : "";
|
|
5903
|
+
out(`${record.host} (${record.scope}) — search=${record.tiers.search}, fetch=${record.tiers.fetch}, installed ${record.createdAt}${by}`);
|
|
5904
|
+
for (const file of record.files)
|
|
5905
|
+
out(` ${file.created ? "created" : "modified"} ${file.file}`);
|
|
5906
|
+
}
|
|
5907
|
+
return 0;
|
|
5908
|
+
}
|
|
5909
|
+
if (command === "config") {
|
|
5910
|
+
return runConfigCommand(flags.values.get("search"), { out, err });
|
|
5911
|
+
}
|
|
5912
|
+
if (command === "uninstall") {
|
|
5913
|
+
const required2 = requireInput(flags, cwd, home, configHome2, platform, false);
|
|
5914
|
+
if (!required2.ok)
|
|
5915
|
+
return usageError(required2.problems, err);
|
|
5378
5916
|
try {
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5917
|
+
for (const line of uninstall(required2.input))
|
|
5918
|
+
out(line);
|
|
5919
|
+
return 0;
|
|
5920
|
+
} catch (error) {
|
|
5921
|
+
err(error instanceof Error ? error.message : String(error));
|
|
5922
|
+
return 1;
|
|
5382
5923
|
}
|
|
5383
|
-
}
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
const
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5924
|
+
}
|
|
5925
|
+
if (command !== "init") {
|
|
5926
|
+
err(`Unknown command "${command}".
|
|
5927
|
+
|
|
5928
|
+
${USAGE}`);
|
|
5929
|
+
return EXIT_USAGE2;
|
|
5930
|
+
}
|
|
5931
|
+
const required = requireInput(flags, cwd, home, configHome2, platform, true);
|
|
5932
|
+
if (!required.ok)
|
|
5933
|
+
return usageError(required.problems, err);
|
|
5934
|
+
const { input } = required;
|
|
5935
|
+
const probe = HOST_VERSION_PROBES[input.host];
|
|
5936
|
+
const hostVersion = probe?.(err);
|
|
5937
|
+
const plan = planForHost(input.host, hostVersion === undefined ? input : { ...input, hostVersion });
|
|
5938
|
+
if (plan.errors.length > 0) {
|
|
5939
|
+
for (const error of plan.errors)
|
|
5940
|
+
err(`error: ${error.message}`);
|
|
5941
|
+
err("Nothing was written.");
|
|
5942
|
+
return 1;
|
|
5943
|
+
}
|
|
5944
|
+
out(renderPlan(plan));
|
|
5945
|
+
if (!flags.booleans.has("apply")) {
|
|
5946
|
+
out(`
|
|
5947
|
+
Plan only — re-run with --apply to write these changes.`);
|
|
5948
|
+
return 0;
|
|
5949
|
+
}
|
|
5397
5950
|
try {
|
|
5398
|
-
const
|
|
5399
|
-
|
|
5951
|
+
const result = applyPlan(plan, input);
|
|
5952
|
+
for (const line of result.lines)
|
|
5953
|
+
out(line);
|
|
5954
|
+
return 0;
|
|
5400
5955
|
} catch (error) {
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
if (signal.aborted)
|
|
5404
|
-
throw abortReason(signal, operation2);
|
|
5405
|
-
throw error;
|
|
5406
|
-
} finally {
|
|
5407
|
-
if (timer !== undefined)
|
|
5408
|
-
clearTimeout(timer);
|
|
5409
|
-
signal.removeEventListener("abort", forwardAbort);
|
|
5410
|
-
}
|
|
5411
|
-
}
|
|
5412
|
-
function parseRetryAfter(value) {
|
|
5413
|
-
if (!value)
|
|
5414
|
-
return;
|
|
5415
|
-
const trimmed = value.trim();
|
|
5416
|
-
if (!trimmed)
|
|
5417
|
-
return;
|
|
5418
|
-
if (/^\d+$/.test(trimmed)) {
|
|
5419
|
-
return Math.min(MAX_RETRY_DELAY_MS, Number(trimmed) * 1000);
|
|
5956
|
+
err(error instanceof Error ? error.message : String(error));
|
|
5957
|
+
return exitCodeForError(error);
|
|
5420
5958
|
}
|
|
5421
|
-
const timestamp = Date.parse(trimmed);
|
|
5422
|
-
if (!Number.isFinite(timestamp))
|
|
5423
|
-
return;
|
|
5424
|
-
return Math.min(MAX_RETRY_DELAY_MS, Math.max(0, timestamp - Date.now()));
|
|
5425
|
-
}
|
|
5426
|
-
function retryDelay(retryAfterMs, baseDelayMs, attempt) {
|
|
5427
|
-
if (retryAfterMs !== undefined)
|
|
5428
|
-
return retryAfterMs;
|
|
5429
|
-
return Math.min(MAX_RETRY_DELAY_MS, baseDelayMs * 2 ** attempt);
|
|
5430
5959
|
}
|
|
5431
|
-
function
|
|
5432
|
-
|
|
5433
|
-
return Promise.resolve();
|
|
5434
|
-
return new Promise((resolve3, reject) => {
|
|
5435
|
-
const timer = setTimeout(() => {
|
|
5436
|
-
signal.removeEventListener("abort", onAbort);
|
|
5437
|
-
resolve3();
|
|
5438
|
-
}, ms);
|
|
5439
|
-
const onAbort = () => {
|
|
5440
|
-
clearTimeout(timer);
|
|
5441
|
-
reject(abortReason(signal, "DeepSeek request"));
|
|
5442
|
-
};
|
|
5443
|
-
if (signal.aborted) {
|
|
5444
|
-
clearTimeout(timer);
|
|
5445
|
-
reject(abortReason(signal, "DeepSeek request"));
|
|
5446
|
-
return;
|
|
5447
|
-
}
|
|
5448
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
5449
|
-
});
|
|
5960
|
+
function exitCodeForError(error) {
|
|
5961
|
+
return error instanceof ApplyError && error.status === "recovery_required" ? EXIT_RECOVERY : EXIT_FAILURE;
|
|
5450
5962
|
}
|
|
5451
|
-
function
|
|
5452
|
-
|
|
5963
|
+
function usageError(problems, err) {
|
|
5964
|
+
for (const problem of problems)
|
|
5965
|
+
err(`error: ${problem}`);
|
|
5966
|
+
err(`
|
|
5967
|
+
${USAGE}`);
|
|
5968
|
+
return EXIT_USAGE2;
|
|
5453
5969
|
}
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
const reader = response.body.getReader();
|
|
5458
|
-
const decoder = new TextDecoder;
|
|
5459
|
-
const limit = maxChars + 1;
|
|
5460
|
-
let text = "";
|
|
5461
|
-
let chars = 0;
|
|
5462
|
-
try {
|
|
5463
|
-
while (chars < limit) {
|
|
5464
|
-
const { done, value } = await reader.read();
|
|
5465
|
-
if (done)
|
|
5466
|
-
break;
|
|
5467
|
-
const chunk = decoder.decode(value, { stream: true });
|
|
5468
|
-
text += chunk;
|
|
5469
|
-
chars += Array.from(chunk).length;
|
|
5470
|
-
}
|
|
5471
|
-
text += decoder.decode();
|
|
5472
|
-
} finally {
|
|
5473
|
-
await reader.cancel().catch(() => {
|
|
5474
|
-
return;
|
|
5475
|
-
});
|
|
5476
|
-
reader.releaseLock();
|
|
5477
|
-
}
|
|
5478
|
-
return truncate(text, limit);
|
|
5970
|
+
function resolveConfigHome(env, home, platform = process.platform) {
|
|
5971
|
+
const xdgTrusted = env.XDG_CONFIG_HOME !== undefined && env.XDG_CONFIG_HOME !== "" && home === (env.HOME ?? "");
|
|
5972
|
+
return userConfigHome({ env: xdgTrusted ? env : { ...env, XDG_CONFIG_HOME: undefined }, home, platform });
|
|
5479
5973
|
}
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
}
|
|
5501
|
-
text += decoder.decode(value, { stream: true });
|
|
5974
|
+
function requireInput(flags, cwd, home, configHome2, platform, needsTiers) {
|
|
5975
|
+
const problems = [];
|
|
5976
|
+
const host = flags.values.get("host");
|
|
5977
|
+
const scope = flags.values.get("scope");
|
|
5978
|
+
const search = needsTiers ? flags.values.get("search") ?? "" : "optional";
|
|
5979
|
+
const fetch = needsTiers ? flags.values.get("fetch") ?? "" : "optional";
|
|
5980
|
+
const path = flags.values.get("path");
|
|
5981
|
+
if (host === undefined)
|
|
5982
|
+
problems.push(`--host is required (one of: ${HOSTS.join(", ")})`);
|
|
5983
|
+
else if (!HOSTS.includes(host))
|
|
5984
|
+
problems.push(`invalid --host value "${host}" (one of: ${HOSTS.join(", ")})`);
|
|
5985
|
+
if (scope === undefined)
|
|
5986
|
+
problems.push(`--scope is required (one of: ${SCOPES.join(", ")})`);
|
|
5987
|
+
else if (!SCOPES.includes(scope))
|
|
5988
|
+
problems.push(`invalid --scope value "${scope}" (one of: ${SCOPES.join(", ")})`);
|
|
5989
|
+
if (path !== undefined) {
|
|
5990
|
+
if (host !== "pi") {
|
|
5991
|
+
problems.push(`--path is only supported for host pi (${host ?? "no host"} has a single integration path)`);
|
|
5992
|
+
} else if (!PATH_CHOICES.includes(path)) {
|
|
5993
|
+
problems.push(`invalid --path value "${path}" (one of: ${PATH_CHOICES.join(", ")})`);
|
|
5502
5994
|
}
|
|
5503
|
-
text += decoder.decode();
|
|
5504
|
-
} finally {
|
|
5505
|
-
await reader.cancel().catch(() => {
|
|
5506
|
-
return;
|
|
5507
|
-
});
|
|
5508
|
-
reader.releaseLock();
|
|
5509
5995
|
}
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5996
|
+
if (needsTiers) {
|
|
5997
|
+
if (!TIERS.includes(search)) {
|
|
5998
|
+
problems.push(flags.values.has("search") ? `invalid --search value "${search}" (one of: ${TIERS.join(", ")})` : `--search is required (one of: ${TIERS.join(", ")})`);
|
|
5999
|
+
}
|
|
6000
|
+
if (!TIERS.includes(fetch)) {
|
|
6001
|
+
problems.push(flags.values.has("fetch") ? `invalid --fetch value "${fetch}" (one of: ${TIERS.join(", ")})` : `--fetch is required (one of: ${TIERS.join(", ")})`);
|
|
6002
|
+
}
|
|
6003
|
+
}
|
|
6004
|
+
if (home === "")
|
|
6005
|
+
problems.push("HOME is not set and the user home cannot be resolved; cannot proceed");
|
|
6006
|
+
if (problems.length > 0)
|
|
6007
|
+
return { ok: false, problems };
|
|
6008
|
+
const packageRef = flags.values.get("package") ?? "deepseek-web-retrieval";
|
|
6009
|
+
return {
|
|
6010
|
+
ok: true,
|
|
6011
|
+
input: {
|
|
6012
|
+
host,
|
|
6013
|
+
scope,
|
|
6014
|
+
search,
|
|
6015
|
+
fetch,
|
|
6016
|
+
packageRef: flags.booleans.has("pin") ? pinPackageRef(packageRef, TOOL_VERSION) : packageRef,
|
|
6017
|
+
home,
|
|
6018
|
+
cwd,
|
|
6019
|
+
configHome: configHome2,
|
|
6020
|
+
platform,
|
|
6021
|
+
...path !== undefined ? { path } : {}
|
|
5531
6022
|
}
|
|
5532
6023
|
};
|
|
6024
|
+
}
|
|
6025
|
+
var HOSTS, HOST_CHOICES, SCOPES, TIERS, EXIT_FAILURE = 1, EXIT_USAGE2 = 2, EXIT_RECOVERY = 3, USAGE, VALUE_FLAGS, BOOLEAN_FLAGS, PATH_CHOICES;
|
|
6026
|
+
var init_cli = __esm(() => {
|
|
6027
|
+
init_config_file();
|
|
6028
|
+
init_version();
|
|
6029
|
+
init_apply();
|
|
6030
|
+
init_config_command();
|
|
6031
|
+
init_hosts();
|
|
6032
|
+
init_ownership();
|
|
6033
|
+
init_state();
|
|
6034
|
+
init_uninstall();
|
|
6035
|
+
HOSTS = Object.keys(HOST_PLANNERS);
|
|
6036
|
+
HOST_CHOICES = `<${HOSTS.join("|")}>`;
|
|
6037
|
+
SCOPES = ["project", "global"];
|
|
6038
|
+
TIERS = ["optional", "preferred", "forced", "off"];
|
|
6039
|
+
USAGE = `Usage:
|
|
6040
|
+
deepseek-web-retrieval init --host ${HOST_CHOICES} --scope <project|global> --search <optional|preferred|forced|off> --fetch <optional|preferred|forced|off> [--path <extension|mcp>] [--package <npm ref>] [--pin] [--cwd <dir>] [--apply]
|
|
6041
|
+
deepseek-web-retrieval uninstall --host ${HOST_CHOICES} --scope <project|global> [--cwd <dir>]
|
|
6042
|
+
deepseek-web-retrieval list [--cwd <dir>]
|
|
6043
|
+
deepseek-web-retrieval config [--search <query>]
|
|
6044
|
+
deepseek-web-retrieval version
|
|
6045
|
+
|
|
6046
|
+
init prints the full plan (every file, old value → new value) and writes nothing
|
|
6047
|
+
until --apply is passed. uninstall restores what the plan applied. --path picks
|
|
6048
|
+
the integration mechanism for hosts with more than one (Pi: extension by
|
|
6049
|
+
default, mcp for the stdio MCP server). --package overrides the package ref
|
|
6050
|
+
written into host config; the Pi extension path turns it into the packages
|
|
6051
|
+
source (a bare name is prefixed with npm:, a ref that already starts with npm:
|
|
6052
|
+
is written as-is). --pin appends the running CLI's version to the package ref
|
|
6053
|
+
(a ref already ending in a version keeps it), so hosts resolve that exact
|
|
6054
|
+
release instead of the latest publish; re-run init --pin to move the pin.
|
|
6055
|
+
|
|
6056
|
+
config prints the effective configuration — every knob with its value and its
|
|
6057
|
+
source (default, shared file, or environment), resolved the way the MCP server
|
|
6058
|
+
resolves it — and exits 1 when the config file or the DEEPSEEK_* environment
|
|
6059
|
+
has problems (unknown or rejected keys, unparseable JSON, set-but-unusable
|
|
6060
|
+
variable values; a missing file is fine). config --search <query>
|
|
6061
|
+
runs one billed search through that configuration to verify the key, config,
|
|
6062
|
+
and network end to end; it exits 1 when the key is missing or the search fails.
|
|
6063
|
+
|
|
6064
|
+
Exit codes: 0 success; 1 refused or failed with nothing written (a failed apply
|
|
6065
|
+
is fully rolled back); 2 invalid command line; 3 recovery required — a failed
|
|
6066
|
+
apply could not roll back completely, follow the reported guidance before
|
|
6067
|
+
re-running.`;
|
|
6068
|
+
VALUE_FLAGS = new Set(["host", "scope", "search", "fetch", "path", "package", "cwd"]);
|
|
6069
|
+
BOOLEAN_FLAGS = new Set(["apply", "pin"]);
|
|
6070
|
+
PATH_CHOICES = ["extension", "mcp"];
|
|
5533
6071
|
});
|
|
5534
6072
|
|
|
5535
|
-
// src/
|
|
5536
|
-
function
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
if (!trimmed)
|
|
5542
|
-
throw new Error("DeepSeek search query must not be empty");
|
|
5543
|
-
if (Array.from(trimmed).length > MAX_QUERY_CHARS) {
|
|
5544
|
-
throw new Error(`DeepSeek search query must be ${MAX_QUERY_CHARS} characters or fewer`);
|
|
5545
|
-
}
|
|
5546
|
-
const builtQuery = buildQuery(trimmed, options);
|
|
5547
|
-
const builtQueryLength = Array.from(builtQuery).length;
|
|
5548
|
-
if (builtQueryLength > MAX_QUERY_CHARS) {
|
|
5549
|
-
throw new Error(`DeepSeek search query must be ${MAX_QUERY_CHARS} characters or fewer after applying ` + `blockedDomains, includeDomains, and language ` + `(built query is ${builtQueryLength} characters)`);
|
|
5550
|
-
}
|
|
5551
|
-
const timeout = effectiveTimeout(options.timeoutMs, request.timeoutMs);
|
|
5552
|
-
const payload = await postJson(SEARCH_ENDPOINT, {
|
|
5553
|
-
query: builtQuery,
|
|
5554
|
-
count: options.count,
|
|
5555
|
-
need_content: false
|
|
5556
|
-
}, "DeepSeek Search", { ...request, timeoutMs: timeout });
|
|
5557
|
-
if (!Array.isArray(payload.results)) {
|
|
5558
|
-
throw new Error("DeepSeek Search returned a response without a results array");
|
|
5559
|
-
}
|
|
5560
|
-
return processResults(payload.results, options);
|
|
6073
|
+
// src/mcp/auth.ts
|
|
6074
|
+
function requireApiKey(env) {
|
|
6075
|
+
const apiKey = apiKeyFromEnv(env);
|
|
6076
|
+
if (!apiKey)
|
|
6077
|
+
throw new Error(MISSING_API_KEY_MESSAGE);
|
|
6078
|
+
return apiKey;
|
|
5561
6079
|
}
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
const
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
throw new Error("DeepSeek Fetch returned no page content (the page may be empty or unavailable)");
|
|
6080
|
+
var init_auth2 = __esm(() => {
|
|
6081
|
+
init_auth();
|
|
6082
|
+
});
|
|
6083
|
+
|
|
6084
|
+
// src/mcp/client.ts
|
|
6085
|
+
import { CLIENT_INFO_META_KEY } from "@modelcontextprotocol/server";
|
|
6086
|
+
function clientNameOf(server, ctx) {
|
|
6087
|
+
const envelope = ctx.mcpReq.envelope;
|
|
6088
|
+
const name = envelope?.[CLIENT_INFO_META_KEY];
|
|
6089
|
+
if (typeof name === "object" && name !== null && "name" in name && typeof name.name === "string") {
|
|
6090
|
+
return name.name;
|
|
5574
6091
|
}
|
|
5575
|
-
return
|
|
6092
|
+
return server.server.getClientVersion()?.name;
|
|
5576
6093
|
}
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
throw new Error('"url" must be a string or URL');
|
|
5580
|
-
const page = await fetchPage(url instanceof URL ? url : parseFetchUrl(url), options, request);
|
|
5581
|
-
const formatted = formatFetchedContent(page.title, page.content, options.maxChars);
|
|
5582
|
-
return {
|
|
5583
|
-
url: page.url,
|
|
5584
|
-
title: page.title,
|
|
5585
|
-
content: formatted.content,
|
|
5586
|
-
truncated: formatted.truncated
|
|
5587
|
-
};
|
|
6094
|
+
function isJsonReplacingClient(name) {
|
|
6095
|
+
return name !== undefined && JSON_REPLACING_CLIENT_NAMES.has(name);
|
|
5588
6096
|
}
|
|
5589
|
-
function
|
|
5590
|
-
|
|
5591
|
-
return {
|
|
5592
|
-
searchWeb: (query, options, request) => searchWeb(query, options, bind(request)),
|
|
5593
|
-
fetchPage: (url, options, request) => fetchPage(url, options, bind(request)),
|
|
5594
|
-
fetchDocument: (url, options, request) => fetchDocument(url, options, bind(request))
|
|
5595
|
-
};
|
|
6097
|
+
function omitStructuredContent(server, ctx) {
|
|
6098
|
+
return isJsonReplacingClient(clientNameOf(server, ctx));
|
|
5596
6099
|
}
|
|
5597
|
-
var
|
|
6100
|
+
var JSON_REPLACING_CLIENT_NAMES;
|
|
5598
6101
|
var init_client2 = __esm(() => {
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
6102
|
+
JSON_REPLACING_CLIENT_NAMES = new Set(["codex-mcp-client"]);
|
|
6103
|
+
});
|
|
6104
|
+
|
|
6105
|
+
// package.json
|
|
6106
|
+
var package_default;
|
|
6107
|
+
var init_package = __esm(() => {
|
|
6108
|
+
package_default = {
|
|
6109
|
+
$schema: "https://json.schemastore.org/package.json",
|
|
6110
|
+
name: "deepseek-web-retrieval",
|
|
6111
|
+
version: "0.8.0",
|
|
6112
|
+
description: "Token-efficient DeepSeek Search and Fetch for coding agents, starting with OpenCode v2.",
|
|
6113
|
+
type: "module",
|
|
6114
|
+
license: "MIT",
|
|
6115
|
+
repository: {
|
|
6116
|
+
type: "git",
|
|
6117
|
+
url: "git+https://github.com/gwokhou/deepseek-web-retrieval.git"
|
|
6118
|
+
},
|
|
6119
|
+
homepage: "https://github.com/gwokhou/deepseek-web-retrieval#readme",
|
|
6120
|
+
bugs: "https://github.com/gwokhou/deepseek-web-retrieval/issues",
|
|
6121
|
+
keywords: [
|
|
6122
|
+
"opencode",
|
|
6123
|
+
"opencode-plugin",
|
|
6124
|
+
"pi",
|
|
6125
|
+
"pi-extension",
|
|
6126
|
+
"deepseek",
|
|
6127
|
+
"websearch",
|
|
6128
|
+
"webfetch",
|
|
6129
|
+
"coding-agent",
|
|
6130
|
+
"mcp"
|
|
6131
|
+
],
|
|
6132
|
+
exports: {
|
|
6133
|
+
".": "./index.ts",
|
|
6134
|
+
"./core": "./src/core/index.ts",
|
|
6135
|
+
"./mcp": "./src/mcp/server.ts",
|
|
6136
|
+
"./pi": "./src/pi/extension.ts"
|
|
6137
|
+
},
|
|
6138
|
+
pi: {
|
|
6139
|
+
extensions: [
|
|
6140
|
+
"./src/pi/extension.ts"
|
|
6141
|
+
]
|
|
6142
|
+
},
|
|
6143
|
+
bin: {
|
|
6144
|
+
"deepseek-web-retrieval": "dist/cli.js"
|
|
6145
|
+
},
|
|
6146
|
+
files: [
|
|
6147
|
+
"index.ts",
|
|
6148
|
+
"src",
|
|
6149
|
+
"dist",
|
|
6150
|
+
"docs",
|
|
6151
|
+
"README.md",
|
|
6152
|
+
"LICENSE",
|
|
6153
|
+
"CHANGELOG.md",
|
|
6154
|
+
"examples"
|
|
6155
|
+
],
|
|
6156
|
+
scripts: {
|
|
6157
|
+
lint: "biome check --error-on-warnings .",
|
|
6158
|
+
clean: "bun run scripts/clean.ts",
|
|
6159
|
+
"lint:ci": "biome ci --error-on-warnings .",
|
|
6160
|
+
"lint:fix": "biome check --write --error-on-warnings .",
|
|
6161
|
+
format: "biome format --write .",
|
|
6162
|
+
typecheck: "tsc --noEmit",
|
|
6163
|
+
build: "bun build src/mcp/cli.ts --target=node --format=esm --outdir=dist --external @modelcontextprotocol/server --external @modelcontextprotocol/server/stdio --external zod --external zod/v4",
|
|
6164
|
+
prepublishOnly: "bun run check:release",
|
|
6165
|
+
test: "bun test",
|
|
6166
|
+
"test:package": "node scripts/test-package.mjs",
|
|
6167
|
+
"test:e2e:keyless": "bun run test:e2e pi pi-extension installer reinit upgrade pkg capability-off",
|
|
6168
|
+
"check:release": "bun run check && bun run test:package && bun run test:e2e:keyless",
|
|
6169
|
+
"test:e2e": "bun run test/e2e/run.ts",
|
|
6170
|
+
"docs:matrix": "bun run scripts/render-matrix.ts",
|
|
6171
|
+
"test:coverage": "bun test --coverage",
|
|
6172
|
+
check: "bun run lint && bun run typecheck && bun run docs:matrix --check && bun run build && bun test"
|
|
6173
|
+
},
|
|
6174
|
+
peerDependencies: {
|
|
6175
|
+
"@earendil-works/pi-ai": "^1.3.0",
|
|
6176
|
+
"@earendil-works/pi-coding-agent": "^1.3.0",
|
|
6177
|
+
"@opencode/plugin": "^2.0.32"
|
|
6178
|
+
},
|
|
6179
|
+
peerDependenciesMeta: {
|
|
6180
|
+
"@earendil-works/pi-ai": {
|
|
6181
|
+
optional: true
|
|
6182
|
+
},
|
|
6183
|
+
"@earendil-works/pi-coding-agent": {
|
|
6184
|
+
optional: true
|
|
6185
|
+
},
|
|
6186
|
+
"@opencode/plugin": {
|
|
6187
|
+
optional: true
|
|
6188
|
+
}
|
|
6189
|
+
},
|
|
6190
|
+
devDependencies: {
|
|
6191
|
+
"@biomejs/biome": "2.5.15",
|
|
6192
|
+
"@earendil-works/pi-ai": "1.3.0",
|
|
6193
|
+
"@earendil-works/pi-coding-agent": "1.3.0",
|
|
6194
|
+
"@modelcontextprotocol/client": "^2.3.1",
|
|
6195
|
+
"@opencode/plugin": "2.0.32",
|
|
6196
|
+
"@types/bun": "1.4.0",
|
|
6197
|
+
"@types/node": "^26.6.5",
|
|
6198
|
+
typescript: "5.9.3"
|
|
6199
|
+
},
|
|
6200
|
+
engines: {
|
|
6201
|
+
bun: ">=1.3.0",
|
|
6202
|
+
node: ">=20"
|
|
6203
|
+
},
|
|
6204
|
+
dependencies: {
|
|
6205
|
+
"@modelcontextprotocol/server": "^2.3.1",
|
|
6206
|
+
"jsonc-parser": "^3.3.1",
|
|
6207
|
+
"smol-toml": "^1.9.1",
|
|
6208
|
+
zod: "^4"
|
|
6209
|
+
}
|
|
6210
|
+
};
|
|
5607
6211
|
});
|
|
5608
6212
|
|
|
5609
6213
|
// src/core/descriptions.ts
|
|
5610
|
-
var FETCH_SERVICE_LIMITS = "DeepSeek does not render JavaScript and cannot reach private networks; PDFs, images, and other binary files are unsupported.", FETCH_OUTPUT_TRUNCATED = "Output is truncated to the configured size.", SEARCH_RANKED_FILTERED = "Results are ranked by relevance and source authority, deduplicated by URL, and capped at the configured maximum.", SEARCH_EMPTY_IS_NORMAL = "An empty result list is normal, not an error.", SEARCH_QUERY_OPERATORS = "Use site:/-site: operators in the query to include or exclude domains; the full query must stay within 128 characters.", SEARCH_TOOL_DESCRIPTION, FETCH_TOOL_DESCRIPTION, SEARCH_QUERY_PARAMETER = "Search query, 128 characters or fewer including operators (site:, -site:, quoted phrases, lang:).", FETCH_URL_PARAMETER = "Absolute http:// or https:// URL to fetch. Credentials are rejected.";
|
|
6214
|
+
var FETCH_SERVICE_LIMITS = "DeepSeek does not render JavaScript and cannot reach private networks; PDFs, images, and other binary files are unsupported.", FETCH_OUTPUT_TRUNCATED = "Output is truncated to the configured size.", SEARCH_RANKED_FILTERED = "Results are ranked by relevance and source authority, deduplicated by URL, and capped at the configured maximum.", SEARCH_EMPTY_IS_NORMAL = "An empty result list is normal, not an error.", SEARCH_QUERY_OPERATORS = "Use site:/-site: operators in the query to include or exclude domains, and after:/before: with a year or date (e.g. after:2025-06-01) to filter by publish time; the full query must stay within 128 characters.", SEARCH_TOOL_DESCRIPTION, FETCH_TOOL_DESCRIPTION, SEARCH_QUERY_PARAMETER = "Search query, 128 characters or fewer including operators (site:, -site:, quoted phrases, lang:, after:, before:).", SEARCH_ORI_QUESTION_PARAMETER, FETCH_URL_PARAMETER = "Absolute http:// or https:// URL to fetch. Credentials are rejected.";
|
|
5611
6215
|
var init_descriptions = __esm(() => {
|
|
5612
6216
|
SEARCH_TOOL_DESCRIPTION = "Search the web with DeepSeek's in-house search engine and return a short, filtered result " + "list (title, URL, summary). " + `${SEARCH_RANKED_FILTERED} ` + `${SEARCH_EMPTY_IS_NORMAL} ` + `${SEARCH_QUERY_OPERATORS} ` + "To read a full page, pass a result URL to the deepseek_fetch tool.";
|
|
5613
6217
|
FETCH_TOOL_DESCRIPTION = "Fetch one web page as clean Markdown through DeepSeek's Fetch API. " + `${FETCH_SERVICE_LIMITS} ` + `${FETCH_OUTPUT_TRUNCATED}`;
|
|
6218
|
+
SEARCH_ORI_QUESTION_PARAMETER = "The user's original question or task that triggered this search, in the user's own words rather than a " + "reformulated query. Optional; 1-128 characters, and longer values are truncated.";
|
|
5614
6219
|
});
|
|
5615
6220
|
|
|
5616
6221
|
// src/mcp/server.ts
|
|
@@ -5624,10 +6229,13 @@ function createDeepSeekServer(config, env = process.env, client = retrievalClien
|
|
|
5624
6229
|
description: SEARCH_TOOL_DESCRIPTION,
|
|
5625
6230
|
inputSchema: SEARCH_INPUT,
|
|
5626
6231
|
annotations: { readOnlyHint: true }
|
|
5627
|
-
}, async ({ query }, ctx) => {
|
|
6232
|
+
}, async ({ query, original_question }, ctx) => {
|
|
5628
6233
|
try {
|
|
5629
6234
|
const request = requestContext(config, env, ctx.mcpReq.signal);
|
|
5630
|
-
const results = await client.searchWeb(query, config.search,
|
|
6235
|
+
const results = await client.searchWeb(query, config.search, {
|
|
6236
|
+
...request,
|
|
6237
|
+
oriQuestion: original_question
|
|
6238
|
+
});
|
|
5631
6239
|
return callResult(server, ctx, formatSearchResults(results), {
|
|
5632
6240
|
results: results.map(toStructuredResult)
|
|
5633
6241
|
});
|
|
@@ -5679,14 +6287,15 @@ function toErrorResult(error) {
|
|
|
5679
6287
|
var MCP_SERVER_NAME = "deepseek-web-retrieval", SEARCH_INPUT, FETCH_INPUT;
|
|
5680
6288
|
var init_server = __esm(() => {
|
|
5681
6289
|
init_package();
|
|
5682
|
-
|
|
6290
|
+
init_client();
|
|
5683
6291
|
init_constants();
|
|
5684
6292
|
init_descriptions();
|
|
5685
6293
|
init_format2();
|
|
5686
6294
|
init_auth2();
|
|
5687
|
-
|
|
6295
|
+
init_client2();
|
|
5688
6296
|
SEARCH_INPUT = z2.object({
|
|
5689
|
-
query: z2.string().min(1).describe(SEARCH_QUERY_PARAMETER)
|
|
6297
|
+
query: z2.string().min(1).describe(SEARCH_QUERY_PARAMETER),
|
|
6298
|
+
original_question: z2.string().optional().describe(SEARCH_ORI_QUESTION_PARAMETER)
|
|
5690
6299
|
});
|
|
5691
6300
|
FETCH_INPUT = z2.object({
|
|
5692
6301
|
url: z2.string().min(1).describe(FETCH_URL_PARAMETER)
|
|
@@ -5705,7 +6314,7 @@ function serveMcp() {
|
|
|
5705
6314
|
console.error(MISSING_API_KEY_MESSAGE);
|
|
5706
6315
|
process.exit(1);
|
|
5707
6316
|
}
|
|
5708
|
-
const config = resolveEnvOptions(env);
|
|
6317
|
+
const config = resolveEnvOptions(env, sharedFileOptions({ env }));
|
|
5709
6318
|
const precheck = servePrecheck(config);
|
|
5710
6319
|
if (precheck) {
|
|
5711
6320
|
console.error(precheck);
|
|
@@ -5726,14 +6335,19 @@ function serveMcp() {
|
|
|
5726
6335
|
config.fetch.enabled ? `fetch (max ${config.fetch.maxChars} chars)` : null
|
|
5727
6336
|
].filter(Boolean).join(", ");
|
|
5728
6337
|
console.error(`deepseek-web-retrieval MCP server on stdio (${tools})`);
|
|
6338
|
+
const deviations = configDeviations(config);
|
|
6339
|
+
if (deviations)
|
|
6340
|
+
console.error(`config: ${deviations}`);
|
|
5729
6341
|
process.on("SIGINT", () => void handle.close());
|
|
5730
6342
|
process.on("SIGTERM", () => void handle.close());
|
|
5731
6343
|
}
|
|
5732
6344
|
var init_serve = __esm(() => {
|
|
5733
6345
|
init_auth();
|
|
6346
|
+
init_config_file();
|
|
6347
|
+
init_config_summary();
|
|
5734
6348
|
init_env();
|
|
5735
6349
|
init_auth2();
|
|
5736
|
-
|
|
6350
|
+
init_client2();
|
|
5737
6351
|
init_server();
|
|
5738
6352
|
});
|
|
5739
6353
|
|