deepseek-web-retrieval 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/README.md +167 -45
  3. package/dist/cli.js +2303 -1836
  4. package/docs/adr/0002-shared-config-base-layer.md +43 -0
  5. package/docs/agents/codex.md +60 -32
  6. package/docs/agents/opencode-v2.md +69 -39
  7. package/docs/agents/pi.md +60 -31
  8. package/docs/design/config-layering-design.md +180 -0
  9. package/docs/design/installer-design.md +33 -20
  10. package/docs/integration.md +41 -7
  11. package/docs/research/control-matrix.md +17 -4
  12. package/docs/research/mcp-compatibility.md +9 -2
  13. package/docs/research/ori-question-sourcing.md +336 -0
  14. package/docs/research/result-quality-passthrough.md +159 -0
  15. package/docs/research/search-api-defaults.md +18 -2
  16. package/docs/research/search-inline-content.md +244 -0
  17. package/examples/opencode.global.jsonc +3 -2
  18. package/package.json +2 -1
  19. package/src/core/auth.ts +3 -0
  20. package/src/core/client.ts +26 -4
  21. package/src/core/config-file.ts +168 -0
  22. package/src/core/config-summary.ts +82 -0
  23. package/src/core/constants.ts +15 -1
  24. package/src/core/descriptions.ts +6 -2
  25. package/src/core/env.ts +99 -32
  26. package/src/core/format.ts +19 -1
  27. package/src/core/options.ts +22 -2
  28. package/src/core/search.ts +6 -0
  29. package/src/core/types.ts +11 -0
  30. package/src/installer/apply.ts +5 -3
  31. package/src/installer/cli.ts +92 -25
  32. package/src/installer/config-command.ts +116 -0
  33. package/src/installer/hosts/codex.ts +5 -3
  34. package/src/installer/hosts/opencode.ts +9 -5
  35. package/src/installer/hosts/pi.ts +10 -6
  36. package/src/installer/ownership.ts +54 -1
  37. package/src/installer/paths.ts +7 -1
  38. package/src/installer/state.ts +73 -19
  39. package/src/installer/types.ts +12 -0
  40. package/src/mcp/auth.ts +2 -3
  41. package/src/mcp/cli.ts +9 -51
  42. package/src/mcp/serve.ts +60 -0
  43. package/src/mcp/server.ts +7 -2
  44. package/src/opencode/plugin.ts +22 -2
  45. package/src/pi/extension.ts +78 -9
  46. package/src/version.ts +12 -0
package/dist/cli.js CHANGED
@@ -16,13 +16,352 @@ var __export = (target, all) => {
16
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
 
18
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_URL_CHARS = 8192, MAX_ERROR_BODY_CHARS = 2000, MAX_RESPONSE_BYTES, FETCH_TRUNCATION_MARKER = `
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
20
 
21
- [content truncated]`, DEFAULT_API_KEY_SOURCE = "auto", DEFAULT_MAX_RETRIES = 0, 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";
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
22
  var init_constants = __esm(() => {
23
23
  MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
24
24
  });
25
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
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";
360
+ var TOOL_VERSION;
361
+ var init_version = __esm(() => {
362
+ TOOL_VERSION = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf8")).version;
363
+ });
364
+
26
365
  // src/installer/persistence.ts
27
366
  import { randomUUID } from "node:crypto";
28
367
  import {
@@ -32,7 +371,7 @@ import {
32
371
  lstatSync,
33
372
  mkdirSync,
34
373
  openSync,
35
- readFileSync,
374
+ readFileSync as readFileSync3,
36
375
  realpathSync,
37
376
  renameSync,
38
377
  rmSync,
@@ -66,7 +405,7 @@ function atomicWrite(file, text, mode) {
66
405
  try {
67
406
  fd = openSync(temporary, "wx", 384);
68
407
  writeFileSync(fd, text);
69
- if (readFileSync(temporary, "utf8") !== text)
408
+ if (readFileSync3(temporary, "utf8") !== text)
70
409
  throw new Error(`Incomplete write to ${file}`);
71
410
  fchmodSync(fd, mode ?? existingMode ?? 438 & ~process.umask());
72
411
  fsyncSync(fd);
@@ -806,10 +1145,10 @@ function parse(text, errors = [], options = ParseOptions.DEFAULT) {
806
1145
  }
807
1146
  const visitor = {
808
1147
  onObjectBegin: () => {
809
- const object2 = {};
810
- onValue(object2);
1148
+ const object = {};
1149
+ onValue(object);
811
1150
  previousParents.push(currentParent);
812
- currentParent = object2;
1151
+ currentParent = object;
813
1152
  currentProperty = null;
814
1153
  },
815
1154
  onObjectProperty: (name) => {
@@ -1448,11 +1787,6 @@ var init_jsonc = __esm(() => {
1448
1787
  init_main();
1449
1788
  });
1450
1789
 
1451
- // src/core/object.ts
1452
- function isPlainObject(value) {
1453
- return typeof value === "object" && value !== null && !Array.isArray(value);
1454
- }
1455
-
1456
1790
  // src/installer/formats/value.ts
1457
1791
  function deepEqual(a, b) {
1458
1792
  if (a === b)
@@ -1506,11 +1840,11 @@ function appendEntries(existing, entries, path) {
1506
1840
  var init_value = () => {};
1507
1841
 
1508
1842
  // src/installer/formats/json.ts
1509
- import { existsSync, readFileSync as readFileSync2 } from "node:fs";
1843
+ import { existsSync, readFileSync as readFileSync4 } from "node:fs";
1510
1844
  function loadJson(file) {
1511
1845
  if (!existsSync(file))
1512
1846
  return { exists: false, text: "", value: {}, lossy: false };
1513
- const text = readFileSync2(file, "utf8");
1847
+ const text = readFileSync4(file, "utf8");
1514
1848
  try {
1515
1849
  return { exists: true, text, value: parseJsonc(text), lossy: false };
1516
1850
  } catch (error) {
@@ -1642,7 +1976,7 @@ var init_json = __esm(() => {
1642
1976
  });
1643
1977
 
1644
1978
  // src/installer/formats/rules.ts
1645
- import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
1979
+ import { existsSync as existsSync2, readFileSync as readFileSync5 } from "node:fs";
1646
1980
  function rulesMarkers(host) {
1647
1981
  return {
1648
1982
  begin: `<!-- deepseek-web-retrieval:${host}:rules:begin -->`,
@@ -1697,7 +2031,7 @@ function rulesCodec(host) {
1697
2031
  format: "rules",
1698
2032
  load: (file) => {
1699
2033
  const exists = existsSync2(file);
1700
- const text = exists ? readFileSync3(file, "utf8") : "";
2034
+ const text = exists ? readFileSync5(file, "utf8") : "";
1701
2035
  try {
1702
2036
  blockRange(host, text);
1703
2037
  return { exists, text, value: {}, lossy: false };
@@ -1718,12 +2052,12 @@ function rulesCodec(host) {
1718
2052
  var init_rules = () => {};
1719
2053
 
1720
2054
  // node_modules/smol-toml/dist/error.js
1721
- function getLineColFromPtr(string2, ptr) {
1722
- let lines = string2.slice(0, ptr).split(/\r?\n/);
2055
+ function getLineColFromPtr(string, ptr) {
2056
+ let lines = string.slice(0, ptr).split(/\r?\n/);
1723
2057
  return [lines.length, lines.pop().length + 1];
1724
2058
  }
1725
- function makeCodeBlock(string2, line, column) {
1726
- let lines = string2.split(/\r?\n/);
2059
+ function makeCodeBlock(string, line, column) {
2060
+ let lines = string.split(/\r?\n/);
1727
2061
  let codeblock = "";
1728
2062
  let numberLen = (Math.log10(line + 1) | 0) + 1;
1729
2063
  for (let i = line - 1;i <= line + 1; i++) {
@@ -2905,11 +3239,11 @@ var init_dist = __esm(() => {
2905
3239
  });
2906
3240
 
2907
3241
  // src/installer/formats/toml.ts
2908
- import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
3242
+ import { existsSync as existsSync3, readFileSync as readFileSync6 } from "node:fs";
2909
3243
  function loadToml(file) {
2910
3244
  if (!existsSync3(file))
2911
3245
  return { exists: false, text: "", value: {}, lossy: false };
2912
- const text = readFileSync4(file, "utf8");
3246
+ const text = readFileSync6(file, "utf8");
2913
3247
  try {
2914
3248
  return { exists: true, text, value: parse3(text), lossy: hasTomlComment(text) };
2915
3249
  } catch (error) {
@@ -3082,7 +3416,7 @@ var init_formats = __esm(() => {
3082
3416
  });
3083
3417
 
3084
3418
  // src/installer/revert.ts
3085
- import { existsSync as existsSync4, readFileSync as readFileSync5, rmdirSync, unlinkSync } from "node:fs";
3419
+ import { existsSync as existsSync4, readFileSync as readFileSync7, rmdirSync, unlinkSync } from "node:fs";
3086
3420
  import { dirname as dirname2 } from "node:path";
3087
3421
  function pruneEmptyDir(dir) {
3088
3422
  try {
@@ -3096,7 +3430,7 @@ function revertRecordedFile(file, host, opts) {
3096
3430
  return lines;
3097
3431
  }
3098
3432
  const codec = codecFor(file.format, host);
3099
- const text = readFileSync5(file.file, "utf8");
3433
+ const text = readFileSync7(file.file, "utf8");
3100
3434
  if (file.format === "rules") {
3101
3435
  const edit2 = { file: file.file, format: "rules", block: file.blockApplied ?? "" };
3102
3436
  if (!codec.verify(text, edit2)) {
@@ -3145,16 +3479,29 @@ function stateDir(roots, scope) {
3145
3479
  var init_paths = () => {};
3146
3480
 
3147
3481
  // src/installer/state.ts
3148
- import { existsSync as existsSync5, lstatSync as lstatSync2, readdirSync, readFileSync as readFileSync6, rmSync as rmSync2 } from "node:fs";
3482
+ import { existsSync as existsSync5, lstatSync as lstatSync2, readdirSync, readFileSync as readFileSync8, rmSync as rmSync2 } from "node:fs";
3149
3483
  import { isAbsolute, join as join3 } from "node:path";
3150
- import { z as z2 } from "zod/v4";
3484
+ import { z } from "zod/v4";
3151
3485
  function statePathFor(input) {
3152
3486
  return join3(stateDir(input, input.scope), `install.${input.host}.json`);
3153
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
+ }
3154
3501
  function readRecordFile(file, host, scope) {
3155
3502
  let text;
3156
3503
  try {
3157
- text = readFileSync6(file, "utf8");
3504
+ text = readFileSync8(file, "utf8");
3158
3505
  } catch (error) {
3159
3506
  if (error.code === "ENOENT") {
3160
3507
  try {
@@ -3182,7 +3529,7 @@ function readRecordFile(file, host, scope) {
3182
3529
  return result.data;
3183
3530
  }
3184
3531
  function readRecord(input) {
3185
- return readRecordFile(statePathFor(input), input.host, input.scope);
3532
+ return readRecordFile(recordReadPath(input), input.host, input.scope);
3186
3533
  }
3187
3534
  function claimedByOtherScope(input, files) {
3188
3535
  const other = input.scope === "global" ? "project" : "global";
@@ -3211,61 +3558,79 @@ function appendedByPrevious(record, path) {
3211
3558
  return entries;
3212
3559
  }
3213
3560
  function deleteRecord(input) {
3214
- const file = statePathFor(input);
3561
+ const file = recordReadPath(input);
3215
3562
  if (existsSync5(file))
3216
3563
  rmSync2(file);
3217
3564
  }
3218
- function listRecords(roots) {
3565
+ function readRecordsFromDir(dir, scope) {
3219
3566
  const records = [];
3220
- for (const scope of ["global", "project"]) {
3221
- const dir = stateDir(roots, scope);
3222
- let names;
3223
- try {
3224
- names = readdirSync(dir);
3225
- } catch (error) {
3226
- if (error.code === "ENOENT")
3227
- continue;
3228
- throw new InstallRecordError(dir, "unreadable", error.code ?? "list failed");
3229
- }
3230
- for (const name of names) {
3231
- if (!name.startsWith("install.") || !name.endsWith(".json"))
3232
- continue;
3233
- const record = readRecordFile(join3(dir, name), name.slice("install.".length, -".json".length), scope);
3234
- if (record !== undefined)
3235
- records.push(record);
3236
- }
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);
3237
3581
  }
3238
3582
  return records;
3239
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
+ }
3240
3604
  var keyPath, tier, operation, recordSchema, InstallRecordError;
3241
3605
  var init_state = __esm(() => {
3242
3606
  init_paths();
3243
3607
  init_persistence();
3244
- keyPath = z2.array(z2.string().min(1)).min(1);
3245
- tier = z2.enum(["optional", "preferred", "forced", "off"]);
3246
- operation = z2.discriminatedUnion("op", [
3247
- z2.object({ op: z2.literal("set"), path: keyPath, applied: z2.unknown(), previous: z2.unknown().optional() }),
3248
- z2.object({
3249
- op: z2.literal("append-array"),
3608
+ keyPath = z.array(z.string().min(1)).min(1);
3609
+ tier = z.enum(["optional", "preferred", "forced", "off"]);
3610
+ operation = z.discriminatedUnion("op", [
3611
+ z.object({ op: z.literal("set"), path: keyPath, applied: z.unknown(), previous: z.unknown().optional() }),
3612
+ z.object({
3613
+ op: z.literal("append-array"),
3250
3614
  path: keyPath,
3251
- applied: z2.array(z2.unknown()),
3252
- previous: z2.unknown().optional()
3615
+ applied: z.array(z.unknown()),
3616
+ previous: z.unknown().optional()
3253
3617
  })
3254
3618
  ]);
3255
- recordSchema = z2.object({
3256
- version: z2.literal(1),
3257
- host: z2.enum(["opencode", "pi", "codex"]),
3258
- scope: z2.enum(["global", "project"]),
3259
- tiers: z2.object({ search: tier, fetch: tier }),
3260
- createdAt: z2.string().refine((value) => Number.isFinite(Date.parse(value))),
3261
- files: z2.array(z2.object({
3262
- file: z2.string().refine(isAbsolute),
3263
- format: z2.enum(["json", "toml", "rules"]),
3264
- created: z2.boolean(),
3265
- shared: z2.literal(true).optional(),
3266
- ops: z2.array(operation),
3267
- blockApplied: z2.string().optional(),
3268
- createdParents: z2.array(keyPath).optional()
3619
+ recordSchema = z.object({
3620
+ version: z.literal(1),
3621
+ installerVersion: z.string().optional(),
3622
+ host: z.enum(["opencode", "pi", "codex"]),
3623
+ scope: z.enum(["global", "project"]),
3624
+ tiers: z.object({ search: tier, fetch: tier }),
3625
+ createdAt: z.string().refine((value) => Number.isFinite(Date.parse(value))),
3626
+ files: z.array(z.object({
3627
+ file: z.string().refine(isAbsolute),
3628
+ format: z.enum(["json", "toml", "rules"]),
3629
+ created: z.boolean(),
3630
+ shared: z.literal(true).optional(),
3631
+ ops: z.array(operation),
3632
+ blockApplied: z.string().optional(),
3633
+ createdParents: z.array(keyPath).optional()
3269
3634
  }))
3270
3635
  });
3271
3636
  InstallRecordError = class InstallRecordError extends Error {
@@ -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 readFileSync7, realpathSync as realpathSync2, rmSync as rmSync3, writeFileSync as writeFileSync2 } from "node:fs";
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(readFileSync7(file, "utf8"));
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 readFileSync8, rmSync as rmSync5 } from "node:fs";
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) {
@@ -3456,6 +3840,7 @@ function applyLocked(plan, input) {
3456
3840
  lines.push(...verification.lines);
3457
3841
  const record = {
3458
3842
  version: 1,
3843
+ installerVersion: TOOL_VERSION,
3459
3844
  host: plan.host,
3460
3845
  scope: plan.scope,
3461
3846
  tiers: plan.tiers,
@@ -3483,7 +3868,7 @@ function applyLocked(plan, input) {
3483
3868
  } catch (error) {
3484
3869
  throw new ApplyError("recovery_required", `apply was written and verified, but old config ownership could not be released (${errorMessage(error)}). ` + "The new install record is retained; repair the reported ownership metadata before re-running init.", error);
3485
3870
  }
3486
- return { lines, ok: true };
3871
+ return { lines };
3487
3872
  }
3488
3873
  function errorMessage(error) {
3489
3874
  return error instanceof Error ? error.message : String(error);
@@ -3515,7 +3900,7 @@ function snapshotTouchedFiles(plan) {
3515
3900
  const snapshots = new Map;
3516
3901
  const touched = [...plan.edits.map((edit2) => edit2.file), ...(plan.reverts ?? []).map((revert) => revert.file)];
3517
3902
  for (const file of new Set(touched)) {
3518
- snapshots.set(file, existsSync7(file) ? { exists: true, text: readFileSync8(file, "utf8") } : { exists: false, text: "" });
3903
+ snapshots.set(file, existsSync7(file) ? { exists: true, text: readFileSync10(file, "utf8") } : { exists: false, text: "" });
3519
3904
  }
3520
3905
  return snapshots;
3521
3906
  }
@@ -3525,10 +3910,10 @@ function rollbackApply(input, previous, snapshots) {
3525
3910
  for (const [file, snapshot] of [...snapshots.entries()].reverse()) {
3526
3911
  try {
3527
3912
  if (snapshot.exists) {
3528
- if (existsSync7(file) && readFileSync8(file, "utf8") === snapshot.text)
3913
+ if (existsSync7(file) && readFileSync10(file, "utf8") === snapshot.text)
3529
3914
  continue;
3530
3915
  writeConfig(file, snapshot.text);
3531
- if (readFileSync8(file, "utf8") !== snapshot.text)
3916
+ if (readFileSync10(file, "utf8") !== snapshot.text)
3532
3917
  throw new Error("restored contents differ from the snapshot");
3533
3918
  } else {
3534
3919
  if (!existsSync7(file))
@@ -3621,7 +4006,7 @@ function verifyEdits(edits, host) {
3621
4006
  continue;
3622
4007
  }
3623
4008
  const codec = codecFor(edit2.format, host);
3624
- const text = readFileSync8(edit2.file, "utf8");
4009
+ const text = readFileSync10(edit2.file, "utf8");
3625
4010
  if (edit2.format === "rules") {
3626
4011
  const good = codec.verify(text, edit2);
3627
4012
  lines.push(`${good ? "pass" : "FAIL"} ${edit2.file}: rules block ${good ? "present" : "missing"}`);
@@ -3643,6 +4028,7 @@ function describeOp(op) {
3643
4028
  }
3644
4029
  var ApplyError;
3645
4030
  var init_apply = __esm(() => {
4031
+ init_version();
3646
4032
  init_formats();
3647
4033
  init_value();
3648
4034
  init_operation_lock();
@@ -3659,1906 +4045,1945 @@ var init_apply = __esm(() => {
3659
4045
  };
3660
4046
  });
3661
4047
 
3662
- // src/installer/detect.ts
3663
- import { existsSync as existsSync8, readFileSync as readFileSync9 } from "node:fs";
3664
- function replacingExistingEntryNote(path) {
3665
- return `An existing ${path} entry is being replaced; its previous form is recorded for uninstall.`;
3666
- }
3667
- function othersStayRegisteredNote(others) {
3668
- return `Other MCP servers stay registered (${others.join(", ")}); routing to DeepSeek is not exclusive while they offer web tools.`;
4048
+ // src/core/auth.ts
4049
+ function apiKeyFromEnv(env = process.env) {
4050
+ const key = env[API_KEY_ENV]?.trim();
4051
+ return key ? key : undefined;
3669
4052
  }
3670
- function looksWebCapable(name, entry) {
3671
- if (WEB_TOOL_HINT.test(name))
3672
- return true;
3673
- if (!isPlainObject(entry))
3674
- return false;
3675
- const command = typeof entry.command === "string" ? entry.command : "";
3676
- const url = typeof entry.url === "string" ? entry.url : "";
3677
- const args = Array.isArray(entry.args) ? entry.args.filter((arg) => typeof arg === "string").join(" ") : "";
3678
- return WEB_TOOL_HINT.test(`${command} ${args} ${url}`);
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.`;
4057
+ });
4058
+
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("");
3679
4066
  }
3680
- function looksLikeOwnPackage(source) {
3681
- return /(^|[/:@])deepseek-web-retrieval($|[@/]|-\d)/.test(source);
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
+ };
3682
4082
  }
3683
- function findCompetitorSteering(host, file) {
3684
- if (!existsSync8(file))
3685
- return [];
3686
- let text;
3687
- try {
3688
- text = readFileSync9(file, "utf8");
3689
- } catch {
3690
- return [];
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
+ `);
3691
4094
  }
3692
- return removeBlock(host, text).split(`
3693
- `).map((line) => line.trim()).filter((line) => line.length > 0 && STEERING_HINT.test(line) && !line.includes("deepseek")).slice(0, 2);
4095
+ return `# ${heading}
4096
+
4097
+ ${content}`;
3694
4098
  }
3695
- function competitorSteeringNote(file, lines) {
3696
- const quoted = lines.map((line) => `"${line}"`).join(", ");
3697
- return `Existing rules in ${file} steer toward other web tools (${quoted}); the plan surfaces this conflict — it does not resolve it.`;
4099
+ function normalizeHeading(value) {
4100
+ return value.trim().replace(/\s+/g, " ").replace(/\s+#+\s*$/, "").toLowerCase();
3698
4101
  }
3699
- function splitCompetitors(servers) {
3700
- const competitors = [];
3701
- const bystanders = [];
3702
- for (const [name, entry] of Object.entries(servers)) {
3703
- if (name === "deepseek")
3704
- continue;
3705
- (looksWebCapable(name, entry) ? competitors : bystanders).push(name);
3706
- }
3707
- return { competitors, bystanders };
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 ***");
3708
4104
  }
3709
- var BOTH_CAPABILITIES_OFF = "Both capabilities are off; nothing would be installed.", FORCED_COST_NOTE, WEB_TOOL_HINT, STEERING_HINT;
3710
- var init_detect = __esm(() => {
3711
- init_rules();
3712
- init_value();
3713
- 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.`;
3714
- WEB_TOOL_HINT = /(web[_ -]?(search|fetch)|search|fetch|browse|scrape|crawl|tavily|brave|exa|perplexity|firecrawl|jina|serp|duckduckgo|reader)/i;
3715
- STEERING_HINT = /(web[_ ]?(search|fetch)|websearch|webfetch|tavily|exa|brave|perplexity|firecrawl|jina|serp|duckduckgo|google|bing)/i;
3716
- });
3717
-
3718
- // src/installer/rulesContent.ts
3719
- function rulesLines(ids, search, fetch) {
3720
- const lines = ["DeepSeek retrieval guidance:"];
3721
- if (search === "preferred") {
3722
- 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.");
3723
- }
3724
- if (fetch === "preferred") {
3725
- lines.push(`- Use ${ids.fetch} to read a specific public web page as Markdown when its content is needed.`);
3726
- }
3727
- 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.");
3728
- return lines;
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}`;
3729
4112
  }
3730
- var MCP_TOOL_IDS, RAW_MCP_TOOL_IDS;
3731
- var init_rulesContent = __esm(() => {
3732
- MCP_TOOL_IDS = {
3733
- search: "mcp__deepseek__deepseek_web_search",
3734
- fetch: "mcp__deepseek__deepseek_fetch"
3735
- };
3736
- RAW_MCP_TOOL_IDS = { search: "deepseek_web_search", fetch: "deepseek_fetch" };
3737
- });
3738
-
3739
- // src/installer/hosts/shared.ts
3740
- function planHeader(input, paths, loaded, dialect) {
3741
- const notes = [];
3742
- const errors = [];
3743
- if (loaded.parseError) {
3744
- errors.push({ message: `${paths.config} exists but does not parse as ${dialect}: ${loaded.parseError}` });
3745
- return { notes, errors, abort: true };
3746
- }
3747
- if (input.search === "off" && input.fetch === "off") {
3748
- errors.push({ message: BOTH_CAPABILITIES_OFF });
3749
- }
3750
- const rules = codecFor("rules", input.host).load(paths.rules);
3751
- if (rules.parseError) {
3752
- errors.push({ message: `${paths.rules}: ${rules.parseError}` });
3753
- return { notes, errors, abort: true };
3754
- }
3755
- const steering = findCompetitorSteering(input.host, paths.rules);
3756
- if (steering.length > 0)
3757
- notes.push(competitorSteeringNote(paths.rules, steering));
3758
- return { notes, errors, abort: false };
4113
+ function formatTimeoutError(operation2, timeoutMs) {
4114
+ return `${operation2} timed out after ${timeoutMs} ms`;
3759
4115
  }
3760
- var init_shared = __esm(() => {
3761
- init_detect();
3762
- init_formats();
3763
- });
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(`
3764
4133
 
3765
- // src/installer/hosts/codex.ts
3766
- import { spawnSync } from "node:child_process";
3767
- function probeCodexVersion(err) {
3768
- const result = spawnSync("codex", ["--version"], { timeout: 5000, encoding: "utf8", env: process.env });
3769
- const version = result.status === 0 ? parseCodexVersion(result.stdout ?? "") : undefined;
3770
- if (version === undefined) {
3771
- err("note: codex was not found on PATH or printed no version; the version-drift check was skipped");
3772
- }
3773
- return version;
4134
+ `);
3774
4135
  }
3775
- function parseCodexVersion(output) {
3776
- return /\bcodex(?:-cli)?\s+(\d+\.\d+\.\d+)\b/.exec(output)?.[1];
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(" · ")}]` : "";
3777
4145
  }
3778
- function codexPaths(input) {
4146
+ function toStructuredResult(result) {
3779
4147
  return {
3780
- config: join2(input.home, ".codex/config.toml"),
3781
- rules: input.scope === "global" ? join2(input.home, ".codex/AGENTS.md") : join2(input.cwd, "AGENTS.md")
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 } : {}
3782
4155
  };
3783
4156
  }
3784
- function planCodex(input, _previous) {
3785
- const paths = codexPaths(input);
3786
- const loaded = codecFor("toml", input.host).load(paths.config);
3787
- const tiers = { search: input.search, fetch: input.fetch };
3788
- const header = planHeader(input, paths, loaded, "TOML");
3789
- if (header.abort) {
3790
- return { host: "codex", scope: input.scope, tiers, edits: [], notes: header.notes, errors: header.errors };
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();
4165
+ });
4166
+
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`);
3791
4174
  }
3792
- const notes = [...header.notes];
3793
- const errors = [...header.errors];
3794
- const edits = [];
3795
- if (input.hostVersion !== undefined && input.hostVersion !== CODEX_TESTED_VERSION) {
3796
- 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.");
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");
3797
4180
  }
3798
- const tools = loaded.value.tools;
3799
- if (isPlainObject(tools) && "web_search" in tools) {
3800
- errors.push({
3801
- 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."
3802
- });
4181
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
4182
+ throw new Error("URL must use http:// or https://");
3803
4183
  }
3804
- const servers = loaded.value.mcp_servers;
3805
- if (isPlainObject(servers)) {
3806
- if ("deepseek" in servers)
3807
- notes.push(replacingExistingEntryNote("mcp_servers.deepseek"));
3808
- const { competitors, bystanders } = splitCompetitors(servers);
3809
- if ((input.search === "forced" || input.fetch === "forced") && competitors.length > 0) {
3810
- errors.push({
3811
- 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."
3812
- });
3813
- }
3814
- const stayRegistered = input.search === "forced" || input.fetch === "forced" ? bystanders : [...competitors, ...bystanders];
3815
- if (stayRegistered.length > 0)
3816
- notes.push(othersStayRegisteredNote(stayRegistered));
4184
+ if (!url.hostname || !normalizeHostname(url.hostname)) {
4185
+ throw new Error("URL must include a host");
3817
4186
  }
3818
- const existingEntry = isPlainObject(servers) ? servers.deepseek : undefined;
3819
- const rawToolIds = Object.values(RAW_MCP_TOOL_IDS);
3820
- const keptDisabled = isPlainObject(existingEntry) && Array.isArray(existingEntry.disabled_tools) ? existingEntry.disabled_tools.filter((name) => typeof name === "string" && !rawToolIds.includes(name)) : [];
3821
- const offDisabled = [
3822
- ...input.search === "off" ? [RAW_MCP_TOOL_IDS.search] : [],
3823
- ...input.fetch === "off" ? [RAW_MCP_TOOL_IDS.fetch] : []
3824
- ];
3825
- const entry = {
3826
- command: "npx",
3827
- args: ["-y", input.packageRef],
3828
- ...keptDisabled.length > 0 || offDisabled.length > 0 ? { disabled_tools: [...keptDisabled, ...offDisabled] } : {},
3829
- env_vars: [API_KEY_ENV]
3830
- };
3831
- const ops = [{ op: "set", path: ["mcp_servers", "deepseek"], value: entry, previous: existingEntry }];
3832
- if (offDisabled.length > 0) {
3833
- 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.");
4187
+ if (url.username || url.password) {
4188
+ throw new Error("URL must not contain credentials");
3834
4189
  }
3835
- if (input.search === "forced") {
3836
- const previous = typeof loaded.value.web_search === "string" ? loaded.value.web_search : undefined;
3837
- ops.push({ op: "set", path: ["web_search"], value: "disabled", previous });
3838
- 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.`);
4190
+ return url;
4191
+ }
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;
3839
4207
  }
3840
- edits.push({ file: paths.config, format: "toml", shared: true, ops });
3841
- 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.");
3842
- if (input.scope === "project") {
3843
- 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.");
3844
- }
3845
- if (loaded.lossy) {
3846
- notes.push(`${paths.config} contains comments; the TOML rewrite will drop them.`);
3847
- }
3848
- if (input.search === "forced" || input.fetch === "forced") {
3849
- notes.push(`Forced search is exclusion: the built-in web_search tool is disabled ("disabled" mode). ${FORCED_COST_NOTE}`);
3850
- }
3851
- if (input.search === "optional" || input.fetch === "optional") {
3852
- 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.");
3853
- }
3854
- if (input.search === "preferred" || input.fetch === "preferred") {
3855
- edits.push({
3856
- file: paths.rules,
3857
- format: "rules",
3858
- block: buildRulesBlock(input.host, rulesLines(MCP_TOOL_IDS, input.search, input.fetch))
3859
- });
3860
- 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.`);
3861
- }
3862
- return { host: "codex", scope: input.scope, tiers, edits, notes, errors };
3863
4208
  }
3864
- var CODEX_TESTED_VERSION = "0.163.0";
3865
- var init_codex = __esm(() => {
4209
+ var init_url = __esm(() => {
3866
4210
  init_constants();
3867
- init_detect();
3868
- init_formats();
3869
- init_rules();
3870
- init_value();
3871
- init_paths();
3872
- init_rulesContent();
3873
- init_shared();
4211
+ init_domains();
3874
4212
  });
3875
4213
 
3876
- // src/installer/hosts/opencode.ts
3877
- function opencodePaths(input) {
3878
- return input.scope === "global" ? {
3879
- config: join2(configHome(input), "opencode/opencode.jsonc"),
3880
- rules: join2(configHome(input), "opencode/AGENTS.md")
3881
- } : { config: join2(input.cwd, "opencode.jsonc"), rules: join2(input.cwd, "AGENTS.md") };
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(" ");
3882
4226
  }
3883
- function planOpenCode(input, previous) {
3884
- const paths = opencodePaths(input);
3885
- const loaded = codecFor("json", input.host).load(paths.config);
3886
- const tiers = { search: input.search, fetch: input.fetch };
3887
- const header = planHeader(input, paths, loaded, "JSONC");
3888
- if (header.abort) {
3889
- return { host: "opencode", scope: input.scope, tiers, edits: [], notes: header.notes, errors: header.errors };
3890
- }
3891
- const notes = [...header.notes];
3892
- const errors = [...header.errors];
3893
- const edits = [];
3894
- if (input.search === "preferred") {
3895
- errors.push({
3896
- 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."
3897
- });
3898
- }
3899
- const plugins = Array.isArray(loaded.value.plugins) ? loaded.value.plugins : [];
3900
- const trackedPackages = new Set(appendedByPrevious(previous, ["plugins"]).filter((entry) => isPlainObject(entry) && typeof entry.package === "string").map((entry) => entry.package));
3901
- const pluginPackages = plugins.filter((entry) => isPlainObject(entry) && typeof entry.package === "string");
3902
- const excused = previous === undefined ? new Set : new Set([...trackedPackages, input.packageRef]);
3903
- const foreignOwn = pluginPackages.filter((entry) => looksLikeOwnPackage(entry.package) && !excused.has(entry.package));
3904
- if (foreignOwn.length > 0) {
3905
- errors.push({
3906
- 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."
3907
- });
3908
- }
3909
- if (previous !== undefined && pluginPackages.some((entry) => excused.has(entry.package))) {
3910
- 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.");
3911
- }
3912
- if (errors.length > 0) {
3913
- return { host: "opencode", scope: input.scope, tiers, edits, notes, errors };
3914
- }
3915
- const options = {};
3916
- if (input.search === "forced")
3917
- options.setDefaultWebsearch = true;
3918
- if (input.search === "off")
3919
- options.searchEnabled = false;
3920
- if (input.fetch === "forced")
3921
- options.fetchOverrideWebfetch = true;
3922
- if (input.fetch === "off")
3923
- options.fetchEnabled = false;
3924
- const ops = [
3925
- {
3926
- op: "append-array",
3927
- path: ["plugins"],
3928
- entries: [{ package: input.packageRef, options }],
3929
- existingCount: plugins.length
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;
4232
+ }
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;
3930
4250
  }
3931
- ];
3932
- const permissions = [];
3933
- if (input.search !== "off")
3934
- permissions.push({ action: "websearch", resource: "*", effect: "allow" });
3935
- if (input.fetch !== "off")
3936
- permissions.push({ action: "deepseek_fetch", resource: "*", effect: "allow" });
3937
- if (input.fetch === "forced")
3938
- permissions.push({ action: "webfetch", resource: "*", effect: "allow" });
3939
- const existingPermissions = Array.isArray(loaded.value.permissions) ? loaded.value.permissions.length : 0;
3940
- if (permissions.length > 0) {
3941
- ops.push({ op: "append-array", path: ["permissions"], entries: permissions, existingCount: existingPermissions });
3942
- }
3943
- if (input.search === "forced") {
3944
- const currentProvider = isPlainObject(loaded.value.websearch) ? loaded.value.websearch.provider : undefined;
3945
- ops.push({ op: "set", path: ["websearch", "provider"], value: "deepseek", previous: currentProvider });
3946
- 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.`);
3947
- }
3948
- edits.push({ file: paths.config, format: "json", ops });
3949
- notes.push("The plugin entry uses the object form so options are honored; auto-discovered plugins cannot receive options.");
3950
- if (input.search === "optional") {
3951
- 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.");
3952
- }
3953
- if (input.search === "forced" || input.fetch === "forced") {
3954
- notes.push(`Forced tier replaces built-in routing. ${FORCED_COST_NOTE}`);
3955
- }
3956
- if (input.fetch === "preferred") {
3957
- edits.push({
3958
- file: paths.rules,
3959
- format: "rules",
3960
- block: buildRulesBlock(input.host, rulesLines(OPENCODE_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 } : {}
3961
4274
  });
3962
- notes.push(`Rules block upserted in ${paths.rules}; OpenCode loads AGENTS.md only (no CLAUDE.md fallback in v2).`);
3963
4275
  }
3964
- return { host: "opencode", scope: input.scope, tiers, edits, notes, errors };
4276
+ return results;
3965
4277
  }
3966
- var OPENCODE_TOOL_IDS;
3967
- var init_opencode = __esm(() => {
3968
- init_detect();
3969
- init_formats();
3970
- init_rules();
3971
- init_value();
3972
- init_paths();
3973
- init_rulesContent();
3974
- init_state();
3975
- init_shared();
3976
- OPENCODE_TOOL_IDS = { search: "websearch (provider deepseek)", fetch: "deepseek_fetch" };
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();
3977
4288
  });
3978
4289
 
3979
- // src/pi/settings.ts
3980
- var SETTINGS_NAMESPACE = "deepseekWebRetrieval";
3981
- var init_settings = () => {};
3982
-
3983
- // src/installer/hosts/pi.ts
3984
- function piPaths(input) {
3985
- const mcp = input.scope === "global" ? join2(input.home, ".pi/agent/mcp.json") : join2(input.cwd, ".pi/mcp.json");
3986
- const settings = input.scope === "global" ? join2(input.home, ".pi/agent/settings.json") : join2(input.cwd, ".pi/settings.json");
3987
- const rules = input.scope === "global" ? join2(input.home, ".pi/agent/AGENTS.md") : join2(input.cwd, "AGENTS.md");
3988
- return { config: input.path === "mcp" ? mcp : settings, rules };
3989
- }
3990
- function piSourceRef(packageRef) {
3991
- return packageRef.startsWith("npm:") ? packageRef : `npm:${packageRef}`;
3992
- }
3993
- function planPi(input, previous) {
3994
- return input.path === "mcp" ? planPiMcp(input) : planPiExtension(input, previous);
3995
- }
3996
- function planPiExtension(input, previous) {
3997
- const paths = piPaths(input);
3998
- const loaded = codecFor("json", input.host).load(paths.config);
3999
- const tiers = { search: input.search, fetch: input.fetch };
4000
- const header = planHeader(input, paths, loaded, "JSON");
4001
- if (header.abort) {
4002
- return { host: "pi", scope: input.scope, tiers, edits: [], notes: header.notes, errors: header.errors };
4003
- }
4004
- const notes = [...header.notes];
4005
- const errors = [...header.errors];
4006
- const edits = [];
4007
- const packages = Array.isArray(loaded.value.packages) ? loaded.value.packages : [];
4008
- const source = piSourceRef(input.packageRef);
4009
- const trackedSources = appendedByPrevious(previous, ["packages"]).filter((entry) => typeof entry === "string");
4010
- const excused = previous === undefined ? new Set : new Set([...trackedSources, source]);
4011
- const sources = packages.filter((entry) => typeof entry === "string");
4012
- const foreignOwn = sources.filter((entry) => looksLikeOwnPackage(entry) && !excused.has(entry));
4013
- if (foreignOwn.length > 0) {
4014
- errors.push({
4015
- 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."
4016
- });
4017
- }
4018
- if (previous !== undefined && sources.some((entry) => excused.has(entry))) {
4019
- 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.");
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);
4304
+ }
4020
4305
  }
4021
- const forced = input.search === "forced" || input.fetch === "forced";
4022
- if (forced) {
4023
- const ownSources = new Set([...trackedSources, source]);
4024
- const unexcludable = [...collectSources(loaded.value.packages), ...collectSources(loaded.value.extensions)].filter((entry) => !ownSources.has(entry) && looksWebCapable(entry, undefined));
4025
- if (unexcludable.length > 0) {
4026
- errors.push({
4027
- 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."
4306
+ throw lastError;
4307
+ }
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"))
4028
4324
  });
4029
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);
4030
4361
  }
4031
- if (errors.length > 0) {
4032
- return { host: "pi", scope: input.scope, tiers, edits: [], notes, errors };
4033
- }
4034
- const ops = [
4035
- { op: "append-array", path: ["packages"], entries: [source], existingCount: packages.length }
4036
- ];
4037
- const namespace = isPlainObject(loaded.value[SETTINGS_NAMESPACE]) ? loaded.value[SETTINGS_NAMESPACE] : {};
4038
- if (input.search === "off") {
4039
- ops.push({
4040
- op: "set",
4041
- path: [SETTINGS_NAMESPACE, "enableSearch"],
4042
- value: false,
4043
- previous: typeof namespace.enableSearch === "boolean" ? namespace.enableSearch : undefined
4044
- });
4045
- }
4046
- if (input.fetch === "off") {
4047
- ops.push({
4048
- op: "set",
4049
- path: [SETTINGS_NAMESPACE, "enableFetch"],
4050
- value: false,
4051
- previous: typeof namespace.enableFetch === "boolean" ? namespace.enableFetch : undefined
4052
- });
4053
- }
4054
- edits.push({ file: paths.config, format: "json", ops });
4055
- 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).");
4056
- 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.");
4057
- if (input.search === "off" || input.fetch === "off") {
4058
- 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).");
4059
- }
4060
- notes.push(`Extension tools are declared to the model with exposure "direct" by Pi's own default; no exposure key is needed.`);
4061
- if (forced) {
4062
- edits.push(...planMcpCompetitorDisables(input, notes, errors));
4063
- }
4064
- if (errors.length > 0) {
4065
- return { host: "pi", scope: input.scope, tiers, edits: [], notes, errors };
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);
4066
4371
  }
4067
- if (input.search === "preferred" || input.fetch === "preferred") {
4068
- edits.push({
4069
- file: paths.rules,
4070
- format: "rules",
4071
- block: buildRulesBlock(input.host, rulesLines(RAW_MCP_TOOL_IDS, input.search, input.fetch))
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()));
4376
+ }
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;
4398
+ }
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;
4072
4426
  });
4073
- notes.push(`Rules block upserted in ${paths.rules}; Pi loads AGENTS.md context files from the working directory and its parents.`);
4074
- }
4075
- if (forced) {
4076
- 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);
4427
+ reader.releaseLock();
4077
4428
  }
4078
- return { host: "pi", scope: input.scope, tiers, edits, notes, errors };
4429
+ return truncate(text, limit);
4079
4430
  }
4080
- function planCompetitorDisables(servers, competitors, notes, errors) {
4081
- const ops = [];
4082
- const excluded = [];
4083
- const unexcludable = [];
4084
- for (const name of competitors) {
4085
- const current = servers[name];
4086
- if (!isPlainObject(current)) {
4087
- unexcludable.push(name);
4088
- continue;
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`);
4089
4436
  }
4090
- const previous = typeof current.enabled === "boolean" ? current.enabled : undefined;
4091
- ops.push({ op: "set", path: ["mcpServers", name, "enabled"], value: false, previous });
4092
- excluded.push(name);
4093
- }
4094
- if (excluded.length > 0) {
4095
- notes.push(`Forced tier: web-capable MCP servers are disabled so DeepSeek routing is exclusive (${excluded.join(", ")}; ` + "prior enabled values are snapshotted for uninstall).");
4437
+ return text2;
4096
4438
  }
4097
- if (unexcludable.length > 0) {
4098
- errors.push({
4099
- 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."
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;
4100
4458
  });
4459
+ reader.releaseLock();
4101
4460
  }
4102
- return ops;
4461
+ return text;
4103
4462
  }
4104
- function planMcpCompetitorDisables(input, notes, errors) {
4105
- const mcpFile = input.scope === "global" ? join2(input.home, ".pi/agent/mcp.json") : join2(input.cwd, ".pi/mcp.json");
4106
- const loaded = codecFor("json", input.host).load(mcpFile);
4107
- if (loaded.parseError) {
4108
- errors.push({ message: `${mcpFile} does not parse: ${loaded.parseError}` });
4109
- return [];
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;
4482
+ }
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`);
4110
4500
  }
4111
- const servers = isPlainObject(loaded.value.mcpServers) ? loaded.value.mcpServers : undefined;
4112
- if (!isPlainObject(servers))
4113
- return [];
4114
- const { competitors, bystanders } = splitCompetitors(servers);
4115
- const edits = [];
4116
- if (competitors.length > 0) {
4117
- const ops = planCompetitorDisables(servers, competitors, notes, errors);
4118
- if (ops.length > 0)
4119
- edits.push({ file: mcpFile, format: "json", ops });
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)`);
4120
4505
  }
4121
- if (bystanders.length > 0)
4122
- notes.push(othersStayRegisteredNote(bystanders));
4123
- return edits;
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");
4517
+ }
4518
+ return processResults(payload.results, options);
4124
4519
  }
4125
- function planPiMcp(input) {
4126
- const paths = piPaths(input);
4127
- const loaded = codecFor("json", input.host).load(paths.config);
4128
- const tiers = { search: input.search, fetch: input.fetch };
4129
- const header = planHeader(input, paths, loaded, "JSON");
4130
- if (header.abort) {
4131
- return { host: "pi", scope: input.scope, tiers, edits: [], notes: header.notes, errors: header.errors };
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)");
4132
4532
  }
4133
- const notes = [...header.notes];
4134
- const errors = [...header.errors];
4135
- const edits = [];
4136
- const env = { [API_KEY_ENV]: `\${${API_KEY_ENV}}` };
4137
- if (input.search === "off")
4138
- env[ENABLE_SEARCH_ENV] = "false";
4139
- if (input.fetch === "off")
4140
- env[ENABLE_FETCH_ENV] = "false";
4141
- const entry = {
4142
- command: "npx",
4143
- args: ["-y", input.packageRef],
4144
- env,
4145
- exposure: "direct",
4146
- enabled: true
4147
- };
4148
- const servers = loaded.value.mcpServers;
4149
- const existingEntry = isPlainObject(servers) ? servers.deepseek : undefined;
4150
- const ops = [{ op: "set", path: ["mcpServers", "deepseek"], value: entry, previous: existingEntry }];
4151
- edits.push({ file: paths.config, format: "json", ops });
4152
- if (isPlainObject(servers)) {
4153
- if (servers.deepseek !== undefined)
4154
- notes.push(replacingExistingEntryNote("mcpServers.deepseek"));
4155
- const { competitors, bystanders } = splitCompetitors(servers);
4156
- const forced = input.search === "forced" || input.fetch === "forced";
4157
- if (forced) {
4158
- ops.push(...planCompetitorDisables(servers, competitors, notes, errors));
4159
- }
4160
- const stayRegistered = forced ? bystanders : [...competitors, ...bystanders];
4161
- if (stayRegistered.length > 0)
4162
- notes.push(othersStayRegisteredNote(stayRegistered));
4163
- }
4164
- notes.push('"exposure": "direct" is required: Pi 1.3.0 defaults MCP tools to codemode, which hides them from the model.');
4165
- 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.`);
4166
- if (input.search === "preferred" || input.fetch === "preferred") {
4167
- edits.push({
4168
- file: paths.rules,
4169
- format: "rules",
4170
- block: buildRulesBlock(input.host, rulesLines(MCP_TOOL_IDS, input.search, input.fetch))
4171
- });
4172
- notes.push(`Rules block upserted in ${paths.rules}; Pi loads AGENTS.md context files from the working directory and its parents.`);
4173
- }
4174
- if (input.search === "forced" || input.fetch === "forced") {
4175
- 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);
4176
- }
4177
- return { host: "pi", scope: input.scope, tiers, edits, notes, errors };
4533
+ return { url: target.href, title, content };
4178
4534
  }
4179
- function collectSources(value) {
4180
- return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
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);
4540
+ return {
4541
+ url: page.url,
4542
+ title: page.title,
4543
+ content: formatted.content,
4544
+ truncated: formatted.truncated
4545
+ };
4181
4546
  }
4182
- var init_pi = __esm(() => {
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
+ };
4554
+ }
4555
+ var retrievalClient;
4556
+ var init_client = __esm(() => {
4183
4557
  init_constants();
4184
- init_settings();
4185
- init_detect();
4186
- init_formats();
4187
- init_rules();
4188
- init_value();
4189
- init_paths();
4190
- init_rulesContent();
4191
- init_state();
4192
- init_shared();
4558
+ init_domains();
4559
+ init_format2();
4560
+ init_search();
4561
+ init_transport();
4562
+ init_url();
4563
+ init_transport();
4564
+ retrievalClient = createRetrievalClient();
4193
4565
  });
4194
4566
 
4195
- // src/installer/hosts/index.ts
4196
- function planForHost(host, input) {
4197
- try {
4198
- const previous = readRecord(input);
4199
- const planned = withCrossScopeGuard(HOST_PLANNERS[host](input, previous), input);
4200
- return {
4201
- ...withSecretGuard(withOrphanReverts(withLegacySharedConfigGuard(planned, previous), previous)),
4202
- previousRecord: previous
4203
- };
4204
- } catch (error) {
4205
- if (!(error instanceof InstallRecordError))
4206
- throw error;
4207
- return {
4208
- host,
4209
- scope: input.scope,
4210
- tiers: { search: input.search, fetch: input.fetch },
4211
- edits: [],
4212
- notes: [],
4213
- errors: [{ message: error.message }]
4214
- };
4215
- }
4216
- }
4217
- function withCrossScopeGuard(plan, input) {
4218
- if (plan.errors.length > 0)
4219
- return plan;
4220
- try {
4221
- assertFileOwnership(input, plan.edits);
4222
- } catch (error) {
4223
- return { ...plan, edits: [], reverts: undefined, errors: [{ message: String(error) }] };
4224
- }
4225
- const other = input.scope === "global" ? "project" : "global";
4226
- const overlap = claimedByOtherScope(input, plan.edits.map((edit2) => edit2.file));
4227
- if (overlap.length === 0)
4228
- return { ...plan, notes: [...plan.notes, ...fileOwnershipNotes(input, plan.edits)] };
4229
- return {
4230
- ...plan,
4231
- edits: [],
4232
- reverts: undefined,
4233
- errors: [
4234
- {
4235
- 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.`
4236
- }
4237
- ]
4238
- };
4567
+ // src/core/env.ts
4568
+ function resolveEnvOptions(env = process.env, fileOptions) {
4569
+ return resolvePluginOptions({ ...fileOptions, ...pluginOptionsFromEnv(env) });
4239
4570
  }
4240
- function withLegacySharedConfigGuard(plan, previous) {
4241
- if (plan.errors.length > 0 || previous !== undefined)
4242
- return plan;
4243
- for (const edit2 of plan.edits) {
4244
- if (edit2.format === "rules" || !edit2.shared)
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))
4245
4597
  continue;
4246
- for (const op of edit2.ops) {
4247
- const old = op.previous;
4248
- 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))
4249
- continue;
4250
- return {
4251
- ...plan,
4252
- edits: [],
4253
- reverts: undefined,
4254
- errors: [
4255
- {
4256
- 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."
4257
- }
4258
- ]
4259
- };
4260
- }
4261
- }
4262
- return plan;
4263
- }
4264
- function withSecretGuard(plan) {
4265
- for (const edit2 of plan.edits) {
4266
- if (edit2.format === "rules")
4598
+ if (!(env[key] ?? "").trim())
4267
4599
  continue;
4268
- for (const op of edit2.ops) {
4269
- if (op.op !== "set" || op.previous === undefined)
4270
- continue;
4271
- const offender = findSecretLike(op.previous, op.path.join("."));
4272
- if (offender !== undefined) {
4273
- return {
4274
- ...plan,
4275
- edits: [],
4276
- reverts: undefined,
4277
- errors: [
4278
- {
4279
- 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.`
4280
- }
4281
- ]
4282
- };
4283
- }
4284
- }
4600
+ warnings.push(`unknown DEEPSEEK_* variable ${key} — ignored`);
4285
4601
  }
4286
- return plan;
4602
+ return { options, warnings };
4287
4603
  }
4288
- function findSecretLike(value, at) {
4289
- if (typeof value !== "object" || value === null)
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)
4610
+ 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`);
4619
+ }
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;
4290
4635
  return;
4291
- for (const [key, child] of Object.entries(value)) {
4292
- const path = `${at}.${key}`;
4293
- if (/(api[_-]?key|token|secret|password|credential)/i.test(key) && typeof child === "string" && child.length > 0 && !/^\$\{[^}]+\}$/.test(child)) {
4294
- return path;
4295
- }
4296
- const nested = findSecretLike(child, path);
4297
- if (nested !== undefined)
4298
- return nested;
4299
4636
  }
4300
- return;
4637
+ warnings.push(`${name}="${value.trim()}" changes nothing — only false/0/no/off turns the switch off`);
4301
4638
  }
4302
- function withOrphanReverts(plan, previous) {
4303
- if (plan.errors.length > 0)
4304
- return plan;
4305
- if (previous === undefined)
4306
- return plan;
4307
- const reverts = [];
4308
- for (const file of previous.files) {
4309
- const edit2 = plan.edits.find((candidate) => candidate.file === file.file);
4310
- if (edit2 === undefined) {
4311
- reverts.push(file);
4312
- continue;
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`);
4313
4648
  }
4314
- if (edit2.format === "rules" || file.format === "rules")
4315
- continue;
4316
- const orphanOps = file.ops.flatMap((op) => {
4317
- const claimed = edit2.ops.find((candidate) => candidate.op === op.op && candidate.path.join("\x00") === op.path.join("\x00"));
4318
- if (op.op !== "append-array" || claimed === undefined || claimed.op !== "append-array") {
4319
- return claimed === undefined ? [op] : [];
4320
- }
4321
- const dropped = (Array.isArray(op.applied) ? op.applied : []).filter((entry) => !(claimed.entries ?? []).some((wanted) => deepEqual(wanted, entry)));
4322
- return dropped.length > 0 ? [{ ...op, applied: dropped }] : [];
4323
- });
4324
- if (orphanOps.length > 0)
4325
- reverts.push({ ...file, ops: orphanOps });
4326
4649
  }
4327
- if (reverts.length === 0)
4328
- return plan;
4329
- return { ...plan, reverts };
4650
+ if (domains.length > 0)
4651
+ target[key] = domains;
4652
+ else
4653
+ warnings.push(`${name} contains no domains — ignored`);
4330
4654
  }
4331
- var HOST_PLANNERS, HOST_VERSION_PROBES;
4332
- var init_hosts = __esm(() => {
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_VARS;
4660
+ var init_env = __esm(() => {
4333
4661
  init_constants();
4334
- init_value();
4335
- init_ownership();
4336
- init_state();
4337
- init_codex();
4338
- init_opencode();
4339
- init_pi();
4340
- HOST_PLANNERS = {
4341
- opencode: planOpenCode,
4342
- pi: planPi,
4343
- codex: planCodex
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"
4344
4675
  };
4345
- HOST_VERSION_PROBES = {
4346
- codex: probeCodexVersion
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
4347
4691
  };
4692
+ OPTION_ENV_VARS = [
4693
+ ...Object.values(NUMBER_ENV),
4694
+ ...Object.values(TEXT_ENV),
4695
+ ...Object.values(DOMAINS_ENV),
4696
+ ...Object.values(FALSE_ONLY_ENV)
4697
+ ];
4348
4698
  });
4349
4699
 
4350
- // src/installer/render.ts
4351
- function renderPlan(plan) {
4352
- const lines = [];
4353
- lines.push(`Install plan: ${plan.host} (${plan.scope} scope)`);
4354
- lines.push(`Tiers: search=${plan.tiers.search}, fetch=${plan.tiers.fetch}`);
4355
- lines.push("");
4356
- lines.push("Files:");
4357
- for (const edit2 of plan.edits) {
4358
- lines.push(` ${edit2.file}`);
4359
- if (edit2.format === "rules") {
4360
- lines.push(" upsert marked rules block");
4361
- continue;
4362
- }
4363
- for (const op of edit2.ops) {
4364
- if (op.op === "set") {
4365
- const was = op.previous === undefined ? "(absent)" : preview(op.previous);
4366
- lines.push(` set ${op.path.join(".")} = ${preview(op.value)} (was ${was})`);
4367
- } else {
4368
- const count = (op.entries ?? []).length;
4369
- const kept = op.existingCount !== undefined ? ` (${op.existingCount} existing kept)` : "";
4370
- lines.push(` append ${count} entr${count === 1 ? "y" : "ies"} to ${op.path.join(".")}${kept}`);
4371
- for (const entry of op.entries ?? [])
4372
- lines.push(` + ${preview(entry)}`);
4373
- }
4374
- }
4375
- }
4376
- if (plan.reverts && plan.reverts.length > 0) {
4377
- lines.push("");
4378
- lines.push("Reverts (artifacts from the previous init that this plan no longer wants):");
4379
- for (const revert of plan.reverts) {
4380
- lines.push(` ${revert.file}`);
4381
- if (revert.format === "rules") {
4382
- lines.push(" remove rules block");
4383
- continue;
4384
- }
4385
- for (const op of revert.ops) {
4386
- if (op.op === "set") {
4387
- lines.push(op.previous === undefined ? ` remove ${op.path.join(".")} (was not set before this install)` : ` restore ${op.path.join(".")} = ${preview(op.previous)}`);
4388
- } else {
4389
- const count = Array.isArray(op.applied) ? op.applied.length : 0;
4390
- lines.push(` remove ${count} appended entr${count === 1 ? "y" : "ies"} from ${op.path.join(".")}`);
4391
- }
4392
- }
4393
- }
4700
+ // src/installer/config-command.ts
4701
+ async function runConfigCommand(query, io, deps = {}) {
4702
+ if (query !== undefined && !query.trim()) {
4703
+ io.err('error: --search requires a query (for example: config --search "deepseek mcp")');
4704
+ return EXIT_USAGE;
4705
+ }
4706
+ const env = deps.env ?? process.env;
4707
+ const file = configFilePath({ env });
4708
+ const report = readFileOptionsReport(file);
4709
+ for (const warning of report.warnings)
4710
+ io.err(`warning: ${warning}`);
4711
+ const envReport = envOptionsReport(env);
4712
+ for (const warning of envReport.warnings)
4713
+ io.err(`warning: ${warning}`);
4714
+ const resolved = resolveEnvOptions(env, report.options);
4715
+ if (query === undefined) {
4716
+ printConfigReport(file, report.exists, report.options, envReport.options, resolved, io);
4717
+ return report.warnings.length > 0 || envReport.warnings.length > 0 ? EXIT_CONFIG_PROBLEMS : 0;
4394
4718
  }
4395
- if (plan.notes.length > 0) {
4396
- lines.push("");
4397
- lines.push("Notes:");
4398
- for (const note of plan.notes)
4399
- lines.push(` - ${note}`);
4719
+ const apiKey = apiKeyFromEnv(env);
4720
+ if (!apiKey) {
4721
+ io.err(MISSING_API_KEY_MESSAGE);
4722
+ return EXIT_CONFIG_PROBLEMS;
4400
4723
  }
4401
- return lines.join(`
4402
- `);
4724
+ const client = deps.client ?? createRetrievalClient();
4725
+ const startedAt = Date.now();
4726
+ try {
4727
+ const results = await client.searchWeb(query, resolved.search, {
4728
+ signal: new AbortController().signal,
4729
+ auth: { apiKey },
4730
+ retry: resolved.retry
4731
+ });
4732
+ const elapsedMs = Date.now() - startedAt;
4733
+ io.out(`search "${query.trim()}" — ${results.length} ${results.length === 1 ? "result" : "results"} in ${elapsedMs} ms`);
4734
+ io.out("");
4735
+ for (const line of formatSearchResults(results).split(`
4736
+ `))
4737
+ io.out(line);
4738
+ return 0;
4739
+ } catch (error) {
4740
+ io.err(error instanceof Error ? error.message : String(error));
4741
+ return EXIT_CONFIG_PROBLEMS;
4742
+ }
4743
+ }
4744
+ function printConfigReport(file, exists, fileOptions, envOptions, resolved, io) {
4745
+ io.out(`Config file: ${file} (${exists ? "exists" : "missing"})`);
4746
+ io.out("");
4747
+ const effective = configSummaryValues(resolved);
4748
+ const fileKeys = new Set(Object.keys(fileOptions));
4749
+ const envKeys = new Set(Object.keys(envOptions));
4750
+ for (const name of Object.keys(effective)) {
4751
+ let source = envKeys.has(name) ? "env" : fileKeys.has(name) ? "file" : "default";
4752
+ if (name === "maxResults" && source === "default" && (envKeys.has("count") || fileKeys.has("count"))) {
4753
+ source = "count";
4754
+ }
4755
+ io.out(`${name} = ${formatConfigValue(effective[name], "join")} (${source})`);
4756
+ }
4757
+ io.out("");
4758
+ io.out(HOST_OVERRIDE_NOTE);
4759
+ }
4760
+ 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.";
4761
+ var init_config_command = __esm(() => {
4762
+ init_auth();
4763
+ init_client();
4764
+ init_config_file();
4765
+ init_config_summary();
4766
+ init_env();
4767
+ init_format2();
4768
+ });
4769
+
4770
+ // src/installer/detect.ts
4771
+ import { existsSync as existsSync8, readFileSync as readFileSync11 } from "node:fs";
4772
+ function replacingExistingEntryNote(path) {
4773
+ return `An existing ${path} entry is being replaced; its previous form is recorded for uninstall.`;
4403
4774
  }
4404
- function preview(value) {
4405
- const text = JSON.stringify(value) ?? String(value);
4406
- return text.length > 100 ? `${text.slice(0, 97)}...` : text;
4775
+ function othersStayRegisteredNote(others) {
4776
+ return `Other MCP servers stay registered (${others.join(", ")}); routing to DeepSeek is not exclusive while they offer web tools.`;
4407
4777
  }
4408
-
4409
- // src/installer/uninstall.ts
4410
- function uninstall(input) {
4411
- return withOperationLock([statePathFor(input)], () => {
4412
- const record = readRecord(input);
4413
- if (record === undefined) {
4414
- throw new Error(`No ${input.host} install is recorded for the ${input.scope} scope.`);
4415
- }
4416
- return withOperationLock(record.files.map((file) => file.file), () => uninstallLocked(input, record));
4417
- });
4778
+ function looksWebCapable(name, entry) {
4779
+ if (WEB_TOOL_HINT.test(name))
4780
+ return true;
4781
+ if (!isPlainObject(entry))
4782
+ return false;
4783
+ const command = typeof entry.command === "string" ? entry.command : "";
4784
+ const url = typeof entry.url === "string" ? entry.url : "";
4785
+ const args = Array.isArray(entry.args) ? entry.args.filter((arg) => typeof arg === "string").join(" ") : "";
4786
+ return WEB_TOOL_HINT.test(`${command} ${args} ${url}`);
4418
4787
  }
4419
- function uninstallLocked(input, record) {
4420
- assertFileOwnership(input, record.files);
4421
- const other = input.scope === "global" ? "project" : "global";
4422
- const overlap = claimedByOtherScope(input, record.files.map((file) => file.file));
4423
- if (overlap.length > 0) {
4424
- 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.`);
4788
+ function looksLikeOwnPackage(source) {
4789
+ return /(^|[/:@])deepseek-web-retrieval($|[@/]|-\d)/.test(source);
4790
+ }
4791
+ function findCompetitorSteering(host, file) {
4792
+ if (!existsSync8(file))
4793
+ return [];
4794
+ let text;
4795
+ try {
4796
+ text = readFileSync11(file, "utf8");
4797
+ } catch {
4798
+ return [];
4425
4799
  }
4426
- const lines = [];
4427
- for (const file of record.files) {
4428
- if (file.format !== "rules")
4800
+ return removeBlock(host, text).split(`
4801
+ `).map((line) => line.trim()).filter((line) => line.length > 0 && STEERING_HINT.test(line) && !line.includes("deepseek")).slice(0, 2);
4802
+ }
4803
+ function competitorSteeringNote(file, lines) {
4804
+ const quoted = lines.map((line) => `"${line}"`).join(", ");
4805
+ return `Existing rules in ${file} steer toward other web tools (${quoted}); the plan surfaces this conflict — it does not resolve it.`;
4806
+ }
4807
+ function splitCompetitors(servers) {
4808
+ const competitors = [];
4809
+ const bystanders = [];
4810
+ for (const [name, entry] of Object.entries(servers)) {
4811
+ if (name === "deepseek")
4429
4812
  continue;
4430
- const loaded = codecFor("rules", record.host).load(file.file);
4431
- if (loaded.parseError)
4432
- throw new Error(`${file.file}: ${loaded.parseError}`);
4813
+ (looksWebCapable(name, entry) ? competitors : bystanders).push(name);
4433
4814
  }
4434
- for (const file of [...record.files].reverse()) {
4435
- lines.push(...revertRecordedFile(file, record.host, { label: "", whole: true }));
4815
+ return { competitors, bystanders };
4816
+ }
4817
+ var BOTH_CAPABILITIES_OFF = "Both capabilities are off; nothing would be installed.", FORCED_COST_NOTE, WEB_TOOL_HINT, STEERING_HINT;
4818
+ var init_detect = __esm(() => {
4819
+ init_rules();
4820
+ init_value();
4821
+ 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.`;
4822
+ WEB_TOOL_HINT = /(web[_ -]?(search|fetch)|search|fetch|browse|scrape|crawl|tavily|brave|exa|perplexity|firecrawl|jina|serp|duckduckgo|reader)/i;
4823
+ STEERING_HINT = /(web[_ ]?(search|fetch)|websearch|webfetch|tavily|exa|brave|perplexity|firecrawl|jina|serp|duckduckgo|google|bing)/i;
4824
+ });
4825
+
4826
+ // src/installer/rulesContent.ts
4827
+ function rulesLines(ids, search, fetch) {
4828
+ const lines = ["DeepSeek retrieval guidance:"];
4829
+ if (search === "preferred") {
4830
+ 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.");
4436
4831
  }
4437
- deleteRecord(input);
4438
- releaseFileOwnership(input, record.files);
4439
- pruneEmptyDir(stateDir(input, input.scope));
4440
- lines.push(`removed install record (${input.host}, ${input.scope})`);
4832
+ if (fetch === "preferred") {
4833
+ lines.push(`- Use ${ids.fetch} to read a specific public web page as Markdown when its content is needed.`);
4834
+ }
4835
+ 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.");
4441
4836
  return lines;
4442
4837
  }
4443
- var init_uninstall = __esm(() => {
4444
- init_formats();
4445
- init_operation_lock();
4446
- init_ownership();
4447
- init_paths();
4448
- init_revert();
4449
- init_state();
4838
+ var MCP_TOOL_IDS, RAW_MCP_TOOL_IDS;
4839
+ var init_rulesContent = __esm(() => {
4840
+ MCP_TOOL_IDS = {
4841
+ search: "mcp__deepseek__deepseek_web_search",
4842
+ fetch: "mcp__deepseek__deepseek_fetch"
4843
+ };
4844
+ RAW_MCP_TOOL_IDS = { search: "deepseek_web_search", fetch: "deepseek_fetch" };
4450
4845
  });
4451
4846
 
4452
- // src/installer/cli.ts
4453
- var exports_cli = {};
4454
- __export(exports_cli, {
4455
- runInstallerCli: () => runInstallerCli
4847
+ // src/installer/hosts/shared.ts
4848
+ function planHeader(input, paths, loaded, dialect) {
4849
+ const notes = [];
4850
+ const errors = [];
4851
+ if (loaded.parseError) {
4852
+ errors.push({ message: `${paths.config} exists but does not parse as ${dialect}: ${loaded.parseError}` });
4853
+ return { notes, errors, abort: true };
4854
+ }
4855
+ if (input.search === "off" && input.fetch === "off") {
4856
+ errors.push({ message: BOTH_CAPABILITIES_OFF });
4857
+ }
4858
+ const rules = codecFor("rules", input.host).load(paths.rules);
4859
+ if (rules.parseError) {
4860
+ errors.push({ message: `${paths.rules}: ${rules.parseError}` });
4861
+ return { notes, errors, abort: true };
4862
+ }
4863
+ const steering = findCompetitorSteering(input.host, paths.rules);
4864
+ if (steering.length > 0)
4865
+ notes.push(competitorSteeringNote(paths.rules, steering));
4866
+ return { notes, errors, abort: false };
4867
+ }
4868
+ var init_shared = __esm(() => {
4869
+ init_detect();
4870
+ init_formats();
4456
4871
  });
4457
- import { homedir } from "node:os";
4458
- import { resolve as resolve2 } from "node:path";
4459
- function parseFlags(args) {
4460
- const values = new Map;
4461
- const booleans = new Set;
4462
- const problems = [];
4463
- for (let i = 0;i < args.length; i += 1) {
4464
- const arg = args[i];
4465
- if (!arg.startsWith("--")) {
4466
- problems.push(`unexpected argument "${arg}" (flags only)`);
4467
- continue;
4468
- }
4469
- const eq = arg.indexOf("=");
4470
- const name = eq === -1 ? arg.slice(2) : arg.slice(2, eq);
4471
- if (BOOLEAN_FLAGS.has(name)) {
4472
- if (eq !== -1) {
4473
- problems.push(`--${name} does not accept a value; use --${name} to enable it or omit it`);
4474
- continue;
4475
- }
4476
- booleans.add(name);
4477
- continue;
4478
- }
4479
- if (!VALUE_FLAGS.has(name)) {
4480
- problems.push(`unknown flag --${name}`);
4481
- continue;
4482
- }
4483
- if (eq !== -1) {
4484
- values.set(name, arg.slice(eq + 1));
4485
- continue;
4486
- }
4487
- const next = args[i + 1];
4488
- if (next === undefined || next.startsWith("--")) {
4489
- problems.push(`--${name} requires a value`);
4490
- continue;
4491
- }
4492
- values.set(name, next);
4493
- i += 1;
4872
+
4873
+ // src/installer/hosts/codex.ts
4874
+ import { spawnSync } from "node:child_process";
4875
+ function probeCodexVersion(err) {
4876
+ const result = spawnSync("codex", ["--version"], { timeout: 5000, encoding: "utf8", env: process.env });
4877
+ const version = result.status === 0 ? parseCodexVersion(result.stdout ?? "") : undefined;
4878
+ if (version === undefined) {
4879
+ err("note: codex was not found on PATH or printed no version; the version-drift check was skipped");
4494
4880
  }
4495
- return { values, booleans, problems };
4881
+ return version;
4496
4882
  }
4497
- async function runInstallerCli(argv, io = {}) {
4498
- const out = io.out ?? ((line) => console.log(line));
4499
- const err = io.err ?? ((line) => console.error(line));
4500
- const [command, ...rest] = argv;
4501
- if (command === undefined || command === "help" || command === "--help") {
4502
- out(USAGE);
4503
- return command === undefined ? 1 : 0;
4883
+ function parseCodexVersion(output) {
4884
+ return /\bcodex(?:-cli)?\s+(\d+\.\d+\.\d+)\b/.exec(output)?.[1];
4885
+ }
4886
+ function codexPaths(input) {
4887
+ return {
4888
+ config: join2(input.home, ".codex/config.toml"),
4889
+ rules: input.scope === "global" ? join2(input.home, ".codex/AGENTS.md") : join2(input.cwd, "AGENTS.md")
4890
+ };
4891
+ }
4892
+ function planCodex(input, _previous) {
4893
+ const paths = codexPaths(input);
4894
+ const loaded = codecFor("toml", input.host).load(paths.config);
4895
+ const tiers = { search: input.search, fetch: input.fetch };
4896
+ const header = planHeader(input, paths, loaded, "TOML");
4897
+ if (header.abort) {
4898
+ return { host: "codex", scope: input.scope, tiers, edits: [], notes: header.notes, errors: header.errors };
4504
4899
  }
4505
- const flags = parseFlags(rest);
4506
- if (flags.problems.length > 0) {
4507
- for (const problem of flags.problems)
4508
- err(`error: ${problem}`);
4509
- err(`
4510
- ${USAGE}`);
4511
- return 1;
4900
+ const notes = [...header.notes];
4901
+ const errors = [...header.errors];
4902
+ const edits = [];
4903
+ if (input.hostVersion !== undefined && input.hostVersion !== CODEX_TESTED_VERSION) {
4904
+ 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.");
4512
4905
  }
4513
- const cwd = resolve2(flags.values.get("cwd") ?? process.cwd());
4514
- const home = process.env.HOME ?? homedir();
4515
- const configHome2 = resolveConfigHome(home);
4516
- if (command === "list") {
4517
- let records;
4518
- try {
4519
- records = listRecords({ home, cwd, configHome: configHome2 });
4520
- } catch (error) {
4521
- err(error instanceof Error ? error.message : String(error));
4522
- return 1;
4523
- }
4524
- if (records.length === 0) {
4525
- out("No installs recorded.");
4526
- return 0;
4527
- }
4528
- for (const record of records) {
4529
- out(`${record.host} (${record.scope}) — search=${record.tiers.search}, fetch=${record.tiers.fetch}, installed ${record.createdAt}`);
4530
- for (const file of record.files)
4531
- out(` ${file.created ? "created" : "modified"} ${file.file}`);
4532
- }
4533
- return 0;
4906
+ const tools = loaded.value.tools;
4907
+ if (isPlainObject(tools) && "web_search" in tools) {
4908
+ errors.push({
4909
+ 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."
4910
+ });
4534
4911
  }
4535
- if (command === "uninstall") {
4536
- const required2 = requireInput(flags, cwd, home, configHome2, false);
4537
- if (!required2.ok)
4538
- return usageError(required2.problems, err);
4539
- try {
4540
- for (const line of uninstall(required2.input))
4541
- out(line);
4542
- return 0;
4543
- } catch (error) {
4544
- err(error instanceof Error ? error.message : String(error));
4545
- return 1;
4912
+ const servers = loaded.value.mcp_servers;
4913
+ if (isPlainObject(servers)) {
4914
+ if ("deepseek" in servers)
4915
+ notes.push(replacingExistingEntryNote("mcp_servers.deepseek"));
4916
+ const { competitors, bystanders } = splitCompetitors(servers);
4917
+ if ((input.search === "forced" || input.fetch === "forced") && competitors.length > 0) {
4918
+ errors.push({
4919
+ 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."
4920
+ });
4546
4921
  }
4922
+ const stayRegistered = input.search === "forced" || input.fetch === "forced" ? bystanders : [...competitors, ...bystanders];
4923
+ if (stayRegistered.length > 0)
4924
+ notes.push(othersStayRegisteredNote(stayRegistered));
4547
4925
  }
4548
- if (command !== "init") {
4549
- err(`Unknown command "${command}".
4550
-
4551
- ${USAGE}`);
4552
- return 1;
4926
+ const existingEntry = isPlainObject(servers) ? servers.deepseek : undefined;
4927
+ const rawToolIds = Object.values(RAW_MCP_TOOL_IDS);
4928
+ const keptDisabled = isPlainObject(existingEntry) && Array.isArray(existingEntry.disabled_tools) ? existingEntry.disabled_tools.filter((name) => typeof name === "string" && !rawToolIds.includes(name)) : [];
4929
+ const offDisabled = [
4930
+ ...input.search === "off" ? [RAW_MCP_TOOL_IDS.search] : [],
4931
+ ...input.fetch === "off" ? [RAW_MCP_TOOL_IDS.fetch] : []
4932
+ ];
4933
+ const entry = {
4934
+ command: "npx",
4935
+ args: ["-y", input.packageRef],
4936
+ ...keptDisabled.length > 0 || offDisabled.length > 0 ? { disabled_tools: [...keptDisabled, ...offDisabled] } : {},
4937
+ env_vars: [API_KEY_ENV, ...OPTION_ENV_VARS]
4938
+ };
4939
+ const ops = [{ op: "set", path: ["mcp_servers", "deepseek"], value: entry, previous: existingEntry }];
4940
+ if (offDisabled.length > 0) {
4941
+ 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.");
4553
4942
  }
4554
- const required = requireInput(flags, cwd, home, configHome2, true);
4555
- if (!required.ok)
4556
- return usageError(required.problems, err);
4557
- const { input } = required;
4558
- const probe = HOST_VERSION_PROBES[input.host];
4559
- const hostVersion = probe?.(err);
4560
- const plan = planForHost(input.host, hostVersion === undefined ? input : { ...input, hostVersion });
4561
- if (plan.errors.length > 0) {
4562
- for (const error of plan.errors)
4563
- err(`error: ${error.message}`);
4564
- err("Nothing was written.");
4565
- return 1;
4943
+ if (input.search === "forced") {
4944
+ const previous = typeof loaded.value.web_search === "string" ? loaded.value.web_search : undefined;
4945
+ ops.push({ op: "set", path: ["web_search"], value: "disabled", previous });
4946
+ 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.`);
4566
4947
  }
4567
- out(renderPlan(plan));
4568
- if (!flags.booleans.has("apply")) {
4569
- out(`
4570
- Plan only — re-run with --apply to write these changes.`);
4571
- return 0;
4948
+ edits.push({ file: paths.config, format: "toml", shared: true, ops });
4949
+ 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).");
4950
+ if (input.scope === "project") {
4951
+ 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.");
4572
4952
  }
4573
- try {
4574
- const result = applyPlan(plan, input);
4575
- for (const line of result.lines)
4576
- out(line);
4577
- return result.ok ? 0 : 1;
4578
- } catch (error) {
4579
- err(error instanceof Error ? error.message : String(error));
4580
- return 1;
4953
+ if (loaded.lossy) {
4954
+ notes.push(`${paths.config} contains comments; the TOML rewrite will drop them.`);
4581
4955
  }
4582
- }
4583
- function usageError(problems, err) {
4584
- for (const problem of problems)
4585
- err(`error: ${problem}`);
4586
- err(`
4587
- ${USAGE}`);
4588
- return 1;
4589
- }
4590
- function resolveConfigHome(home) {
4591
- const xdg = process.env.XDG_CONFIG_HOME;
4592
- if (xdg && home === (process.env.HOME ?? ""))
4593
- return xdg;
4594
- return;
4595
- }
4596
- function requireInput(flags, cwd, home, configHome2, needsTiers) {
4597
- const problems = [];
4598
- const host = flags.values.get("host");
4599
- const scope = flags.values.get("scope");
4600
- const search = needsTiers ? flags.values.get("search") ?? "" : "optional";
4601
- const fetch = needsTiers ? flags.values.get("fetch") ?? "" : "optional";
4602
- const path = flags.values.get("path");
4603
- if (host === undefined)
4604
- problems.push(`--host is required (one of: ${HOSTS.join(", ")})`);
4605
- else if (!HOSTS.includes(host))
4606
- problems.push(`invalid --host value "${host}" (one of: ${HOSTS.join(", ")})`);
4607
- if (scope === undefined)
4608
- problems.push(`--scope is required (one of: ${SCOPES.join(", ")})`);
4609
- else if (!SCOPES.includes(scope))
4610
- problems.push(`invalid --scope value "${scope}" (one of: ${SCOPES.join(", ")})`);
4611
- if (path !== undefined) {
4612
- if (host !== "pi") {
4613
- problems.push(`--path is only supported for host pi (${host ?? "no host"} has a single integration path)`);
4614
- } else if (!PATH_CHOICES.includes(path)) {
4615
- problems.push(`invalid --path value "${path}" (one of: ${PATH_CHOICES.join(", ")})`);
4616
- }
4956
+ if (input.search === "forced" || input.fetch === "forced") {
4957
+ notes.push(`Forced search is exclusion: the built-in web_search tool is disabled ("disabled" mode). ${FORCED_COST_NOTE}`);
4617
4958
  }
4618
- if (needsTiers) {
4619
- if (!TIERS.includes(search)) {
4620
- problems.push(flags.values.has("search") ? `invalid --search value "${search}" (one of: ${TIERS.join(", ")})` : `--search is required (one of: ${TIERS.join(", ")})`);
4621
- }
4622
- if (!TIERS.includes(fetch)) {
4623
- problems.push(flags.values.has("fetch") ? `invalid --fetch value "${fetch}" (one of: ${TIERS.join(", ")})` : `--fetch is required (one of: ${TIERS.join(", ")})`);
4624
- }
4959
+ if (input.search === "optional" || input.fetch === "optional") {
4960
+ 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.");
4625
4961
  }
4626
- if (home === "")
4627
- problems.push("HOME is not set and the user home cannot be resolved; cannot proceed");
4628
- if (problems.length > 0)
4629
- return { ok: false, problems };
4630
- return {
4631
- ok: true,
4632
- input: {
4633
- host,
4634
- scope,
4635
- search,
4636
- fetch,
4637
- packageRef: flags.values.get("package") ?? "deepseek-web-retrieval",
4638
- home,
4639
- cwd,
4640
- configHome: configHome2,
4641
- ...path !== undefined ? { path } : {}
4642
- }
4643
- };
4962
+ if (input.search === "preferred" || input.fetch === "preferred") {
4963
+ edits.push({
4964
+ file: paths.rules,
4965
+ format: "rules",
4966
+ block: buildRulesBlock(input.host, rulesLines(MCP_TOOL_IDS, input.search, input.fetch))
4967
+ });
4968
+ 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.`);
4969
+ }
4970
+ return { host: "codex", scope: input.scope, tiers, edits, notes, errors };
4644
4971
  }
4645
- var HOSTS, HOST_CHOICES, SCOPES, TIERS, USAGE, VALUE_FLAGS, BOOLEAN_FLAGS, PATH_CHOICES;
4646
- var init_cli = __esm(() => {
4647
- init_apply();
4648
- init_hosts();
4649
- init_state();
4650
- init_uninstall();
4651
- HOSTS = Object.keys(HOST_PLANNERS);
4652
- HOST_CHOICES = `<${HOSTS.join("|")}>`;
4653
- SCOPES = ["project", "global"];
4654
- TIERS = ["optional", "preferred", "forced", "off"];
4655
- USAGE = `Usage:
4656
- 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]
4657
- deepseek-web-retrieval uninstall --host ${HOST_CHOICES} --scope <project|global> [--cwd <dir>]
4658
- deepseek-web-retrieval list [--cwd <dir>]
4659
-
4660
- init prints the full plan (every file, old value → new value) and writes nothing
4661
- until --apply is passed. uninstall restores what the plan applied. --path picks
4662
- the integration mechanism for hosts with more than one (Pi: extension by
4663
- default, mcp for the stdio MCP server). --package overrides the package ref
4664
- written into host config; the Pi extension path turns it into the packages
4665
- source (a bare name is prefixed with npm:, a ref that already starts with npm:
4666
- is written as-is).`;
4667
- VALUE_FLAGS = new Set(["host", "scope", "search", "fetch", "path", "package", "cwd"]);
4668
- BOOLEAN_FLAGS = new Set(["apply"]);
4669
- PATH_CHOICES = ["extension", "mcp"];
4972
+ var CODEX_TESTED_VERSION = "0.163.0";
4973
+ var init_codex = __esm(() => {
4974
+ init_constants();
4975
+ init_env();
4976
+ init_detect();
4977
+ init_formats();
4978
+ init_rules();
4979
+ init_value();
4980
+ init_paths();
4981
+ init_rulesContent();
4982
+ init_shared();
4670
4983
  });
4671
4984
 
4672
- // src/mcp/cli.ts
4673
- import { serveStdio } from "@modelcontextprotocol/server/stdio";
4674
-
4675
- // src/core/auth.ts
4676
- init_constants();
4677
- function apiKeyFromEnv(env = process.env) {
4678
- const key = env[API_KEY_ENV]?.trim();
4679
- return key ? key : undefined;
4680
- }
4681
-
4682
- // src/core/env.ts
4683
- init_constants();
4684
-
4685
- // src/core/options.ts
4686
- init_constants();
4687
-
4688
- // src/core/domains.ts
4689
- import { isIP } from "node:net";
4690
- function normalizeDomain(value) {
4691
- const trimmed = value.trim();
4692
- if (!trimmed || /\s/.test(trimmed))
4693
- return;
4694
- const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed);
4695
- if (hasScheme && !/^https?:\/\//i.test(trimmed))
4696
- return;
4697
- let hostname;
4698
- try {
4699
- hostname = new URL(hasScheme ? trimmed : `http://${trimmed}`).hostname;
4700
- } catch {
4701
- return;
4702
- }
4703
- return normalizeHostname(hostname);
4704
- }
4705
- function normalizeHostname(value) {
4706
- return value.trim().toLowerCase().replace(/^\[/, "").replace(/\]$/, "").replace(/\.+$/, "");
4707
- }
4708
- function domainMatches(hostname, domain) {
4709
- const host = normalizeHostname(hostname);
4710
- const target = normalizeHostname(domain);
4711
- if (!host || !target)
4712
- return false;
4713
- return host === target || host.endsWith(`.${target}`);
4985
+ // src/installer/hosts/opencode.ts
4986
+ function opencodePaths(input) {
4987
+ return input.scope === "global" ? {
4988
+ config: join2(configHome(input), "opencode/opencode.jsonc"),
4989
+ rules: join2(configHome(input), "opencode/AGENTS.md")
4990
+ } : { config: join2(input.cwd, "opencode.jsonc"), rules: join2(input.cwd, "AGENTS.md") };
4714
4991
  }
4715
- function normalizeDomains(values) {
4716
- if (!Array.isArray(values))
4717
- return [];
4718
- const domains = new Set;
4719
- for (const value of values) {
4720
- if (typeof value !== "string")
4721
- continue;
4722
- const domain = normalizeDomain(value);
4723
- if (domain)
4724
- domains.add(domain);
4992
+ function planOpenCode(input, previous) {
4993
+ const paths = opencodePaths(input);
4994
+ const loaded = codecFor("json", input.host).load(paths.config);
4995
+ const tiers = { search: input.search, fetch: input.fetch };
4996
+ const header = planHeader(input, paths, loaded, "JSONC");
4997
+ if (header.abort) {
4998
+ return { host: "opencode", scope: input.scope, tiers, edits: [], notes: header.notes, errors: header.errors };
4725
4999
  }
4726
- return [...domains];
4727
- }
4728
- function isPrivateHostname(value) {
4729
- const host = normalizeHostname(value);
4730
- if (!host)
4731
- return true;
4732
- if (host === "localhost" || host === "local")
4733
- return true;
4734
- if (host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal") || host.endsWith(".lan") || host.endsWith(".home.arpa")) {
4735
- return true;
5000
+ const notes = [...header.notes];
5001
+ const errors = [...header.errors];
5002
+ const edits = [];
5003
+ if (input.search === "preferred") {
5004
+ errors.push({
5005
+ 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."
5006
+ });
4736
5007
  }
4737
- const version = isIP(host);
4738
- if (version === 4)
4739
- return isPrivateIpv4(host);
4740
- if (version === 6)
4741
- return isPrivateIpv6(host);
4742
- return false;
4743
- }
4744
- function assertFetchHostPolicy(url, options) {
4745
- const host = normalizeHostname(url.hostname);
4746
- if (options.blockPrivateHosts && isPrivateHostname(host)) {
4747
- 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.");
5008
+ const plugins = Array.isArray(loaded.value.plugins) ? loaded.value.plugins : [];
5009
+ const trackedPackages = new Set(appendedByPrevious(previous, ["plugins"]).filter((entry) => isPlainObject(entry) && typeof entry.package === "string").map((entry) => entry.package));
5010
+ const pluginPackages = plugins.filter((entry) => isPlainObject(entry) && typeof entry.package === "string");
5011
+ const excusedRefs = previous === undefined ? [] : [...trackedPackages, input.packageRef];
5012
+ const isExcusedRef = lineageMatcher(excusedRefs);
5013
+ const foreignOwn = pluginPackages.filter((entry) => looksLikeOwnPackage(entry.package) && !isExcusedRef(entry.package));
5014
+ if (foreignOwn.length > 0) {
5015
+ errors.push({
5016
+ 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."
5017
+ });
4748
5018
  }
4749
- const blocked = options.blockedDomains.find((domain) => domainMatches(host, domain));
4750
- if (blocked) {
4751
- throw new Error(`Fetch blocked for ${host}: matched fetchBlockedDomains entry "${blocked}"`);
5019
+ if (previous !== undefined && pluginPackages.some((entry) => isExcusedRef(entry.package))) {
5020
+ 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.");
4752
5021
  }
4753
- if (options.allowedDomains.length > 0 && !options.allowedDomains.some((domain) => domainMatches(host, domain))) {
4754
- throw new Error(`Fetch not allowed for ${host}: add it to fetchAllowedDomains (exact host or subdomain)`);
5022
+ if (errors.length > 0) {
5023
+ return { host: "opencode", scope: input.scope, tiers, edits, notes, errors };
4755
5024
  }
4756
- }
4757
- function isPrivateIpv4(host) {
4758
- const parts = host.split(".").map((part) => Number(part));
4759
- if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
4760
- return true;
5025
+ const options = {};
5026
+ if (input.search === "forced")
5027
+ options.setDefaultWebsearch = true;
5028
+ if (input.search === "off")
5029
+ options.searchEnabled = false;
5030
+ if (input.fetch === "forced")
5031
+ options.fetchOverrideWebfetch = true;
5032
+ if (input.fetch === "off")
5033
+ options.fetchEnabled = false;
5034
+ const ops = [
5035
+ {
5036
+ op: "append-array",
5037
+ path: ["plugins"],
5038
+ entries: [{ package: input.packageRef, options }],
5039
+ existingCount: plugins.length
5040
+ }
5041
+ ];
5042
+ const permissions = [];
5043
+ if (input.search !== "off")
5044
+ permissions.push({ action: "websearch", resource: "*", effect: "allow" });
5045
+ if (input.fetch !== "off")
5046
+ permissions.push({ action: "deepseek_fetch", resource: "*", effect: "allow" });
5047
+ if (input.fetch === "forced")
5048
+ permissions.push({ action: "webfetch", resource: "*", effect: "allow" });
5049
+ const existingPermissions = Array.isArray(loaded.value.permissions) ? loaded.value.permissions.length : 0;
5050
+ if (permissions.length > 0) {
5051
+ ops.push({ op: "append-array", path: ["permissions"], entries: permissions, existingCount: existingPermissions });
4761
5052
  }
4762
- const [a, b] = parts;
4763
- 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;
4764
- }
4765
- function isPrivateIpv6(host) {
4766
- const value = host.toLowerCase();
4767
- if (value === "::" || value === "::1")
4768
- return true;
4769
- if (value.startsWith("fc") || value.startsWith("fd"))
4770
- return true;
4771
- if (/^fe[89ab]/.test(value))
4772
- return true;
4773
- const dottedMapped = value.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/)?.[1];
4774
- if (dottedMapped)
4775
- return isPrivateIpv4(dottedMapped);
4776
- const mapped = ipv4FromIpv6Hextets(value, /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
4777
- if (mapped)
4778
- return isPrivateIpv4(mapped);
4779
- const compatible = ipv4FromIpv6Hextets(value, /^::([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
4780
- if (compatible)
4781
- return isPrivateIpv4(compatible);
4782
- return false;
4783
- }
4784
- function ipv4FromIpv6Hextets(value, pattern) {
4785
- const match = value.match(pattern);
4786
- if (!match)
4787
- return;
4788
- const high = Number.parseInt(match[1], 16);
4789
- const low = Number.parseInt(match[2], 16);
4790
- if (!Number.isFinite(high) || !Number.isFinite(low))
4791
- return;
4792
- return `${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`;
5053
+ if (input.search === "forced") {
5054
+ const currentProvider = isPlainObject(loaded.value.websearch) ? loaded.value.websearch.provider : undefined;
5055
+ ops.push({ op: "set", path: ["websearch", "provider"], value: "deepseek", previous: currentProvider });
5056
+ 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.`);
5057
+ }
5058
+ edits.push({ file: paths.config, format: "json", ops });
5059
+ notes.push("The plugin entry uses the object form so options are honored; auto-discovered plugins cannot receive options.");
5060
+ if (input.search === "optional") {
5061
+ 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.");
5062
+ }
5063
+ if (input.search === "forced" || input.fetch === "forced") {
5064
+ notes.push(`Forced tier replaces built-in routing. ${FORCED_COST_NOTE}`);
5065
+ }
5066
+ if (input.fetch === "preferred") {
5067
+ edits.push({
5068
+ file: paths.rules,
5069
+ format: "rules",
5070
+ block: buildRulesBlock(input.host, rulesLines(OPENCODE_TOOL_IDS, input.search, input.fetch))
5071
+ });
5072
+ notes.push(`Rules block upserted in ${paths.rules}; OpenCode loads AGENTS.md only (no CLAUDE.md fallback in v2).`);
5073
+ }
5074
+ return { host: "opencode", scope: input.scope, tiers, edits, notes, errors };
4793
5075
  }
5076
+ var OPENCODE_TOOL_IDS;
5077
+ var init_opencode = __esm(() => {
5078
+ init_detect();
5079
+ init_formats();
5080
+ init_rules();
5081
+ init_value();
5082
+ init_ownership();
5083
+ init_paths();
5084
+ init_rulesContent();
5085
+ init_state();
5086
+ init_shared();
5087
+ OPENCODE_TOOL_IDS = { search: "websearch (provider deepseek)", fetch: "deepseek_fetch" };
5088
+ });
4794
5089
 
4795
- // src/core/options.ts
4796
- function resolvePluginOptions(input) {
4797
- return {
4798
- search: resolveSearchOptions(input),
4799
- fetch: resolveFetchOptions(input),
4800
- auth: resolveAuthOptions(input),
4801
- retry: resolveRetryOptions(input)
4802
- };
4803
- }
4804
- function resolveSearchOptions(input) {
4805
- return {
4806
- count: clampInt(input?.count, DEFAULT_SEARCH_COUNT, 1, 50),
4807
- maxResults: clampInt(input?.maxResults, DEFAULT_MAX_RESULTS, 0, 50),
4808
- minRerank: finiteNumber(input?.minRerank, DEFAULT_MIN_RERANK),
4809
- minAuthority: finiteNumber(input?.minAuthority, DEFAULT_MIN_AUTHORITY),
4810
- allowUnknownAuthority: input?.allowUnknownAuthority !== false,
4811
- blockedDomains: normalizeDomains(input?.blockedDomains),
4812
- includeDomains: normalizeDomains(input?.includeDomains),
4813
- language: optionalText(input?.language),
4814
- maxSummaryChars: clampInt(input?.maxSummaryChars, DEFAULT_MAX_SUMMARY_CHARS, 0, 1e6),
4815
- timeoutMs: clampInt(input?.searchTimeoutMs, DEFAULT_TIMEOUT_MS, MIN_TIMEOUT_MS, MAX_TIMEOUT_MS),
4816
- enabled: input?.searchEnabled !== false
4817
- };
4818
- }
4819
- function resolveFetchOptions(input) {
4820
- return {
4821
- maxChars: clampInt(input?.fetchMaxChars, DEFAULT_FETCH_MAX_CHARS, 0, 1e6),
4822
- allowedDomains: normalizeDomains(input?.fetchAllowedDomains),
4823
- blockedDomains: normalizeDomains(input?.fetchBlockedDomains),
4824
- timeoutMs: clampInt(input?.fetchTimeoutMs, DEFAULT_TIMEOUT_MS, MIN_TIMEOUT_MS, MAX_TIMEOUT_MS),
4825
- blockPrivateHosts: input?.fetchBlockPrivateHosts !== false,
4826
- enabled: input?.fetchEnabled !== false
4827
- };
4828
- }
4829
- function resolveAuthOptions(input) {
4830
- const value = input?.apiKeySource;
4831
- return {
4832
- source: value === "opencode" || value === "env" ? value : DEFAULT_API_KEY_SOURCE
4833
- };
4834
- }
4835
- function resolveRetryOptions(input) {
4836
- return {
4837
- maxRetries: clampInt(input?.maxRetries, DEFAULT_MAX_RETRIES, 0, MAX_RETRIES),
4838
- baseDelayMs: clampInt(input?.retryBaseDelayMs, DEFAULT_RETRY_BASE_DELAY_MS, 0, MAX_RETRY_DELAY_MS)
4839
- };
4840
- }
4841
- function optionalText(value) {
4842
- if (typeof value !== "string")
4843
- return;
4844
- const trimmed = value.trim();
4845
- return trimmed ? trimmed : undefined;
4846
- }
4847
- function finiteNumber(value, fallback) {
4848
- return typeof value === "number" && Number.isFinite(value) ? value : fallback;
4849
- }
4850
- function clampInt(value, fallback, min, max) {
4851
- const number = typeof value === "number" && Number.isFinite(value) ? Math.trunc(value) : fallback;
4852
- return Math.min(max, Math.max(min, number));
4853
- }
5090
+ // src/pi/settings.ts
5091
+ var SETTINGS_NAMESPACE = "deepseekWebRetrieval";
5092
+ var init_settings = () => {};
4854
5093
 
4855
- // src/core/env.ts
4856
- var NUMBER_ENV = {
4857
- count: "DEEPSEEK_SEARCH_COUNT",
4858
- maxResults: "DEEPSEEK_SEARCH_MAX_RESULTS",
4859
- minRerank: "DEEPSEEK_SEARCH_MIN_RERANK",
4860
- minAuthority: "DEEPSEEK_SEARCH_MIN_AUTHORITY",
4861
- maxSummaryChars: "DEEPSEEK_SEARCH_MAX_SUMMARY_CHARS",
4862
- searchTimeoutMs: "DEEPSEEK_SEARCH_TIMEOUT_MS",
4863
- fetchMaxChars: "DEEPSEEK_FETCH_MAX_CHARS",
4864
- fetchTimeoutMs: "DEEPSEEK_FETCH_TIMEOUT_MS",
4865
- maxRetries: "DEEPSEEK_MAX_RETRIES",
4866
- retryBaseDelayMs: "DEEPSEEK_RETRY_BASE_DELAY_MS"
4867
- };
4868
- var TEXT_ENV = {
4869
- language: "DEEPSEEK_SEARCH_LANGUAGE"
4870
- };
4871
- var DOMAINS_ENV = {
4872
- blockedDomains: "DEEPSEEK_SEARCH_BLOCKED_DOMAINS",
4873
- includeDomains: "DEEPSEEK_SEARCH_INCLUDE_DOMAINS",
4874
- fetchAllowedDomains: "DEEPSEEK_FETCH_ALLOWED_DOMAINS",
4875
- fetchBlockedDomains: "DEEPSEEK_FETCH_BLOCKED_DOMAINS"
4876
- };
4877
- var FALSE_ONLY_ENV = {
4878
- allowUnknownAuthority: "DEEPSEEK_SEARCH_ALLOW_UNKNOWN_AUTHORITY",
4879
- fetchBlockPrivateHosts: "DEEPSEEK_FETCH_BLOCK_PRIVATE_HOSTS",
4880
- searchEnabled: ENABLE_SEARCH_ENV,
4881
- fetchEnabled: ENABLE_FETCH_ENV
4882
- };
4883
- function resolveEnvOptions(env = process.env) {
4884
- return resolvePluginOptions(pluginOptionsFromEnv(env));
4885
- }
4886
- function pluginOptionsFromEnv(env) {
4887
- const options = {};
4888
- putNumber(options, "count", env);
4889
- putNumber(options, "maxResults", env);
4890
- putNumber(options, "minRerank", env);
4891
- putNumber(options, "minAuthority", env);
4892
- putFalseOnly(options, "allowUnknownAuthority", env);
4893
- putDomains(options, "blockedDomains", env);
4894
- putDomains(options, "includeDomains", env);
4895
- putText(options, "language", env);
4896
- putNumber(options, "maxSummaryChars", env);
4897
- putNumber(options, "searchTimeoutMs", env);
4898
- putNumber(options, "fetchMaxChars", env);
4899
- putDomains(options, "fetchAllowedDomains", env);
4900
- putDomains(options, "fetchBlockedDomains", env);
4901
- putNumber(options, "fetchTimeoutMs", env);
4902
- putFalseOnly(options, "fetchBlockPrivateHosts", env);
4903
- putFalseOnly(options, "searchEnabled", env);
4904
- putFalseOnly(options, "fetchEnabled", env);
4905
- putNumber(options, "maxRetries", env);
4906
- putNumber(options, "retryBaseDelayMs", env);
4907
- return options;
4908
- }
4909
- function putNumber(target, key, env) {
4910
- const value = env[NUMBER_ENV[key]];
4911
- if (value === undefined)
4912
- return;
4913
- const trimmed = value.trim();
4914
- if (!trimmed)
4915
- return;
4916
- const parsed = Number(trimmed);
4917
- if (Number.isFinite(parsed))
4918
- target[key] = parsed;
4919
- }
4920
- function putText(target, key, env) {
4921
- const value = env[TEXT_ENV[key]];
4922
- if (value === undefined)
4923
- return;
4924
- const trimmed = value.trim();
4925
- if (trimmed)
4926
- target[key] = trimmed;
4927
- }
4928
- function putFalseOnly(target, key, env) {
4929
- if (isFalseValue(env[FALSE_ONLY_ENV[key]]))
4930
- target[key] = false;
4931
- }
4932
- function putDomains(target, key, env) {
4933
- const value = env[DOMAINS_ENV[key]];
4934
- if (value === undefined || !value.trim())
4935
- return;
4936
- const domains = value.split(",").map((domain) => domain.trim()).filter((domain) => domain.length > 0);
4937
- if (domains.length > 0)
4938
- target[key] = domains;
5094
+ // src/installer/hosts/pi.ts
5095
+ function piPaths(input) {
5096
+ const mcp = input.scope === "global" ? join2(input.home, ".pi/agent/mcp.json") : join2(input.cwd, ".pi/mcp.json");
5097
+ const settings = input.scope === "global" ? join2(input.home, ".pi/agent/settings.json") : join2(input.cwd, ".pi/settings.json");
5098
+ const rules = input.scope === "global" ? join2(input.home, ".pi/agent/AGENTS.md") : join2(input.cwd, "AGENTS.md");
5099
+ return { config: input.path === "mcp" ? mcp : settings, rules };
4939
5100
  }
4940
- function isFalseValue(value) {
4941
- const trimmed = value?.trim().toLowerCase();
4942
- return trimmed === "false" || trimmed === "0" || trimmed === "no" || trimmed === "off";
5101
+ function piSourceRef(packageRef) {
5102
+ return packageRef.startsWith("npm:") ? packageRef : `npm:${packageRef}`;
4943
5103
  }
4944
-
4945
- // src/mcp/auth.ts
4946
- init_constants();
4947
- var MISSING_API_KEY_MESSAGE = `DeepSeek API key not found. Set ${API_KEY_ENV} in the MCP server's env configuration.`;
4948
- function requireApiKey(env) {
4949
- const apiKey = apiKeyFromEnv(env);
4950
- if (!apiKey)
4951
- throw new Error(MISSING_API_KEY_MESSAGE);
4952
- return apiKey;
5104
+ function planPi(input, previous) {
5105
+ return input.path === "mcp" ? planPiMcp(input) : planPiExtension(input, previous);
4953
5106
  }
4954
-
4955
- // src/mcp/client.ts
4956
- import { CLIENT_INFO_META_KEY } from "@modelcontextprotocol/server";
4957
- var JSON_REPLACING_CLIENT_NAMES = new Set(["codex-mcp-client"]);
4958
- function clientNameOf(server, ctx) {
4959
- const envelope = ctx.mcpReq.envelope;
4960
- const name = envelope?.[CLIENT_INFO_META_KEY];
4961
- if (typeof name === "object" && name !== null && "name" in name && typeof name.name === "string") {
4962
- return name.name;
5107
+ function planPiExtension(input, previous) {
5108
+ const paths = piPaths(input);
5109
+ const loaded = codecFor("json", input.host).load(paths.config);
5110
+ const tiers = { search: input.search, fetch: input.fetch };
5111
+ const header = planHeader(input, paths, loaded, "JSON");
5112
+ if (header.abort) {
5113
+ return { host: "pi", scope: input.scope, tiers, edits: [], notes: header.notes, errors: header.errors };
4963
5114
  }
4964
- return server.server.getClientVersion()?.name;
5115
+ const notes = [...header.notes];
5116
+ const errors = [...header.errors];
5117
+ const edits = [];
5118
+ const packages = Array.isArray(loaded.value.packages) ? loaded.value.packages : [];
5119
+ const source = piSourceRef(input.packageRef);
5120
+ const trackedSources = appendedByPrevious(previous, ["packages"]).filter((entry) => typeof entry === "string");
5121
+ const sources = packages.filter((entry) => typeof entry === "string");
5122
+ const excusedSources = previous === undefined ? [] : [...trackedSources, source];
5123
+ const isExcusedSource = lineageMatcher(excusedSources);
5124
+ const foreignOwn = sources.filter((entry) => looksLikeOwnPackage(entry) && !isExcusedSource(entry));
5125
+ if (foreignOwn.length > 0) {
5126
+ errors.push({
5127
+ 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."
5128
+ });
5129
+ }
5130
+ if (previous !== undefined && sources.some((entry) => isExcusedSource(entry))) {
5131
+ 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.");
5132
+ }
5133
+ const forced = input.search === "forced" || input.fetch === "forced";
5134
+ if (forced) {
5135
+ const isOwnSource = lineageMatcher([...trackedSources, source]);
5136
+ const unexcludable = [...collectSources(loaded.value.packages), ...collectSources(loaded.value.extensions)].filter((entry) => !isOwnSource(entry) && looksWebCapable(entry, undefined));
5137
+ if (unexcludable.length > 0) {
5138
+ errors.push({
5139
+ 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."
5140
+ });
5141
+ }
5142
+ }
5143
+ if (errors.length > 0) {
5144
+ return { host: "pi", scope: input.scope, tiers, edits: [], notes, errors };
5145
+ }
5146
+ const ops = [
5147
+ { op: "append-array", path: ["packages"], entries: [source], existingCount: packages.length }
5148
+ ];
5149
+ const namespace = isPlainObject(loaded.value[SETTINGS_NAMESPACE]) ? loaded.value[SETTINGS_NAMESPACE] : {};
5150
+ if (input.search === "off") {
5151
+ ops.push({
5152
+ op: "set",
5153
+ path: [SETTINGS_NAMESPACE, "enableSearch"],
5154
+ value: false,
5155
+ previous: typeof namespace.enableSearch === "boolean" ? namespace.enableSearch : undefined
5156
+ });
5157
+ }
5158
+ if (input.fetch === "off") {
5159
+ ops.push({
5160
+ op: "set",
5161
+ path: [SETTINGS_NAMESPACE, "enableFetch"],
5162
+ value: false,
5163
+ previous: typeof namespace.enableFetch === "boolean" ? namespace.enableFetch : undefined
5164
+ });
5165
+ }
5166
+ edits.push({ file: paths.config, format: "json", ops });
5167
+ 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).");
5168
+ 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.");
5169
+ if (input.search === "off" || input.fetch === "off") {
5170
+ 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).");
5171
+ }
5172
+ notes.push(`Extension tools are declared to the model with exposure "direct" by Pi's own default; no exposure key is needed.`);
5173
+ if (forced) {
5174
+ edits.push(...planMcpCompetitorDisables(input, notes, errors));
5175
+ }
5176
+ if (errors.length > 0) {
5177
+ return { host: "pi", scope: input.scope, tiers, edits: [], notes, errors };
5178
+ }
5179
+ if (input.search === "preferred" || input.fetch === "preferred") {
5180
+ edits.push({
5181
+ file: paths.rules,
5182
+ format: "rules",
5183
+ block: buildRulesBlock(input.host, rulesLines(RAW_MCP_TOOL_IDS, input.search, input.fetch))
5184
+ });
5185
+ notes.push(`Rules block upserted in ${paths.rules}; Pi loads AGENTS.md context files from the working directory and its parents.`);
5186
+ }
5187
+ if (forced) {
5188
+ 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);
5189
+ }
5190
+ return { host: "pi", scope: input.scope, tiers, edits, notes, errors };
4965
5191
  }
4966
- function isJsonReplacingClient(name) {
4967
- return name !== undefined && JSON_REPLACING_CLIENT_NAMES.has(name);
5192
+ function planCompetitorDisables(servers, competitors, notes, errors) {
5193
+ const ops = [];
5194
+ const excluded = [];
5195
+ const unexcludable = [];
5196
+ for (const name of competitors) {
5197
+ const current = servers[name];
5198
+ if (!isPlainObject(current)) {
5199
+ unexcludable.push(name);
5200
+ continue;
5201
+ }
5202
+ const previous = typeof current.enabled === "boolean" ? current.enabled : undefined;
5203
+ ops.push({ op: "set", path: ["mcpServers", name, "enabled"], value: false, previous });
5204
+ excluded.push(name);
5205
+ }
5206
+ if (excluded.length > 0) {
5207
+ notes.push(`Forced tier: web-capable MCP servers are disabled so DeepSeek routing is exclusive (${excluded.join(", ")}; ` + "prior enabled values are snapshotted for uninstall).");
5208
+ }
5209
+ if (unexcludable.length > 0) {
5210
+ errors.push({
5211
+ 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."
5212
+ });
5213
+ }
5214
+ return ops;
4968
5215
  }
4969
- function omitStructuredContent(server, ctx) {
4970
- return isJsonReplacingClient(clientNameOf(server, ctx));
5216
+ function planMcpCompetitorDisables(input, notes, errors) {
5217
+ const mcpFile = input.scope === "global" ? join2(input.home, ".pi/agent/mcp.json") : join2(input.cwd, ".pi/mcp.json");
5218
+ const loaded = codecFor("json", input.host).load(mcpFile);
5219
+ if (loaded.parseError) {
5220
+ errors.push({ message: `${mcpFile} does not parse: ${loaded.parseError}` });
5221
+ return [];
5222
+ }
5223
+ const servers = isPlainObject(loaded.value.mcpServers) ? loaded.value.mcpServers : undefined;
5224
+ if (!isPlainObject(servers))
5225
+ return [];
5226
+ const { competitors, bystanders } = splitCompetitors(servers);
5227
+ const edits = [];
5228
+ if (competitors.length > 0) {
5229
+ const ops = planCompetitorDisables(servers, competitors, notes, errors);
5230
+ if (ops.length > 0)
5231
+ edits.push({ file: mcpFile, format: "json", ops });
5232
+ }
5233
+ if (bystanders.length > 0)
5234
+ notes.push(othersStayRegisteredNote(bystanders));
5235
+ return edits;
4971
5236
  }
4972
-
4973
- // src/mcp/server.ts
4974
- import { McpServer } from "@modelcontextprotocol/server";
4975
- import * as z from "zod/v4";
4976
- // package.json
4977
- var package_default = {
4978
- $schema: "https://json.schemastore.org/package.json",
4979
- name: "deepseek-web-retrieval",
4980
- version: "0.5.1",
4981
- description: "Token-efficient DeepSeek Search and Fetch for coding agents, starting with OpenCode v2.",
4982
- type: "module",
4983
- license: "MIT",
4984
- repository: {
4985
- type: "git",
4986
- url: "git+https://github.com/gwokhou/deepseek-web-retrieval.git"
4987
- },
4988
- homepage: "https://github.com/gwokhou/deepseek-web-retrieval#readme",
4989
- bugs: "https://github.com/gwokhou/deepseek-web-retrieval/issues",
4990
- keywords: [
4991
- "opencode",
4992
- "opencode-plugin",
4993
- "pi",
4994
- "pi-extension",
4995
- "deepseek",
4996
- "websearch",
4997
- "webfetch",
4998
- "coding-agent",
4999
- "mcp"
5000
- ],
5001
- exports: {
5002
- ".": "./index.ts",
5003
- "./core": "./src/core/index.ts",
5004
- "./mcp": "./src/mcp/server.ts",
5005
- "./pi": "./src/pi/extension.ts"
5006
- },
5007
- pi: {
5008
- extensions: [
5009
- "./src/pi/extension.ts"
5010
- ]
5011
- },
5012
- bin: {
5013
- "deepseek-web-retrieval": "dist/cli.js"
5014
- },
5015
- files: [
5016
- "index.ts",
5017
- "src",
5018
- "dist",
5019
- "docs",
5020
- "README.md",
5021
- "LICENSE",
5022
- "CHANGELOG.md",
5023
- "examples"
5024
- ],
5025
- scripts: {
5026
- lint: "biome check --error-on-warnings .",
5027
- "lint:ci": "biome ci --error-on-warnings .",
5028
- "lint:fix": "biome check --write --error-on-warnings .",
5029
- format: "biome format --write .",
5030
- typecheck: "tsc --noEmit",
5031
- 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",
5032
- prepublishOnly: "bun run check:release",
5033
- test: "bun test",
5034
- "test:package": "node scripts/test-package.mjs",
5035
- "test:e2e:keyless": "bun run test:e2e pi pi-extension installer reinit upgrade pkg capability-off",
5036
- "check:release": "bun run check && bun run test:package && bun run test:e2e:keyless",
5037
- "test:e2e": "bun run test/e2e/run.ts",
5038
- "docs:matrix": "bun run scripts/render-matrix.ts",
5039
- "test:coverage": "bun test --coverage",
5040
- check: "bun run lint && bun run typecheck && bun run docs:matrix --check && bun run build && bun test"
5041
- },
5042
- peerDependencies: {
5043
- "@earendil-works/pi-ai": "^1.3.0",
5044
- "@earendil-works/pi-coding-agent": "^1.3.0",
5045
- "@opencode/plugin": "^2.0.32"
5046
- },
5047
- peerDependenciesMeta: {
5048
- "@earendil-works/pi-ai": {
5049
- optional: true
5050
- },
5051
- "@earendil-works/pi-coding-agent": {
5052
- optional: true
5053
- },
5054
- "@opencode/plugin": {
5055
- optional: true
5056
- }
5057
- },
5058
- devDependencies: {
5059
- "@biomejs/biome": "2.5.15",
5060
- "@earendil-works/pi-ai": "1.3.0",
5061
- "@earendil-works/pi-coding-agent": "1.3.0",
5062
- "@modelcontextprotocol/client": "^2.3.1",
5063
- "@opencode/plugin": "2.0.32",
5064
- "@types/bun": "1.4.0",
5065
- "@types/node": "^26.6.5",
5066
- typescript: "5.9.3"
5067
- },
5068
- engines: {
5069
- bun: ">=1.3.0",
5070
- node: ">=20"
5071
- },
5072
- dependencies: {
5073
- "@modelcontextprotocol/server": "^2.3.1",
5074
- "jsonc-parser": "^3.3.1",
5075
- "smol-toml": "^1.9.1",
5076
- zod: "^4"
5237
+ function planPiMcp(input) {
5238
+ const paths = piPaths(input);
5239
+ const loaded = codecFor("json", input.host).load(paths.config);
5240
+ const tiers = { search: input.search, fetch: input.fetch };
5241
+ const header = planHeader(input, paths, loaded, "JSON");
5242
+ if (header.abort) {
5243
+ return { host: "pi", scope: input.scope, tiers, edits: [], notes: header.notes, errors: header.errors };
5077
5244
  }
5078
- };
5079
-
5080
- // src/core/client.ts
5081
- init_constants();
5245
+ const notes = [...header.notes];
5246
+ const errors = [...header.errors];
5247
+ const edits = [];
5248
+ const env = { [API_KEY_ENV]: `\${${API_KEY_ENV}}` };
5249
+ if (input.search === "off")
5250
+ env[ENABLE_SEARCH_ENV] = "false";
5251
+ if (input.fetch === "off")
5252
+ env[ENABLE_FETCH_ENV] = "false";
5253
+ const entry = {
5254
+ command: "npx",
5255
+ args: ["-y", input.packageRef],
5256
+ env,
5257
+ exposure: "direct",
5258
+ enabled: true
5259
+ };
5260
+ const servers = loaded.value.mcpServers;
5261
+ const existingEntry = isPlainObject(servers) ? servers.deepseek : undefined;
5262
+ const ops = [{ op: "set", path: ["mcpServers", "deepseek"], value: entry, previous: existingEntry }];
5263
+ edits.push({ file: paths.config, format: "json", ops });
5264
+ if (isPlainObject(servers)) {
5265
+ if (servers.deepseek !== undefined)
5266
+ notes.push(replacingExistingEntryNote("mcpServers.deepseek"));
5267
+ const { competitors, bystanders } = splitCompetitors(servers);
5268
+ const forced = input.search === "forced" || input.fetch === "forced";
5269
+ if (forced) {
5270
+ ops.push(...planCompetitorDisables(servers, competitors, notes, errors));
5271
+ }
5272
+ const stayRegistered = forced ? bystanders : [...competitors, ...bystanders];
5273
+ if (stayRegistered.length > 0)
5274
+ notes.push(othersStayRegisteredNote(stayRegistered));
5275
+ }
5276
+ notes.push('"exposure": "direct" is required: Pi 1.3.0 defaults MCP tools to codemode, which hides them from the model.');
5277
+ 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.`);
5278
+ if (input.search === "preferred" || input.fetch === "preferred") {
5279
+ edits.push({
5280
+ file: paths.rules,
5281
+ format: "rules",
5282
+ block: buildRulesBlock(input.host, rulesLines(MCP_TOOL_IDS, input.search, input.fetch))
5283
+ });
5284
+ notes.push(`Rules block upserted in ${paths.rules}; Pi loads AGENTS.md context files from the working directory and its parents.`);
5285
+ }
5286
+ if (input.search === "forced" || input.fetch === "forced") {
5287
+ 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);
5288
+ }
5289
+ return { host: "pi", scope: input.scope, tiers, edits, notes, errors };
5290
+ }
5291
+ function collectSources(value) {
5292
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
5293
+ }
5294
+ var init_pi = __esm(() => {
5295
+ init_constants();
5296
+ init_settings();
5297
+ init_detect();
5298
+ init_formats();
5299
+ init_rules();
5300
+ init_value();
5301
+ init_ownership();
5302
+ init_paths();
5303
+ init_rulesContent();
5304
+ init_state();
5305
+ init_shared();
5306
+ });
5082
5307
 
5083
- // src/core/format.ts
5084
- init_constants();
5085
- function truncate(value, maxChars) {
5086
- if (maxChars <= 0)
5087
- return "";
5088
- if (value.length <= maxChars)
5089
- return value;
5090
- return Array.from(value).slice(0, maxChars).join("");
5308
+ // src/installer/hosts/index.ts
5309
+ function planForHost(host, input) {
5310
+ try {
5311
+ const previous = readRecord(input);
5312
+ const planned = withCrossScopeGuard(HOST_PLANNERS[host](input, previous), input);
5313
+ return {
5314
+ ...withSecretGuard(withOrphanReverts(withLegacySharedConfigGuard(planned, previous), previous)),
5315
+ previousRecord: previous
5316
+ };
5317
+ } catch (error) {
5318
+ if (!(error instanceof InstallRecordError))
5319
+ throw error;
5320
+ return {
5321
+ host,
5322
+ scope: input.scope,
5323
+ tiers: { search: input.search, fetch: input.fetch },
5324
+ edits: [],
5325
+ notes: [],
5326
+ errors: [{ message: error.message }]
5327
+ };
5328
+ }
5091
5329
  }
5092
- function formatFetchedContent(title, content, maxChars) {
5093
- const document = withTitle(title, content);
5094
- const length = Array.from(document).length;
5095
- if (maxChars <= 0)
5096
- return { content: "", truncated: length > 0 };
5097
- if (length <= maxChars)
5098
- return { content: document, truncated: false };
5099
- const markerLength = Array.from(FETCH_TRUNCATION_MARKER).length;
5100
- if (maxChars <= markerLength) {
5101
- return { content: truncate(document, maxChars), truncated: true };
5330
+ function withCrossScopeGuard(plan, input) {
5331
+ if (plan.errors.length > 0)
5332
+ return plan;
5333
+ try {
5334
+ assertFileOwnership(input, plan.edits);
5335
+ } catch (error) {
5336
+ return { ...plan, edits: [], reverts: undefined, errors: [{ message: String(error) }] };
5337
+ }
5338
+ const other = input.scope === "global" ? "project" : "global";
5339
+ const overlap = claimedByOtherScope(input, plan.edits.map((edit2) => edit2.file));
5340
+ if (overlap.length === 0)
5341
+ return { ...plan, notes: [...plan.notes, ...fileOwnershipNotes(input, plan.edits)] };
5342
+ return {
5343
+ ...plan,
5344
+ edits: [],
5345
+ reverts: undefined,
5346
+ errors: [
5347
+ {
5348
+ 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.`
5349
+ }
5350
+ ]
5351
+ };
5352
+ }
5353
+ function withLegacySharedConfigGuard(plan, previous) {
5354
+ if (plan.errors.length > 0 || previous !== undefined)
5355
+ return plan;
5356
+ for (const edit2 of plan.edits) {
5357
+ if (edit2.format === "rules" || !edit2.shared)
5358
+ continue;
5359
+ for (const op of edit2.ops) {
5360
+ const old = op.previous;
5361
+ 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))
5362
+ continue;
5363
+ return {
5364
+ ...plan,
5365
+ edits: [],
5366
+ reverts: undefined,
5367
+ errors: [
5368
+ {
5369
+ 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."
5370
+ }
5371
+ ]
5372
+ };
5373
+ }
5102
5374
  }
5103
- return {
5104
- content: `${truncate(document, maxChars - markerLength)}${FETCH_TRUNCATION_MARKER}`,
5105
- truncated: true
5106
- };
5375
+ return plan;
5107
5376
  }
5108
- function withTitle(title, content) {
5109
- const heading = title.trim();
5110
- if (!heading)
5111
- return content;
5112
- const body = content.trimStart();
5113
- const lines = body.split(/\r?\n/);
5114
- const firstHeading = lines[0]?.match(/^\\?#\s+(.+?)\s*$/)?.[1];
5115
- if (firstHeading && normalizeHeading(firstHeading) === normalizeHeading(heading)) {
5116
- lines[0] = `# ${heading}`;
5117
- return lines.join(`
5118
- `);
5377
+ function withSecretGuard(plan) {
5378
+ for (const edit2 of plan.edits) {
5379
+ if (edit2.format === "rules")
5380
+ continue;
5381
+ for (const op of edit2.ops) {
5382
+ if (op.op !== "set" || op.previous === undefined)
5383
+ continue;
5384
+ const offender = findSecretLike(op.previous, op.path.join("."));
5385
+ if (offender !== undefined) {
5386
+ return {
5387
+ ...plan,
5388
+ edits: [],
5389
+ reverts: undefined,
5390
+ errors: [
5391
+ {
5392
+ 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.`
5393
+ }
5394
+ ]
5395
+ };
5396
+ }
5397
+ }
5119
5398
  }
5120
- return `# ${heading}
5121
-
5122
- ${content}`;
5123
- }
5124
- function normalizeHeading(value) {
5125
- return value.trim().replace(/\s+/g, " ").replace(/\s+#+\s*$/, "").toLowerCase();
5126
- }
5127
- function redactSecrets(value) {
5128
- return value.replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, "sk-***").replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi, "Bearer ***");
5129
- }
5130
- function formatApiError(operation, status, body) {
5131
- const detail = redactSecrets(body.trim());
5132
- const detailChars = Array.from(detail);
5133
- const text = detail ? detailChars.length > MAX_ERROR_BODY_CHARS ? `${detailChars.slice(0, MAX_ERROR_BODY_CHARS).join("")}…` : detail : "empty response body";
5134
- const hint = status === 401 ? "invalid API key" : status === 402 ? "insufficient balance" : status === 403 ? "permission denied" : status === 429 ? "rate limited" : undefined;
5135
- const label = hint ? `${operation} failed (HTTP ${status}, ${hint})` : `${operation} failed (HTTP ${status})`;
5136
- return `${label}: ${text}`;
5137
- }
5138
- function formatTimeoutError(operation, timeoutMs) {
5139
- return `${operation} timed out after ${timeoutMs} ms`;
5399
+ return plan;
5140
5400
  }
5141
- function formatSearchResults(results) {
5142
- if (results.length === 0)
5143
- return "No results found.";
5144
- return results.map((result, index) => {
5145
- const title = result.title?.trim() || result.url;
5146
- const lines = [`${index + 1}. [${escapeBrackets(title)}](${result.url})`];
5147
- if (result.content) {
5148
- lines.push(` ${result.content.replaceAll(`
5149
- `, " ")}`);
5401
+ function findSecretLike(value, at) {
5402
+ if (typeof value !== "object" || value === null)
5403
+ return;
5404
+ for (const [key, child] of Object.entries(value)) {
5405
+ const path = `${at}.${key}`;
5406
+ if (/(api[_-]?key|token|secret|password|credential)/i.test(key) && typeof child === "string" && child.length > 0 && !/^\$\{[^}]+\}$/.test(child)) {
5407
+ return path;
5150
5408
  }
5151
- if (result.published !== undefined) {
5152
- lines.push(` Published: ${formatPublished(result.published)}`);
5409
+ const nested = findSecretLike(child, path);
5410
+ if (nested !== undefined)
5411
+ return nested;
5412
+ }
5413
+ return;
5414
+ }
5415
+ function withOrphanReverts(plan, previous) {
5416
+ if (plan.errors.length > 0)
5417
+ return plan;
5418
+ if (previous === undefined)
5419
+ return plan;
5420
+ const reverts = [];
5421
+ for (const file of previous.files) {
5422
+ const edit2 = plan.edits.find((candidate) => candidate.file === file.file);
5423
+ if (edit2 === undefined) {
5424
+ reverts.push(file);
5425
+ continue;
5153
5426
  }
5154
- return lines.join(`
5155
- `);
5156
- }).join(`
5157
-
5158
- `);
5427
+ if (edit2.format === "rules" || file.format === "rules")
5428
+ continue;
5429
+ const orphanOps = file.ops.flatMap((op) => {
5430
+ const claimed = edit2.ops.find((candidate) => candidate.op === op.op && candidate.path.join("\x00") === op.path.join("\x00"));
5431
+ if (op.op !== "append-array" || claimed === undefined || claimed.op !== "append-array") {
5432
+ return claimed === undefined ? [op] : [];
5433
+ }
5434
+ const dropped = (Array.isArray(op.applied) ? op.applied : []).filter((entry) => !(claimed.entries ?? []).some((wanted) => deepEqual(wanted, entry)));
5435
+ return dropped.length > 0 ? [{ ...op, applied: dropped }] : [];
5436
+ });
5437
+ if (orphanOps.length > 0)
5438
+ reverts.push({ ...file, ops: orphanOps });
5439
+ }
5440
+ if (reverts.length === 0)
5441
+ return plan;
5442
+ return { ...plan, reverts };
5159
5443
  }
5160
- function toStructuredResult(result) {
5161
- return {
5162
- url: result.url,
5163
- ...result.title ? { title: result.title } : {},
5164
- ...result.content ? { content: result.content } : {},
5165
- ...result.published !== undefined ? { published: result.published } : {}
5444
+ var HOST_PLANNERS, HOST_VERSION_PROBES;
5445
+ var init_hosts = __esm(() => {
5446
+ init_constants();
5447
+ init_value();
5448
+ init_ownership();
5449
+ init_state();
5450
+ init_codex();
5451
+ init_opencode();
5452
+ init_pi();
5453
+ HOST_PLANNERS = {
5454
+ opencode: planOpenCode,
5455
+ pi: planPi,
5456
+ codex: planCodex
5166
5457
  };
5167
- }
5168
- function escapeBrackets(value) {
5169
- return value.replaceAll("[", "\\[").replaceAll("]", "\\]");
5170
- }
5171
- function formatPublished(epochMs) {
5172
- return new Date(epochMs).toISOString().slice(0, 10);
5173
- }
5458
+ HOST_VERSION_PROBES = {
5459
+ codex: probeCodexVersion
5460
+ };
5461
+ });
5174
5462
 
5175
- // src/core/url.ts
5176
- init_constants();
5177
- function parseFetchUrl(value) {
5178
- const input = value.trim();
5179
- if (!input)
5180
- throw new Error("URL must not be empty");
5181
- if (input.length > MAX_URL_CHARS) {
5182
- throw new Error(`URL must be ${MAX_URL_CHARS} characters or fewer`);
5183
- }
5184
- let url;
5185
- try {
5186
- url = new URL(input);
5187
- } catch {
5188
- throw new Error("URL must be an absolute http:// or https:// URL");
5189
- }
5190
- if (url.protocol !== "http:" && url.protocol !== "https:") {
5191
- throw new Error("URL must use http:// or https://");
5463
+ // src/installer/render.ts
5464
+ function renderPlan(plan) {
5465
+ const lines = [];
5466
+ lines.push(`Install plan: ${plan.host} (${plan.scope} scope)`);
5467
+ lines.push(`Tiers: search=${plan.tiers.search}, fetch=${plan.tiers.fetch}`);
5468
+ lines.push("");
5469
+ lines.push("Files:");
5470
+ for (const edit2 of plan.edits) {
5471
+ lines.push(` ${edit2.file}`);
5472
+ if (edit2.format === "rules") {
5473
+ lines.push(" upsert marked rules block");
5474
+ continue;
5475
+ }
5476
+ for (const op of edit2.ops) {
5477
+ if (op.op === "set") {
5478
+ const was = op.previous === undefined ? "(absent)" : preview(op.previous);
5479
+ lines.push(` set ${op.path.join(".")} = ${preview(op.value)} (was ${was})`);
5480
+ } else {
5481
+ const count = (op.entries ?? []).length;
5482
+ const kept = op.existingCount !== undefined ? ` (${op.existingCount} existing kept)` : "";
5483
+ lines.push(` append ${count} entr${count === 1 ? "y" : "ies"} to ${op.path.join(".")}${kept}`);
5484
+ for (const entry of op.entries ?? [])
5485
+ lines.push(` + ${preview(entry)}`);
5486
+ }
5487
+ }
5192
5488
  }
5193
- if (!url.hostname || !normalizeHostname(url.hostname)) {
5194
- throw new Error("URL must include a host");
5489
+ if (plan.reverts && plan.reverts.length > 0) {
5490
+ lines.push("");
5491
+ lines.push("Reverts (artifacts from the previous init that this plan no longer wants):");
5492
+ for (const revert of plan.reverts) {
5493
+ lines.push(` ${revert.file}`);
5494
+ if (revert.format === "rules") {
5495
+ lines.push(" remove rules block");
5496
+ continue;
5497
+ }
5498
+ for (const op of revert.ops) {
5499
+ if (op.op === "set") {
5500
+ lines.push(op.previous === undefined ? ` remove ${op.path.join(".")} (was not set before this install)` : ` restore ${op.path.join(".")} = ${preview(op.previous)}`);
5501
+ } else {
5502
+ const count = Array.isArray(op.applied) ? op.applied.length : 0;
5503
+ lines.push(` remove ${count} appended entr${count === 1 ? "y" : "ies"} from ${op.path.join(".")}`);
5504
+ }
5505
+ }
5506
+ }
5195
5507
  }
5196
- if (url.username || url.password) {
5197
- throw new Error("URL must not contain credentials");
5508
+ if (plan.notes.length > 0) {
5509
+ lines.push("");
5510
+ lines.push("Notes:");
5511
+ for (const note of plan.notes)
5512
+ lines.push(` - ${note}`);
5198
5513
  }
5199
- return url;
5514
+ return lines.join(`
5515
+ `);
5200
5516
  }
5201
- function normalizeResultUrl(value) {
5202
- if (typeof value !== "string")
5203
- return;
5204
- const input = value.trim();
5205
- if (!input || input.length > MAX_URL_CHARS)
5206
- return;
5207
- try {
5208
- const url = new URL(input);
5209
- if (url.protocol !== "http:" && url.protocol !== "https:")
5210
- return;
5211
- if (!url.hostname || url.username || url.password)
5212
- return;
5213
- return url.href;
5214
- } catch {
5215
- return;
5517
+ function preview(value) {
5518
+ const text = JSON.stringify(value) ?? String(value);
5519
+ return text.length > 100 ? `${text.slice(0, 97)}...` : text;
5520
+ }
5521
+
5522
+ // src/installer/uninstall.ts
5523
+ function uninstall(input) {
5524
+ return withOperationLock([statePathFor(input)], () => {
5525
+ const record = readRecord(input);
5526
+ if (record === undefined) {
5527
+ throw new Error(`No ${input.host} install is recorded for the ${input.scope} scope.`);
5528
+ }
5529
+ return withOperationLock(record.files.map((file) => file.file), () => uninstallLocked(input, record));
5530
+ });
5531
+ }
5532
+ function uninstallLocked(input, record) {
5533
+ assertFileOwnership(input, record.files);
5534
+ const other = input.scope === "global" ? "project" : "global";
5535
+ const overlap = claimedByOtherScope(input, record.files.map((file) => file.file));
5536
+ if (overlap.length > 0) {
5537
+ 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.`);
5538
+ }
5539
+ const lines = [];
5540
+ for (const file of record.files) {
5541
+ if (file.format !== "rules")
5542
+ continue;
5543
+ const loaded = codecFor("rules", record.host).load(file.file);
5544
+ if (loaded.parseError)
5545
+ throw new Error(`${file.file}: ${loaded.parseError}`);
5546
+ }
5547
+ for (const file of [...record.files].reverse()) {
5548
+ lines.push(...revertRecordedFile(file, record.host, { label: "", whole: true }));
5216
5549
  }
5550
+ deleteRecord(input);
5551
+ releaseFileOwnership(input, record.files);
5552
+ pruneEmptyDir(stateDir(input, input.scope));
5553
+ lines.push(`removed install record (${input.host}, ${input.scope})`);
5554
+ return lines;
5217
5555
  }
5556
+ var init_uninstall = __esm(() => {
5557
+ init_formats();
5558
+ init_operation_lock();
5559
+ init_ownership();
5560
+ init_paths();
5561
+ init_revert();
5562
+ init_state();
5563
+ });
5218
5564
 
5219
- // src/core/search.ts
5220
- function buildQuery(query, options) {
5221
- const parts = [query.trim()];
5222
- for (const domain of normalizeDomains(options.blockedDomains))
5223
- parts.push(`-site:${domain}`);
5224
- const include = normalizeDomains(options.includeDomains);
5225
- if (include.length)
5226
- parts.push(include.map((domain) => `site:${domain}`).join(" OR "));
5227
- const language = options.language?.trim().replace(/^lang:/i, "");
5228
- if (language)
5229
- parts.push(`lang:${language}`);
5230
- return parts.filter((part) => part.length > 0).join(" ");
5231
- }
5232
- function parsePublished(value) {
5233
- if (typeof value !== "string" || !value.trim())
5234
- return;
5235
- const timestamp = Date.parse(value);
5236
- return Number.isFinite(timestamp) ? timestamp : undefined;
5237
- }
5238
- function processResults(raw, options) {
5239
- const results = [];
5240
- const seen = new Set;
5241
- for (const item of raw) {
5242
- if (results.length >= options.maxResults)
5243
- break;
5244
- if (!item || typeof item !== "object")
5245
- continue;
5246
- const url = normalizeResultUrl(item.url);
5247
- if (!url || seen.has(url))
5565
+ // src/installer/cli.ts
5566
+ var exports_cli = {};
5567
+ __export(exports_cli, {
5568
+ runInstallerCli: () => runInstallerCli,
5569
+ resolveConfigHome: () => resolveConfigHome,
5570
+ exitCodeForError: () => exitCodeForError
5571
+ });
5572
+ import { homedir as homedir2 } from "node:os";
5573
+ import { resolve as resolve2 } from "node:path";
5574
+ function parseFlags(args) {
5575
+ const values = new Map;
5576
+ const booleans = new Set;
5577
+ const problems = [];
5578
+ for (let i = 0;i < args.length; i += 1) {
5579
+ const arg = args[i];
5580
+ if (!arg.startsWith("--")) {
5581
+ problems.push(`unexpected argument "${arg}" (flags only)`);
5248
5582
  continue;
5249
- const rerank = item.rerank_score;
5250
- if (typeof rerank === "number" && Number.isFinite(rerank)) {
5251
- if (rerank < options.minRerank)
5583
+ }
5584
+ const eq = arg.indexOf("=");
5585
+ const name = eq === -1 ? arg.slice(2) : arg.slice(2, eq);
5586
+ if (BOOLEAN_FLAGS.has(name)) {
5587
+ if (eq !== -1) {
5588
+ problems.push(`--${name} does not accept a value; use --${name} to enable it or omit it`);
5252
5589
  continue;
5253
- } else if (options.minRerank > 0) {
5590
+ }
5591
+ if (booleans.has(name)) {
5592
+ problems.push(`--${name} was given more than once`);
5593
+ continue;
5594
+ }
5595
+ booleans.add(name);
5254
5596
  continue;
5255
5597
  }
5256
- const authority = item.authority;
5257
- const knownAuthority = typeof authority === "number" && Number.isFinite(authority) && authority >= 0;
5258
- if (knownAuthority) {
5259
- if (authority < options.minAuthority)
5260
- continue;
5261
- } else if (!options.allowUnknownAuthority) {
5598
+ if (!VALUE_FLAGS.has(name)) {
5599
+ problems.push(`unknown flag --${name}`);
5262
5600
  continue;
5263
5601
  }
5264
- seen.add(url);
5265
- const content = truncate(optionalText2(item.summary) ?? optionalText2(item.snippet) ?? "", options.maxSummaryChars);
5266
- const published = parsePublished(item.date);
5267
- const title = optionalText2(item.title);
5268
- results.push({
5269
- url,
5270
- ...title ? { title } : {},
5271
- ...content ? { content } : {},
5272
- ...published !== undefined ? { published } : {}
5273
- });
5602
+ if (values.has(name)) {
5603
+ problems.push(`--${name} was given more than once`);
5604
+ continue;
5605
+ }
5606
+ if (eq !== -1) {
5607
+ values.set(name, arg.slice(eq + 1));
5608
+ continue;
5609
+ }
5610
+ const next = args[i + 1];
5611
+ if (next === undefined || next.startsWith("--")) {
5612
+ problems.push(`--${name} requires a value`);
5613
+ continue;
5614
+ }
5615
+ values.set(name, next);
5616
+ i += 1;
5274
5617
  }
5275
- return results;
5276
- }
5277
- function optionalText2(value) {
5278
- if (typeof value !== "string")
5279
- return;
5280
- const trimmed = value.trim();
5281
- return trimmed ? trimmed : undefined;
5618
+ return { values, booleans, problems };
5282
5619
  }
5283
-
5284
- // src/core/transport.ts
5285
- init_constants();
5286
- class DeepSeekApiError extends Error {
5287
- operation;
5288
- status;
5289
- body;
5290
- retryable;
5291
- retryAfterMs;
5292
- constructor(input) {
5293
- const body = redactSecrets(input.body);
5294
- super(formatApiError(input.operation, input.status, body));
5295
- this.name = "DeepSeekApiError";
5296
- this.operation = input.operation;
5297
- this.status = input.status;
5298
- this.body = body;
5299
- this.retryable = input.status === 408 || input.status === 425 || input.status === 429 || input.status >= 500 && input.status <= 599;
5300
- this.retryAfterMs = input.retryAfterMs;
5301
- }
5302
- }
5303
- async function postJson(endpoint, body, operation, opts) {
5304
- const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
5305
- const attempts = opts.retry.maxRetries + 1;
5306
- let lastError;
5307
- for (let attempt = 0;attempt < attempts; attempt++) {
5620
+ async function runInstallerCli(argv, io = {}) {
5621
+ const out = io.out ?? ((line) => console.log(line));
5622
+ const err = io.err ?? ((line) => console.error(line));
5623
+ const [command, ...rest] = argv;
5624
+ if (command === "version" || command === "--version") {
5625
+ out(TOOL_VERSION);
5626
+ return 0;
5627
+ }
5628
+ if (command === undefined) {
5629
+ err(USAGE);
5630
+ return EXIT_USAGE2;
5631
+ }
5632
+ if (command === "help" || command === "--help") {
5633
+ out(USAGE);
5634
+ return 0;
5635
+ }
5636
+ const flags = parseFlags(rest);
5637
+ if (flags.problems.length > 0) {
5638
+ for (const problem of flags.problems)
5639
+ err(`error: ${problem}`);
5640
+ err(`
5641
+ ${USAGE}`);
5642
+ return EXIT_USAGE2;
5643
+ }
5644
+ const cwd = resolve2(flags.values.get("cwd") ?? process.cwd());
5645
+ const home = process.env.HOME ?? homedir2();
5646
+ const platform = io.platform ?? process.platform;
5647
+ const roots = { home, cwd, platform };
5648
+ const configHome2 = resolveConfigHome(process.env, home, platform);
5649
+ if (command === "list") {
5650
+ let records;
5308
5651
  try {
5309
- return await requestOnce(endpoint, body, operation, opts, fetchImpl);
5652
+ records = listRecords({ ...roots, configHome: configHome2 });
5310
5653
  } catch (error) {
5311
- lastError = error;
5312
- if (!(error instanceof DeepSeekApiError) || !error.retryable || attempt === attempts - 1) {
5313
- throw error;
5314
- }
5315
- await sleep(retryDelay(error.retryAfterMs, opts.retry.baseDelayMs, attempt), opts.signal);
5654
+ err(error instanceof Error ? error.message : String(error));
5655
+ return 1;
5316
5656
  }
5317
- }
5318
- throw lastError;
5319
- }
5320
- async function requestOnce(endpoint, body, operation, opts, fetchImpl) {
5321
- return fetchWithTimeout(endpoint, {
5322
- method: "POST",
5323
- headers: {
5324
- Authorization: `Bearer ${opts.auth.apiKey}`,
5325
- "Content-Type": "application/json"
5326
- },
5327
- body: JSON.stringify(body)
5328
- }, opts.signal, opts.timeoutMs, operation, fetchImpl, async (response) => {
5329
- if (!response.ok) {
5330
- const text2 = await readBoundedText(response, MAX_ERROR_BODY_CHARS);
5331
- throw new DeepSeekApiError({
5332
- operation,
5333
- status: response.status,
5334
- body: text2,
5335
- retryAfterMs: parseRetryAfter(response.headers.get("retry-after"))
5336
- });
5657
+ if (records.length === 0) {
5658
+ out("No installs recorded.");
5659
+ return 0;
5337
5660
  }
5338
- const text = await readResponseText(response, MAX_RESPONSE_BYTES, operation);
5339
- if (!text.trim())
5340
- throw new Error(`${operation} returned an empty response body`);
5661
+ if (readingLegacyGlobalRecords({ ...roots, configHome: configHome2 })) {
5662
+ out(`Global install records are being read from the legacy ${legacyGlobalStateDir(roots)} ` + "(written before the Windows config home moved to %APPDATA%).");
5663
+ }
5664
+ for (const record of records) {
5665
+ const by = record.installerVersion ? ` by v${record.installerVersion}` : "";
5666
+ out(`${record.host} (${record.scope}) — search=${record.tiers.search}, fetch=${record.tiers.fetch}, installed ${record.createdAt}${by}`);
5667
+ for (const file of record.files)
5668
+ out(` ${file.created ? "created" : "modified"} ${file.file}`);
5669
+ }
5670
+ return 0;
5671
+ }
5672
+ if (command === "config") {
5673
+ return runConfigCommand(flags.values.get("search"), { out, err });
5674
+ }
5675
+ if (command === "uninstall") {
5676
+ const required2 = requireInput(flags, cwd, home, configHome2, platform, false);
5677
+ if (!required2.ok)
5678
+ return usageError(required2.problems, err);
5341
5679
  try {
5342
- return JSON.parse(text);
5343
- } catch {
5344
- throw new Error(`${operation} returned invalid JSON: ${truncate(redactSecrets(text), 500)}`);
5680
+ for (const line of uninstall(required2.input))
5681
+ out(line);
5682
+ return 0;
5683
+ } catch (error) {
5684
+ err(error instanceof Error ? error.message : String(error));
5685
+ return 1;
5345
5686
  }
5346
- });
5347
- }
5348
- async function fetchWithTimeout(endpoint, init, signal, timeoutMs, operation, fetchImpl, handleResponse) {
5349
- const controller = new AbortController;
5350
- let timedOut = false;
5351
- const forwardAbort = () => controller.abort(signal.reason);
5352
- if (signal.aborted)
5353
- forwardAbort();
5354
- else
5355
- signal.addEventListener("abort", forwardAbort, { once: true });
5356
- const timer = signal.aborted ? undefined : setTimeout(() => {
5357
- timedOut = true;
5358
- controller.abort(new Error("timeout"));
5359
- }, timeoutMs);
5360
- try {
5361
- const response = await fetchImpl(endpoint, { ...init, signal: controller.signal });
5362
- return await handleResponse(response);
5363
- } catch (error) {
5364
- if (timedOut)
5365
- throw new Error(formatTimeoutError(operation, timeoutMs));
5366
- if (signal.aborted)
5367
- throw abortReason(signal, operation);
5368
- throw error;
5369
- } finally {
5370
- if (timer !== undefined)
5371
- clearTimeout(timer);
5372
- signal.removeEventListener("abort", forwardAbort);
5373
5687
  }
5374
- }
5375
- function parseRetryAfter(value) {
5376
- if (!value)
5377
- return;
5378
- const trimmed = value.trim();
5379
- if (!trimmed)
5380
- return;
5381
- if (/^\d+$/.test(trimmed)) {
5382
- return Math.min(MAX_RETRY_DELAY_MS, Number(trimmed) * 1000);
5688
+ if (command !== "init") {
5689
+ err(`Unknown command "${command}".
5690
+
5691
+ ${USAGE}`);
5692
+ return EXIT_USAGE2;
5693
+ }
5694
+ const required = requireInput(flags, cwd, home, configHome2, platform, true);
5695
+ if (!required.ok)
5696
+ return usageError(required.problems, err);
5697
+ const { input } = required;
5698
+ const probe = HOST_VERSION_PROBES[input.host];
5699
+ const hostVersion = probe?.(err);
5700
+ const plan = planForHost(input.host, hostVersion === undefined ? input : { ...input, hostVersion });
5701
+ if (plan.errors.length > 0) {
5702
+ for (const error of plan.errors)
5703
+ err(`error: ${error.message}`);
5704
+ err("Nothing was written.");
5705
+ return 1;
5706
+ }
5707
+ out(renderPlan(plan));
5708
+ if (!flags.booleans.has("apply")) {
5709
+ out(`
5710
+ Plan only — re-run with --apply to write these changes.`);
5711
+ return 0;
5712
+ }
5713
+ try {
5714
+ const result = applyPlan(plan, input);
5715
+ for (const line of result.lines)
5716
+ out(line);
5717
+ return 0;
5718
+ } catch (error) {
5719
+ err(error instanceof Error ? error.message : String(error));
5720
+ return exitCodeForError(error);
5383
5721
  }
5384
- const timestamp = Date.parse(trimmed);
5385
- if (!Number.isFinite(timestamp))
5386
- return;
5387
- return Math.min(MAX_RETRY_DELAY_MS, Math.max(0, timestamp - Date.now()));
5388
5722
  }
5389
- function retryDelay(retryAfterMs, baseDelayMs, attempt) {
5390
- if (retryAfterMs !== undefined)
5391
- return retryAfterMs;
5392
- return Math.min(MAX_RETRY_DELAY_MS, baseDelayMs * 2 ** attempt);
5723
+ function exitCodeForError(error) {
5724
+ return error instanceof ApplyError && error.status === "recovery_required" ? EXIT_RECOVERY : EXIT_FAILURE;
5393
5725
  }
5394
- function sleep(ms, signal) {
5395
- if (ms <= 0)
5396
- return Promise.resolve();
5397
- return new Promise((resolve, reject) => {
5398
- const timer = setTimeout(() => {
5399
- signal.removeEventListener("abort", onAbort);
5400
- resolve();
5401
- }, ms);
5402
- const onAbort = () => {
5403
- clearTimeout(timer);
5404
- reject(abortReason(signal, "DeepSeek request"));
5405
- };
5406
- if (signal.aborted) {
5407
- clearTimeout(timer);
5408
- reject(abortReason(signal, "DeepSeek request"));
5409
- return;
5410
- }
5411
- signal.addEventListener("abort", onAbort, { once: true });
5412
- });
5726
+ function usageError(problems, err) {
5727
+ for (const problem of problems)
5728
+ err(`error: ${problem}`);
5729
+ err(`
5730
+ ${USAGE}`);
5731
+ return EXIT_USAGE2;
5413
5732
  }
5414
- function abortReason(signal, operation) {
5415
- return signal.reason instanceof Error ? signal.reason : new Error(`${operation} request aborted`);
5733
+ function resolveConfigHome(env, home, platform = process.platform) {
5734
+ const xdgTrusted = env.XDG_CONFIG_HOME !== undefined && env.XDG_CONFIG_HOME !== "" && home === (env.HOME ?? "");
5735
+ return userConfigHome({ env: xdgTrusted ? env : { ...env, XDG_CONFIG_HOME: undefined }, home, platform });
5416
5736
  }
5417
- async function readBoundedText(response, maxChars) {
5418
- if (!response.body)
5419
- return truncate(await response.text(), maxChars + 1);
5420
- const reader = response.body.getReader();
5421
- const decoder = new TextDecoder;
5422
- const limit = maxChars + 1;
5423
- let text = "";
5424
- let chars = 0;
5425
- try {
5426
- while (chars < limit) {
5427
- const { done, value } = await reader.read();
5428
- if (done)
5429
- break;
5430
- const chunk = decoder.decode(value, { stream: true });
5431
- text += chunk;
5432
- chars += Array.from(chunk).length;
5737
+ function requireInput(flags, cwd, home, configHome2, platform, needsTiers) {
5738
+ const problems = [];
5739
+ const host = flags.values.get("host");
5740
+ const scope = flags.values.get("scope");
5741
+ const search = needsTiers ? flags.values.get("search") ?? "" : "optional";
5742
+ const fetch = needsTiers ? flags.values.get("fetch") ?? "" : "optional";
5743
+ const path = flags.values.get("path");
5744
+ if (host === undefined)
5745
+ problems.push(`--host is required (one of: ${HOSTS.join(", ")})`);
5746
+ else if (!HOSTS.includes(host))
5747
+ problems.push(`invalid --host value "${host}" (one of: ${HOSTS.join(", ")})`);
5748
+ if (scope === undefined)
5749
+ problems.push(`--scope is required (one of: ${SCOPES.join(", ")})`);
5750
+ else if (!SCOPES.includes(scope))
5751
+ problems.push(`invalid --scope value "${scope}" (one of: ${SCOPES.join(", ")})`);
5752
+ if (path !== undefined) {
5753
+ if (host !== "pi") {
5754
+ problems.push(`--path is only supported for host pi (${host ?? "no host"} has a single integration path)`);
5755
+ } else if (!PATH_CHOICES.includes(path)) {
5756
+ problems.push(`invalid --path value "${path}" (one of: ${PATH_CHOICES.join(", ")})`);
5433
5757
  }
5434
- text += decoder.decode();
5435
- } finally {
5436
- await reader.cancel().catch(() => {
5437
- return;
5438
- });
5439
- reader.releaseLock();
5440
5758
  }
5441
- return truncate(text, limit);
5442
- }
5443
- async function readResponseText(response, maxBytes, operation) {
5444
- if (!response.body) {
5445
- const text2 = await response.text();
5446
- if (new TextEncoder().encode(text2).byteLength > maxBytes) {
5447
- throw new Error(`${operation} response exceeded ${maxBytes} bytes`);
5759
+ if (needsTiers) {
5760
+ if (!TIERS.includes(search)) {
5761
+ problems.push(flags.values.has("search") ? `invalid --search value "${search}" (one of: ${TIERS.join(", ")})` : `--search is required (one of: ${TIERS.join(", ")})`);
5448
5762
  }
5449
- return text2;
5450
- }
5451
- const reader = response.body.getReader();
5452
- const decoder = new TextDecoder;
5453
- let text = "";
5454
- let bytes = 0;
5455
- try {
5456
- while (true) {
5457
- const { done, value } = await reader.read();
5458
- if (done)
5459
- break;
5460
- bytes += value.byteLength;
5461
- if (bytes > maxBytes) {
5462
- throw new Error(`${operation} response exceeded ${maxBytes} bytes`);
5463
- }
5464
- text += decoder.decode(value, { stream: true });
5763
+ if (!TIERS.includes(fetch)) {
5764
+ problems.push(flags.values.has("fetch") ? `invalid --fetch value "${fetch}" (one of: ${TIERS.join(", ")})` : `--fetch is required (one of: ${TIERS.join(", ")})`);
5465
5765
  }
5466
- text += decoder.decode();
5467
- } finally {
5468
- await reader.cancel().catch(() => {
5469
- return;
5470
- });
5471
- reader.releaseLock();
5472
5766
  }
5473
- return text;
5767
+ if (home === "")
5768
+ problems.push("HOME is not set and the user home cannot be resolved; cannot proceed");
5769
+ if (problems.length > 0)
5770
+ return { ok: false, problems };
5771
+ const packageRef = flags.values.get("package") ?? "deepseek-web-retrieval";
5772
+ return {
5773
+ ok: true,
5774
+ input: {
5775
+ host,
5776
+ scope,
5777
+ search,
5778
+ fetch,
5779
+ packageRef: flags.booleans.has("pin") ? pinPackageRef(packageRef, TOOL_VERSION) : packageRef,
5780
+ home,
5781
+ cwd,
5782
+ configHome: configHome2,
5783
+ platform,
5784
+ ...path !== undefined ? { path } : {}
5785
+ }
5786
+ };
5474
5787
  }
5788
+ var HOSTS, HOST_CHOICES, SCOPES, TIERS, EXIT_FAILURE = 1, EXIT_USAGE2 = 2, EXIT_RECOVERY = 3, USAGE, VALUE_FLAGS, BOOLEAN_FLAGS, PATH_CHOICES;
5789
+ var init_cli = __esm(() => {
5790
+ init_config_file();
5791
+ init_version();
5792
+ init_apply();
5793
+ init_config_command();
5794
+ init_hosts();
5795
+ init_ownership();
5796
+ init_state();
5797
+ init_uninstall();
5798
+ HOSTS = Object.keys(HOST_PLANNERS);
5799
+ HOST_CHOICES = `<${HOSTS.join("|")}>`;
5800
+ SCOPES = ["project", "global"];
5801
+ TIERS = ["optional", "preferred", "forced", "off"];
5802
+ USAGE = `Usage:
5803
+ 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]
5804
+ deepseek-web-retrieval uninstall --host ${HOST_CHOICES} --scope <project|global> [--cwd <dir>]
5805
+ deepseek-web-retrieval list [--cwd <dir>]
5806
+ deepseek-web-retrieval config [--search <query>]
5807
+ deepseek-web-retrieval version
5475
5808
 
5476
- // src/core/client.ts
5477
- function effectiveTimeout(configuredMs, perCallMs) {
5478
- return perCallMs === undefined ? configuredMs : Math.min(configuredMs, perCallMs);
5479
- }
5480
- async function searchWeb(query, options, request) {
5481
- const trimmed = query.trim();
5482
- if (!trimmed)
5483
- throw new Error("DeepSeek search query must not be empty");
5484
- if (Array.from(trimmed).length > MAX_QUERY_CHARS) {
5485
- throw new Error(`DeepSeek search query must be ${MAX_QUERY_CHARS} characters or fewer`);
5486
- }
5487
- const builtQuery = buildQuery(trimmed, options);
5488
- const builtQueryLength = Array.from(builtQuery).length;
5489
- if (builtQueryLength > MAX_QUERY_CHARS) {
5490
- 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)`);
5491
- }
5492
- const timeout = effectiveTimeout(options.timeoutMs, request.timeoutMs);
5493
- const payload = await postJson(SEARCH_ENDPOINT, {
5494
- query: builtQuery,
5495
- count: options.count,
5496
- need_content: false
5497
- }, "DeepSeek Search", { ...request, timeoutMs: timeout });
5498
- if (!Array.isArray(payload.results)) {
5499
- throw new Error("DeepSeek Search returned a response without a results array");
5500
- }
5501
- return processResults(payload.results, options);
5809
+ init prints the full plan (every file, old value → new value) and writes nothing
5810
+ until --apply is passed. uninstall restores what the plan applied. --path picks
5811
+ the integration mechanism for hosts with more than one (Pi: extension by
5812
+ default, mcp for the stdio MCP server). --package overrides the package ref
5813
+ written into host config; the Pi extension path turns it into the packages
5814
+ source (a bare name is prefixed with npm:, a ref that already starts with npm:
5815
+ is written as-is). --pin appends the running CLI's version to the package ref
5816
+ (a ref already ending in a version keeps it), so hosts resolve that exact
5817
+ release instead of the latest publish; re-run init --pin to move the pin.
5818
+
5819
+ config prints the effective configuration — every knob with its value and its
5820
+ source (default, shared file, or environment), resolved the way the MCP server
5821
+ resolves it — and exits 1 when the config file or the DEEPSEEK_* environment
5822
+ has problems (unknown or rejected keys, unparseable JSON, set-but-unusable
5823
+ variable values; a missing file is fine). config --search <query>
5824
+ runs one billed search through that configuration to verify the key, config,
5825
+ and network end to end; it exits 1 when the key is missing or the search fails.
5826
+
5827
+ Exit codes: 0 success; 1 refused or failed with nothing written (a failed apply
5828
+ is fully rolled back); 2 invalid command line; 3 recovery required — a failed
5829
+ apply could not roll back completely, follow the reported guidance before
5830
+ re-running.`;
5831
+ VALUE_FLAGS = new Set(["host", "scope", "search", "fetch", "path", "package", "cwd"]);
5832
+ BOOLEAN_FLAGS = new Set(["apply", "pin"]);
5833
+ PATH_CHOICES = ["extension", "mcp"];
5834
+ });
5835
+
5836
+ // src/mcp/auth.ts
5837
+ function requireApiKey(env) {
5838
+ const apiKey = apiKeyFromEnv(env);
5839
+ if (!apiKey)
5840
+ throw new Error(MISSING_API_KEY_MESSAGE);
5841
+ return apiKey;
5502
5842
  }
5503
- async function fetchPage(url, options, request) {
5504
- const target = parseFetchUrl(url.href);
5505
- assertFetchHostPolicy(target, options);
5506
- const timeout = effectiveTimeout(options.timeoutMs, request.timeoutMs);
5507
- const payload = await postJson(FETCH_ENDPOINT, { url: target.href }, "DeepSeek Fetch", {
5508
- ...request,
5509
- timeoutMs: timeout
5510
- });
5511
- const title = typeof payload.title === "string" ? payload.title.trim() : "";
5512
- const content = typeof payload.content === "string" ? payload.content : "";
5513
- if (!content.trim()) {
5514
- throw new Error("DeepSeek Fetch returned no page content (the page may be empty or unavailable)");
5843
+ var init_auth2 = __esm(() => {
5844
+ init_auth();
5845
+ });
5846
+
5847
+ // src/mcp/client.ts
5848
+ import { CLIENT_INFO_META_KEY } from "@modelcontextprotocol/server";
5849
+ function clientNameOf(server, ctx) {
5850
+ const envelope = ctx.mcpReq.envelope;
5851
+ const name = envelope?.[CLIENT_INFO_META_KEY];
5852
+ if (typeof name === "object" && name !== null && "name" in name && typeof name.name === "string") {
5853
+ return name.name;
5515
5854
  }
5516
- return { url: target.href, title, content };
5855
+ return server.server.getClientVersion()?.name;
5517
5856
  }
5518
- async function fetchDocument(url, options, request) {
5519
- if (typeof url !== "string" && !(url instanceof URL))
5520
- throw new Error('"url" must be a string or URL');
5521
- const page = await fetchPage(url instanceof URL ? url : parseFetchUrl(url), options, request);
5522
- const formatted = formatFetchedContent(page.title, page.content, options.maxChars);
5523
- return {
5524
- url: page.url,
5525
- title: page.title,
5526
- content: formatted.content,
5527
- truncated: formatted.truncated
5528
- };
5857
+ function isJsonReplacingClient(name) {
5858
+ return name !== undefined && JSON_REPLACING_CLIENT_NAMES.has(name);
5529
5859
  }
5530
- function createRetrievalClient(dependencies = {}) {
5531
- const bind = (request) => ({ ...request, ...dependencies });
5532
- return {
5533
- searchWeb: (query, options, request) => searchWeb(query, options, bind(request)),
5534
- fetchPage: (url, options, request) => fetchPage(url, options, bind(request)),
5535
- fetchDocument: (url, options, request) => fetchDocument(url, options, bind(request))
5536
- };
5860
+ function omitStructuredContent(server, ctx) {
5861
+ return isJsonReplacingClient(clientNameOf(server, ctx));
5537
5862
  }
5538
- var retrievalClient = createRetrievalClient();
5863
+ var JSON_REPLACING_CLIENT_NAMES;
5864
+ var init_client2 = __esm(() => {
5865
+ JSON_REPLACING_CLIENT_NAMES = new Set(["codex-mcp-client"]);
5866
+ });
5539
5867
 
5540
- // src/mcp/server.ts
5541
- init_constants();
5868
+ // package.json
5869
+ var package_default;
5870
+ var init_package = __esm(() => {
5871
+ package_default = {
5872
+ $schema: "https://json.schemastore.org/package.json",
5873
+ name: "deepseek-web-retrieval",
5874
+ version: "0.7.0",
5875
+ description: "Token-efficient DeepSeek Search and Fetch for coding agents, starting with OpenCode v2.",
5876
+ type: "module",
5877
+ license: "MIT",
5878
+ repository: {
5879
+ type: "git",
5880
+ url: "git+https://github.com/gwokhou/deepseek-web-retrieval.git"
5881
+ },
5882
+ homepage: "https://github.com/gwokhou/deepseek-web-retrieval#readme",
5883
+ bugs: "https://github.com/gwokhou/deepseek-web-retrieval/issues",
5884
+ keywords: [
5885
+ "opencode",
5886
+ "opencode-plugin",
5887
+ "pi",
5888
+ "pi-extension",
5889
+ "deepseek",
5890
+ "websearch",
5891
+ "webfetch",
5892
+ "coding-agent",
5893
+ "mcp"
5894
+ ],
5895
+ exports: {
5896
+ ".": "./index.ts",
5897
+ "./core": "./src/core/index.ts",
5898
+ "./mcp": "./src/mcp/server.ts",
5899
+ "./pi": "./src/pi/extension.ts"
5900
+ },
5901
+ pi: {
5902
+ extensions: [
5903
+ "./src/pi/extension.ts"
5904
+ ]
5905
+ },
5906
+ bin: {
5907
+ "deepseek-web-retrieval": "dist/cli.js"
5908
+ },
5909
+ files: [
5910
+ "index.ts",
5911
+ "src",
5912
+ "dist",
5913
+ "docs",
5914
+ "README.md",
5915
+ "LICENSE",
5916
+ "CHANGELOG.md",
5917
+ "examples"
5918
+ ],
5919
+ scripts: {
5920
+ lint: "biome check --error-on-warnings .",
5921
+ clean: "bun run scripts/clean.ts",
5922
+ "lint:ci": "biome ci --error-on-warnings .",
5923
+ "lint:fix": "biome check --write --error-on-warnings .",
5924
+ format: "biome format --write .",
5925
+ typecheck: "tsc --noEmit",
5926
+ 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",
5927
+ prepublishOnly: "bun run check:release",
5928
+ test: "bun test",
5929
+ "test:package": "node scripts/test-package.mjs",
5930
+ "test:e2e:keyless": "bun run test:e2e pi pi-extension installer reinit upgrade pkg capability-off",
5931
+ "check:release": "bun run check && bun run test:package && bun run test:e2e:keyless",
5932
+ "test:e2e": "bun run test/e2e/run.ts",
5933
+ "docs:matrix": "bun run scripts/render-matrix.ts",
5934
+ "test:coverage": "bun test --coverage",
5935
+ check: "bun run lint && bun run typecheck && bun run docs:matrix --check && bun run build && bun test"
5936
+ },
5937
+ peerDependencies: {
5938
+ "@earendil-works/pi-ai": "^1.3.0",
5939
+ "@earendil-works/pi-coding-agent": "^1.3.0",
5940
+ "@opencode/plugin": "^2.0.32"
5941
+ },
5942
+ peerDependenciesMeta: {
5943
+ "@earendil-works/pi-ai": {
5944
+ optional: true
5945
+ },
5946
+ "@earendil-works/pi-coding-agent": {
5947
+ optional: true
5948
+ },
5949
+ "@opencode/plugin": {
5950
+ optional: true
5951
+ }
5952
+ },
5953
+ devDependencies: {
5954
+ "@biomejs/biome": "2.5.15",
5955
+ "@earendil-works/pi-ai": "1.3.0",
5956
+ "@earendil-works/pi-coding-agent": "1.3.0",
5957
+ "@modelcontextprotocol/client": "^2.3.1",
5958
+ "@opencode/plugin": "2.0.32",
5959
+ "@types/bun": "1.4.0",
5960
+ "@types/node": "^26.6.5",
5961
+ typescript: "5.9.3"
5962
+ },
5963
+ engines: {
5964
+ bun: ">=1.3.0",
5965
+ node: ">=20"
5966
+ },
5967
+ dependencies: {
5968
+ "@modelcontextprotocol/server": "^2.3.1",
5969
+ "jsonc-parser": "^3.3.1",
5970
+ "smol-toml": "^1.9.1",
5971
+ zod: "^4"
5972
+ }
5973
+ };
5974
+ });
5542
5975
 
5543
5976
  // src/core/descriptions.ts
5544
- var FETCH_SERVICE_LIMITS = "DeepSeek does not render JavaScript and cannot reach private networks; PDFs, images, and other binary files are unsupported.";
5545
- var FETCH_OUTPUT_TRUNCATED = "Output is truncated to the configured size.";
5546
- var SEARCH_RANKED_FILTERED = "Results are ranked by relevance and source authority, deduplicated by URL, and capped at the configured maximum.";
5547
- var SEARCH_EMPTY_IS_NORMAL = "An empty result list is normal, not an error.";
5548
- var SEARCH_QUERY_OPERATORS = "Use site:/-site: operators in the query to include or exclude domains; the full query must stay within 128 characters.";
5549
- var 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.";
5550
- var FETCH_TOOL_DESCRIPTION = "Fetch one web page as clean Markdown through DeepSeek's Fetch API. " + `${FETCH_SERVICE_LIMITS} ` + `${FETCH_OUTPUT_TRUNCATED}`;
5551
- var SEARCH_QUERY_PARAMETER = "Search query, 128 characters or fewer including operators (site:, -site:, quoted phrases, lang:).";
5552
- var FETCH_URL_PARAMETER = "Absolute http:// or https:// URL to fetch. Credentials are rejected.";
5977
+ 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.";
5978
+ var init_descriptions = __esm(() => {
5979
+ 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.";
5980
+ FETCH_TOOL_DESCRIPTION = "Fetch one web page as clean Markdown through DeepSeek's Fetch API. " + `${FETCH_SERVICE_LIMITS} ` + `${FETCH_OUTPUT_TRUNCATED}`;
5981
+ 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.";
5982
+ });
5553
5983
 
5554
5984
  // src/mcp/server.ts
5555
- var MCP_SERVER_NAME = "deepseek-web-retrieval";
5556
- var SEARCH_INPUT = z.object({
5557
- query: z.string().min(1).describe(SEARCH_QUERY_PARAMETER)
5558
- });
5559
- var FETCH_INPUT = z.object({
5560
- url: z.string().min(1).describe(FETCH_URL_PARAMETER)
5561
- });
5985
+ import { McpServer } from "@modelcontextprotocol/server";
5986
+ import * as z2 from "zod/v4";
5562
5987
  function createDeepSeekServer(config, env = process.env, client = retrievalClient) {
5563
5988
  const server = new McpServer({ name: MCP_SERVER_NAME, version: package_default.version });
5564
5989
  if (config.search.enabled) {
@@ -5567,10 +5992,13 @@ function createDeepSeekServer(config, env = process.env, client = retrievalClien
5567
5992
  description: SEARCH_TOOL_DESCRIPTION,
5568
5993
  inputSchema: SEARCH_INPUT,
5569
5994
  annotations: { readOnlyHint: true }
5570
- }, async ({ query }, ctx) => {
5995
+ }, async ({ query, original_question }, ctx) => {
5571
5996
  try {
5572
5997
  const request = requestContext(config, env, ctx.mcpReq.signal);
5573
- const results = await client.searchWeb(query, config.search, request);
5998
+ const results = await client.searchWeb(query, config.search, {
5999
+ ...request,
6000
+ oriQuestion: original_question
6001
+ });
5574
6002
  return callResult(server, ctx, formatSearchResults(results), {
5575
6003
  results: results.map(toStructuredResult)
5576
6004
  });
@@ -5619,38 +6047,77 @@ function toErrorResult(error) {
5619
6047
  const message = error instanceof Error ? error.message : String(error);
5620
6048
  return { content: [{ type: "text", text: message }], isError: true };
5621
6049
  }
6050
+ var MCP_SERVER_NAME = "deepseek-web-retrieval", SEARCH_INPUT, FETCH_INPUT;
6051
+ var init_server = __esm(() => {
6052
+ init_package();
6053
+ init_client();
6054
+ init_constants();
6055
+ init_descriptions();
6056
+ init_format2();
6057
+ init_auth2();
6058
+ init_client2();
6059
+ SEARCH_INPUT = z2.object({
6060
+ query: z2.string().min(1).describe(SEARCH_QUERY_PARAMETER),
6061
+ original_question: z2.string().optional().describe(SEARCH_ORI_QUESTION_PARAMETER)
6062
+ });
6063
+ FETCH_INPUT = z2.object({
6064
+ url: z2.string().min(1).describe(FETCH_URL_PARAMETER)
6065
+ });
6066
+ });
6067
+
6068
+ // src/mcp/serve.ts
6069
+ var exports_serve = {};
6070
+ __export(exports_serve, {
6071
+ serveMcp: () => serveMcp
6072
+ });
6073
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
6074
+ function serveMcp() {
6075
+ const env = process.env;
6076
+ if (!apiKeyFromEnv(env)) {
6077
+ console.error(MISSING_API_KEY_MESSAGE);
6078
+ process.exit(1);
6079
+ }
6080
+ const config = resolveEnvOptions(env, sharedFileOptions({ env }));
6081
+ const precheck = servePrecheck(config);
6082
+ if (precheck) {
6083
+ console.error(precheck);
6084
+ process.exit(1);
6085
+ }
6086
+ const handle = serveStdio(() => {
6087
+ const server = createDeepSeekServer(config, env);
6088
+ server.server.oninitialized = () => {
6089
+ const info = server.server.getClientVersion();
6090
+ if (info === undefined)
6091
+ return;
6092
+ console.error(`mcp client: ${info.name} ${info.version ?? "unknown"} — ` + (isJsonReplacingClient(info.name) ? "text-only results (this client replaces content with serialized structuredContent)" : "text + structuredContent results"));
6093
+ };
6094
+ return server;
6095
+ });
6096
+ const tools = [
6097
+ config.search.enabled ? `search (max ${config.search.maxResults} results)` : null,
6098
+ config.fetch.enabled ? `fetch (max ${config.fetch.maxChars} chars)` : null
6099
+ ].filter(Boolean).join(", ");
6100
+ console.error(`deepseek-web-retrieval MCP server on stdio (${tools})`);
6101
+ const deviations = configDeviations(config);
6102
+ if (deviations)
6103
+ console.error(`config: ${deviations}`);
6104
+ process.on("SIGINT", () => void handle.close());
6105
+ process.on("SIGTERM", () => void handle.close());
6106
+ }
6107
+ var init_serve = __esm(() => {
6108
+ init_auth();
6109
+ init_config_file();
6110
+ init_config_summary();
6111
+ init_env();
6112
+ init_auth2();
6113
+ init_client2();
6114
+ init_server();
6115
+ });
5622
6116
 
5623
6117
  // src/mcp/cli.ts
5624
- var subcommand = process.argv[2];
5625
- if (subcommand === "init" || subcommand === "uninstall" || subcommand === "list") {
6118
+ if (process.argv.length > 2) {
5626
6119
  const { runInstallerCli: runInstallerCli2 } = await Promise.resolve().then(() => (init_cli(), exports_cli));
5627
6120
  process.exit(await runInstallerCli2(process.argv.slice(2)));
5628
6121
  }
5629
- var env = process.env;
5630
- if (!apiKeyFromEnv(env)) {
5631
- console.error(MISSING_API_KEY_MESSAGE);
5632
- process.exit(1);
5633
- }
5634
- var config = resolveEnvOptions(env);
5635
- var precheck = servePrecheck(config);
5636
- if (precheck) {
5637
- console.error(precheck);
5638
- process.exit(1);
5639
- }
5640
- var handle = serveStdio(() => {
5641
- const server = createDeepSeekServer(config, env);
5642
- server.server.oninitialized = () => {
5643
- const info = server.server.getClientVersion();
5644
- if (info === undefined)
5645
- return;
5646
- console.error(`mcp client: ${info.name} ${info.version ?? "unknown"} — ` + (isJsonReplacingClient(info.name) ? "text-only results (this client replaces content with serialized structuredContent)" : "text + structuredContent results"));
5647
- };
5648
- return server;
5649
- });
5650
- var tools = [
5651
- config.search.enabled ? `search (max ${config.search.maxResults} results)` : null,
5652
- config.fetch.enabled ? `fetch (max ${config.fetch.maxChars} chars)` : null
5653
- ].filter(Boolean).join(", ");
5654
- console.error(`deepseek-web-retrieval MCP server on stdio (${tools})`);
5655
- process.on("SIGINT", () => void handle.close());
5656
- process.on("SIGTERM", () => void handle.close());
6122
+ var { serveMcp: serveMcp2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
6123
+ serveMcp2();