deepseek-web-retrieval 0.8.0 → 0.9.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 (40) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +13 -5
  3. package/dist/cli.js +281 -167
  4. package/docs/agents/claude-code.md +7 -1
  5. package/docs/agents/codex.md +7 -1
  6. package/docs/agents/opencode-v2.md +11 -3
  7. package/docs/agents/pi.md +6 -1
  8. package/docs/configuration.md +7 -0
  9. package/docs/design/installer-design.md +4 -4
  10. package/docs/research/ori-question-sourcing.md +12 -1
  11. package/package.json +3 -2
  12. package/src/core/auth.ts +7 -0
  13. package/src/core/client.ts +89 -25
  14. package/src/core/config-summary.ts +1 -0
  15. package/src/core/constants.ts +17 -2
  16. package/src/core/descriptions.ts +10 -1
  17. package/src/core/env.ts +3 -1
  18. package/src/core/format.ts +25 -4
  19. package/src/core/options.ts +47 -0
  20. package/src/core/search.ts +97 -14
  21. package/src/core/types.ts +17 -0
  22. package/src/installer/apply.ts +4 -4
  23. package/src/installer/cli.ts +2 -2
  24. package/src/installer/config-command.ts +4 -3
  25. package/src/installer/hosts/claude-code.ts +3 -17
  26. package/src/installer/hosts/codex.ts +3 -17
  27. package/src/installer/hosts/index.ts +2 -2
  28. package/src/installer/hosts/opencode.ts +1 -1
  29. package/src/installer/hosts/pi.ts +14 -15
  30. package/src/installer/hosts/shared.ts +23 -0
  31. package/src/installer/ownership.ts +2 -2
  32. package/src/installer/paths.ts +2 -2
  33. package/src/installer/{state.ts → record.ts} +15 -15
  34. package/src/installer/types.ts +1 -1
  35. package/src/installer/uninstall.ts +4 -4
  36. package/src/mcp/serve.ts +1 -2
  37. package/src/mcp/server.ts +16 -19
  38. package/src/opencode/plugin.ts +15 -10
  39. package/src/pi/extension.ts +23 -17
  40. package/src/mcp/auth.ts +0 -11
package/dist/cli.js CHANGED
@@ -16,10 +16,12 @@ 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_ORI_QUESTION_CHARS = 128, MAX_URL_CHARS = 8192, MAX_ERROR_BODY_CHARS = 2000, MAX_RESPONSE_BYTES, FETCH_TRUNCATION_MARKER = `
19
+ var DEFAULT_API_BASE_URL = "https://api.deepseek.com", SEARCH_ENDPOINT_PATH = "/v1/tools/search", FETCH_ENDPOINT_PATH = "/v1/tools/fetch", SEARCH_ENDPOINT, FETCH_ENDPOINT, 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, MAX_FETCH_CHARS = 1e6, MAX_SEARCH_COUNT = 50, 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 = 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";
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", BASE_URL_ENV = "DEEPSEEK_BASE_URL", 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
+ SEARCH_ENDPOINT = `${DEFAULT_API_BASE_URL}${SEARCH_ENDPOINT_PATH}`;
24
+ FETCH_ENDPOINT = `${DEFAULT_API_BASE_URL}${FETCH_ENDPOINT_PATH}`;
23
25
  MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
24
26
  });
25
27
 
@@ -138,9 +140,15 @@ function resolvePluginOptions(input) {
138
140
  search: resolveSearchOptions(input),
139
141
  fetch: resolveFetchOptions(input),
140
142
  auth: resolveAuthOptions(input),
141
- retry: resolveRetryOptions(input)
143
+ retry: resolveRetryOptions(input),
144
+ baseUrl: resolveBaseUrl(input?.baseUrl)
142
145
  };
143
146
  }
147
+ function bothDisabledPrecheck(config, reason) {
148
+ if (config.search.enabled || config.fetch.enabled)
149
+ return;
150
+ return `Both tools are disabled (${reason}); at least one must stay enabled.`;
151
+ }
144
152
  function resolveSearchOptions(input) {
145
153
  const count = clampInt(input?.count, DEFAULT_SEARCH_COUNT, 1, 50);
146
154
  return {
@@ -180,6 +188,28 @@ function resolveRetryOptions(input) {
180
188
  baseDelayMs: clampInt(input?.retryBaseDelayMs, DEFAULT_RETRY_BASE_DELAY_MS, 0, MAX_RETRY_DELAY_MS)
181
189
  };
182
190
  }
191
+ function resolveBaseUrl(value) {
192
+ if (typeof value !== "string")
193
+ return DEFAULT_API_BASE_URL;
194
+ const trimmed = value.trim();
195
+ if (!trimmed)
196
+ return DEFAULT_API_BASE_URL;
197
+ let parsed;
198
+ try {
199
+ parsed = new URL(trimmed);
200
+ } catch {
201
+ return DEFAULT_API_BASE_URL;
202
+ }
203
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
204
+ return DEFAULT_API_BASE_URL;
205
+ return trimmed.replace(/\/+$/, "");
206
+ }
207
+ function withPerCallCount(options, count) {
208
+ if (count === undefined || !Number.isFinite(count))
209
+ return options;
210
+ const clamped = Math.min(MAX_SEARCH_COUNT, Math.max(1, Math.trunc(count)));
211
+ return { ...options, count: clamped, maxResults: clamped };
212
+ }
183
213
  function optionalText(value) {
184
214
  if (typeof value !== "string")
185
215
  return;
@@ -256,7 +286,8 @@ var init_config_summary = __esm(() => {
256
286
  { key: "fetchTimeoutMs", read: (o) => o.fetch.timeoutMs },
257
287
  { key: "fetchBlockPrivateHosts", read: (o) => o.fetch.blockPrivateHosts },
258
288
  { key: "maxRetries", read: (o) => o.retry.maxRetries },
259
- { key: "retryBaseDelayMs", read: (o) => o.retry.baseDelayMs }
289
+ { key: "retryBaseDelayMs", read: (o) => o.retry.baseDelayMs },
290
+ { key: "baseUrl", read: (o) => o.baseUrl }
260
291
  ];
261
292
  CONFIG_KEYS = VOCABULARY.map((entry) => entry.key);
262
293
  });
@@ -3415,55 +3446,6 @@ var init_formats = __esm(() => {
3415
3446
  init_toml();
3416
3447
  });
3417
3448
 
3418
- // src/installer/revert.ts
3419
- import { existsSync as existsSync4, readFileSync as readFileSync7, rmdirSync, unlinkSync } from "node:fs";
3420
- import { dirname as dirname2 } from "node:path";
3421
- function pruneEmptyDir(dir) {
3422
- try {
3423
- rmdirSync(dir);
3424
- } catch {}
3425
- }
3426
- function revertRecordedFile(file, host, opts) {
3427
- const lines = [];
3428
- if (!existsSync4(file.file)) {
3429
- lines.push(`already absent: ${file.file}${opts.label}`);
3430
- return lines;
3431
- }
3432
- const codec = codecFor(file.format, host);
3433
- const text = readFileSync7(file.file, "utf8");
3434
- if (file.format === "rules") {
3435
- const edit2 = { file: file.file, format: "rules", block: file.blockApplied ?? "" };
3436
- if (!codec.verify(text, edit2)) {
3437
- lines.push(`no rules block${opts.label} in ${file.file}; left untouched`);
3438
- return lines;
3439
- }
3440
- const updated = codec.revert(text, file).text;
3441
- if (opts.whole && codec.isEffectivelyEmpty(updated)) {
3442
- unlinkSync(file.file);
3443
- pruneEmptyDir(dirname2(file.file));
3444
- lines.push(`deleted ${file.file} (installer-created, now empty)`);
3445
- } else {
3446
- writeConfig(file.file, updated);
3447
- lines.push(`removed rules block${opts.label} from ${file.file}`);
3448
- }
3449
- return lines;
3450
- }
3451
- const reverted = codec.revert(text, file);
3452
- if (opts.whole && file.created && codec.isEffectivelyEmpty(reverted.text)) {
3453
- unlinkSync(file.file);
3454
- pruneEmptyDir(dirname2(file.file));
3455
- lines.push(`deleted ${file.file} (installer-created, now empty)`);
3456
- } else {
3457
- writeConfig(file.file, reverted.text);
3458
- lines.push(`reverted artifacts${opts.label} in ${file.file}`);
3459
- }
3460
- lines.push(...reverted.notes.map((note) => ` note: ${note}`));
3461
- return lines;
3462
- }
3463
- var init_revert = __esm(() => {
3464
- init_formats();
3465
- });
3466
-
3467
3449
  // src/installer/paths.ts
3468
3450
  import { join as pathJoin } from "node:path";
3469
3451
  function join2(...parts) {
@@ -3473,35 +3455,35 @@ function join2(...parts) {
3473
3455
  function configHome(roots) {
3474
3456
  return roots.configHome ?? join2(roots.home, ".config");
3475
3457
  }
3476
- function stateDir(roots, scope) {
3458
+ function recordDir(roots, scope) {
3477
3459
  return scope === "global" ? join2(configHome(roots), "deepseek-web-retrieval") : join2(roots.cwd, ".deepseek-web-retrieval");
3478
3460
  }
3479
3461
  var init_paths = () => {};
3480
3462
 
3481
- // src/installer/state.ts
3482
- import { existsSync as existsSync5, lstatSync as lstatSync2, readdirSync, readFileSync as readFileSync8, rmSync as rmSync2 } from "node:fs";
3463
+ // src/installer/record.ts
3464
+ import { existsSync as existsSync4, lstatSync as lstatSync2, readdirSync, readFileSync as readFileSync7, rmSync as rmSync2 } from "node:fs";
3483
3465
  import { isAbsolute, join as join3 } from "node:path";
3484
3466
  import { z } from "zod/v4";
3485
- function statePathFor(input) {
3486
- return join3(stateDir(input, input.scope), `install.${input.host}.json`);
3467
+ function recordPathFor(input) {
3468
+ return join3(recordDir(input, input.scope), `install.${input.host}.json`);
3487
3469
  }
3488
3470
  function platformOf(roots) {
3489
3471
  return roots.platform ?? process.platform;
3490
3472
  }
3491
- function legacyGlobalStateDir(roots) {
3473
+ function legacyGlobalRecordDir(roots) {
3492
3474
  return join3(roots.home, ".config", "deepseek-web-retrieval");
3493
3475
  }
3494
3476
  function recordReadPath(input, platform = platformOf(input)) {
3495
- const canonical = statePathFor(input);
3496
- if (platform !== "win32" || input.scope !== "global" || existsSync5(canonical))
3477
+ const canonical = recordPathFor(input);
3478
+ if (platform !== "win32" || input.scope !== "global" || existsSync4(canonical))
3497
3479
  return canonical;
3498
- const legacy = join3(legacyGlobalStateDir(input), `install.${input.host}.json`);
3499
- return existsSync5(legacy) ? legacy : canonical;
3480
+ const legacy = join3(legacyGlobalRecordDir(input), `install.${input.host}.json`);
3481
+ return existsSync4(legacy) ? legacy : canonical;
3500
3482
  }
3501
3483
  function readRecordFile(file, host, scope) {
3502
3484
  let text;
3503
3485
  try {
3504
- text = readFileSync8(file, "utf8");
3486
+ text = readFileSync7(file, "utf8");
3505
3487
  } catch (error) {
3506
3488
  if (error.code === "ENOENT") {
3507
3489
  try {
@@ -3539,7 +3521,7 @@ function claimedByOtherScope(input, files) {
3539
3521
  return files.filter((file) => record.files.some((candidate) => candidate.file === file));
3540
3522
  }
3541
3523
  function writeRecord(input, record) {
3542
- const file = statePathFor(input);
3524
+ const file = recordPathFor(input);
3543
3525
  atomicWrite(file, `${JSON.stringify(record, null, 2)}
3544
3526
  `, 384);
3545
3527
  }
@@ -3559,7 +3541,7 @@ function appendedByPrevious(record, path) {
3559
3541
  }
3560
3542
  function deleteRecord(input) {
3561
3543
  const file = recordReadPath(input);
3562
- if (existsSync5(file))
3544
+ if (existsSync4(file))
3563
3545
  rmSync2(file);
3564
3546
  }
3565
3547
  function readRecordsFromDir(dir, scope) {
@@ -3591,18 +3573,18 @@ function hasRecordFiles(dir) {
3591
3573
  function readingLegacyGlobalRecords(roots) {
3592
3574
  if (platformOf(roots) !== "win32")
3593
3575
  return false;
3594
- if (hasRecordFiles(stateDir(roots, "global")))
3576
+ if (hasRecordFiles(recordDir(roots, "global")))
3595
3577
  return false;
3596
- return hasRecordFiles(legacyGlobalStateDir(roots));
3578
+ return hasRecordFiles(legacyGlobalRecordDir(roots));
3597
3579
  }
3598
3580
  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") : [];
3581
+ const global = readRecordsFromDir(recordDir(roots, "global"), "global");
3582
+ const project = readRecordsFromDir(recordDir(roots, "project"), "project");
3583
+ const legacy = readingLegacyGlobalRecords(roots) ? readRecordsFromDir(legacyGlobalRecordDir(roots), "global") : [];
3602
3584
  return [...global, ...legacy, ...project];
3603
3585
  }
3604
3586
  var keyPath, tier, operation, recordSchema, InstallRecordError;
3605
- var init_state = __esm(() => {
3587
+ var init_record = __esm(() => {
3606
3588
  init_paths();
3607
3589
  init_persistence();
3608
3590
  keyPath = z.array(z.string().min(1)).min(1);
@@ -3637,7 +3619,7 @@ var init_state = __esm(() => {
3637
3619
  file;
3638
3620
  kind;
3639
3621
  constructor(file, kind, detail) {
3640
- super(`${kind === "invalid" ? "Invalid install record" : "Cannot read install record"} at ${file} (${detail}). ` + "Restore a valid record from backup or repair its permissions before retrying; configuration was not changed. Do not delete undo state to bypass this check.");
3622
+ super(`${kind === "invalid" ? "Invalid install record" : "Cannot read install record"} at ${file} (${detail}). ` + "Restore a valid install record from your own copy or repair its permissions before retrying; configuration was not changed. Do not delete the install record to bypass this check.");
3641
3623
  this.file = file;
3642
3624
  this.kind = kind;
3643
3625
  this.name = "InstallRecordError";
@@ -3645,6 +3627,55 @@ var init_state = __esm(() => {
3645
3627
  };
3646
3628
  });
3647
3629
 
3630
+ // src/installer/revert.ts
3631
+ import { existsSync as existsSync5, readFileSync as readFileSync8, rmdirSync, unlinkSync } from "node:fs";
3632
+ import { dirname as dirname2 } from "node:path";
3633
+ function pruneEmptyDir(dir) {
3634
+ try {
3635
+ rmdirSync(dir);
3636
+ } catch {}
3637
+ }
3638
+ function revertRecordedFile(file, host, opts) {
3639
+ const lines = [];
3640
+ if (!existsSync5(file.file)) {
3641
+ lines.push(`already absent: ${file.file}${opts.label}`);
3642
+ return lines;
3643
+ }
3644
+ const codec = codecFor(file.format, host);
3645
+ const text = readFileSync8(file.file, "utf8");
3646
+ if (file.format === "rules") {
3647
+ const edit2 = { file: file.file, format: "rules", block: file.blockApplied ?? "" };
3648
+ if (!codec.verify(text, edit2)) {
3649
+ lines.push(`no rules block${opts.label} in ${file.file}; left untouched`);
3650
+ return lines;
3651
+ }
3652
+ const updated = codec.revert(text, file).text;
3653
+ if (opts.whole && codec.isEffectivelyEmpty(updated)) {
3654
+ unlinkSync(file.file);
3655
+ pruneEmptyDir(dirname2(file.file));
3656
+ lines.push(`deleted ${file.file} (installer-created, now empty)`);
3657
+ } else {
3658
+ writeConfig(file.file, updated);
3659
+ lines.push(`removed rules block${opts.label} from ${file.file}`);
3660
+ }
3661
+ return lines;
3662
+ }
3663
+ const reverted = codec.revert(text, file);
3664
+ if (opts.whole && file.created && codec.isEffectivelyEmpty(reverted.text)) {
3665
+ unlinkSync(file.file);
3666
+ pruneEmptyDir(dirname2(file.file));
3667
+ lines.push(`deleted ${file.file} (installer-created, now empty)`);
3668
+ } else {
3669
+ writeConfig(file.file, reverted.text);
3670
+ lines.push(`reverted artifacts${opts.label} in ${file.file}`);
3671
+ }
3672
+ lines.push(...reverted.notes.map((note) => ` note: ${note}`));
3673
+ return lines;
3674
+ }
3675
+ var init_revert = __esm(() => {
3676
+ init_formats();
3677
+ });
3678
+
3648
3679
  // src/installer/ownership.ts
3649
3680
  import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync9, realpathSync as realpathSync2, rmSync as rmSync3, writeFileSync as writeFileSync2 } from "node:fs";
3650
3681
  import { basename as basename2, dirname as dirname3, join as join4, resolve } from "node:path";
@@ -3672,7 +3703,7 @@ function canonicalPath(file) {
3672
3703
  return join4(canonicalPath(dirname3(absolute)), basename2(absolute));
3673
3704
  }
3674
3705
  function ownerOf(input) {
3675
- return { recordPath: canonicalPath(statePathFor(input)), scope: input.scope, cwd: canonicalPath(input.cwd) };
3706
+ return { recordPath: canonicalPath(recordPathFor(input)), scope: input.scope, cwd: canonicalPath(input.cwd) };
3676
3707
  }
3677
3708
  function claimPaths(input, files) {
3678
3709
  return [
@@ -3748,8 +3779,8 @@ function releaseFileOwnership(input, files) {
3748
3779
  releaseClaim(input, claim);
3749
3780
  }
3750
3781
  var init_ownership = __esm(() => {
3782
+ init_record();
3751
3783
  init_revert();
3752
- init_state();
3753
3784
  });
3754
3785
 
3755
3786
  // src/installer/operation-lock.ts
@@ -3808,7 +3839,7 @@ function applyPlan(plan, input) {
3808
3839
  if (plan.errors.length > 0) {
3809
3840
  throw new Error(`refusing to apply a plan with errors: ${plan.errors.map((error) => error.message).join("; ")}`);
3810
3841
  }
3811
- return withOperationLock([statePathFor(input)], () => withOperationLock([...plan.edits, ...plan.reverts ?? []].map((file) => file.file), () => applyLocked(plan, input)));
3842
+ return withOperationLock([recordPathFor(input)], () => withOperationLock([...plan.edits, ...plan.reverts ?? []].map((file) => file.file), () => applyLocked(plan, input)));
3812
3843
  }
3813
3844
  function applyLocked(plan, input) {
3814
3845
  const touched = [...plan.edits, ...plan.reverts ?? []];
@@ -3934,7 +3965,7 @@ function rollbackApply(input, previous, snapshots) {
3934
3965
  writeRecord(input, previous);
3935
3966
  else
3936
3967
  deleteRecord(input);
3937
- if (previous === undefined ? existsSync7(statePathFor(input)) : !deepEqual(readRecord(input), previous)) {
3968
+ if (previous === undefined ? existsSync7(recordPathFor(input)) : !deepEqual(readRecord(input), previous)) {
3938
3969
  throw new Error("restored install record differs from its previous state");
3939
3970
  }
3940
3971
  } catch (restoreError) {
@@ -4033,8 +4064,8 @@ var init_apply = __esm(() => {
4033
4064
  init_value();
4034
4065
  init_operation_lock();
4035
4066
  init_ownership();
4067
+ init_record();
4036
4068
  init_revert();
4037
- init_state();
4038
4069
  ApplyError = class ApplyError extends Error {
4039
4070
  status;
4040
4071
  constructor(status, message, cause) {
@@ -4050,6 +4081,12 @@ function apiKeyFromEnv(env = process.env) {
4050
4081
  const key = env[API_KEY_ENV]?.trim();
4051
4082
  return key ? key : undefined;
4052
4083
  }
4084
+ function requireApiKey(env) {
4085
+ const apiKey = apiKeyFromEnv(env);
4086
+ if (!apiKey)
4087
+ throw new Error(MISSING_API_KEY_MESSAGE);
4088
+ return apiKey;
4089
+ }
4053
4090
  var MISSING_API_KEY_MESSAGE;
4054
4091
  var init_auth = __esm(() => {
4055
4092
  init_constants();
@@ -4113,10 +4150,22 @@ function formatApiError(operation2, status, body) {
4113
4150
  function formatTimeoutError(operation2, timeoutMs) {
4114
4151
  return `${operation2} timed out after ${timeoutMs} ms`;
4115
4152
  }
4116
- function formatSearchResults(results) {
4117
- if (results.length === 0)
4118
- return "No results found.";
4119
- return results.map((result, index) => {
4153
+ function searchHeader(context) {
4154
+ if (!context)
4155
+ return;
4156
+ if (context.effectiveQuery === context.inputQuery && context.notes.length === 0)
4157
+ return;
4158
+ const header = `Searched for: ${context.effectiveQuery}`;
4159
+ return context.notes.length > 0 ? `${header} — ${context.notes.join("; ")}` : header;
4160
+ }
4161
+ function formatSearchResults(results, context) {
4162
+ const header = searchHeader(context);
4163
+ if (results.length === 0) {
4164
+ return header ? `${header}
4165
+
4166
+ No results found.` : "No results found.";
4167
+ }
4168
+ const body = results.map((result, index) => {
4120
4169
  const title = result.title?.trim() || result.url;
4121
4170
  const marker = formatQualityMarker(result);
4122
4171
  const lines = [`${index + 1}. [${escapeBrackets(title)}](${result.url})${marker ? ` ${marker}` : ""}`];
@@ -4132,6 +4181,9 @@ function formatSearchResults(results) {
4132
4181
  }).join(`
4133
4182
 
4134
4183
  `);
4184
+ return header ? `${header}
4185
+
4186
+ ${body}` : body;
4135
4187
  }
4136
4188
  function formatQualityMarker(result) {
4137
4189
  const parts = [];
@@ -4212,17 +4264,62 @@ var init_url = __esm(() => {
4212
4264
  });
4213
4265
 
4214
4266
  // 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 "));
4267
+ function charCount(value) {
4268
+ return Array.from(value).length;
4269
+ }
4270
+ function operatorParts(options) {
4271
+ const blocked = normalizeDomains(options.blockedDomains).map((domain) => `-site:${domain}`);
4272
+ const include = normalizeDomains(options.includeDomains).map((domain) => `site:${domain}`);
4222
4273
  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(" ");
4274
+ return {
4275
+ blocked,
4276
+ include: include.length ? include.join(" OR ") : undefined,
4277
+ language: language ? `lang:${language}` : undefined
4278
+ };
4279
+ }
4280
+ function assemble(body, parts, withInclude, withLang) {
4281
+ const segments = [body, ...parts.blocked];
4282
+ if (withInclude && parts.include)
4283
+ segments.push(parts.include);
4284
+ if (withLang && parts.language)
4285
+ segments.push(parts.language);
4286
+ return segments.filter((segment) => segment.length > 0).join(" ");
4287
+ }
4288
+ function bodyBudget(parts, withInclude, withLang, maxChars) {
4289
+ const fixed = [
4290
+ ...parts.blocked,
4291
+ ...withInclude && parts.include ? [parts.include] : [],
4292
+ ...withLang && parts.language ? [parts.language] : []
4293
+ ].join(" ");
4294
+ return maxChars - (fixed ? charCount(fixed) + 1 : 0);
4295
+ }
4296
+ function composeSearchQuery(query, options, maxChars = MAX_QUERY_CHARS) {
4297
+ const parts = operatorParts(options);
4298
+ const body = query.trim();
4299
+ const notes = [];
4300
+ const attempt = (withInclude, withLang) => {
4301
+ const budget = bodyBudget(parts, withInclude, withLang, maxChars);
4302
+ if (charCount(body) <= budget)
4303
+ return assemble(body, parts, withInclude, withLang);
4304
+ if (budget >= MIN_BODY_CHARS) {
4305
+ notes.push(`query truncated to ${budget} characters (query length limit)`);
4306
+ return assemble(truncate(body, budget), parts, withInclude, withLang);
4307
+ }
4308
+ return;
4309
+ };
4310
+ let assembled = attempt(true, true);
4311
+ if (assembled === undefined && parts.language) {
4312
+ notes.push("language filter dropped (query length limit)");
4313
+ assembled = attempt(true, false);
4314
+ }
4315
+ if (assembled === undefined && parts.include) {
4316
+ notes.push("includeDomains filter dropped (query length limit)");
4317
+ assembled = attempt(false, false);
4318
+ }
4319
+ if (assembled === undefined) {
4320
+ throw new Error(`DeepSeek search query cannot fit ${maxChars} characters: the blockedDomains -site: ` + `operators alone occupy ${charCount(parts.blocked.join(" "))} characters`);
4321
+ }
4322
+ return { query: assembled, notes };
4226
4323
  }
4227
4324
  function parsePublished(value) {
4228
4325
  if (typeof value !== "string" || !value.trim())
@@ -4257,7 +4354,9 @@ function processResults(raw, options) {
4257
4354
  continue;
4258
4355
  }
4259
4356
  seen.add(url);
4260
- const content = truncate(optionalText2(item.summary) ?? optionalText2(item.snippet) ?? "", options.maxSummaryChars);
4357
+ const rawContent = optionalText2(item.summary) ?? optionalText2(item.snippet) ?? "";
4358
+ const cutContent = truncate(rawContent, options.maxSummaryChars);
4359
+ const content = cutContent && charCount(rawContent) > options.maxSummaryChars ? `${cutContent}…` : cutContent;
4261
4360
  const published = parsePublished(item.date);
4262
4361
  const title = optionalText2(item.title);
4263
4362
  const rerankScore = typeof rerank === "number" && Number.isFinite(rerank) ? rerank : undefined;
@@ -4281,7 +4380,9 @@ function optionalText2(value) {
4281
4380
  const trimmed = value.trim();
4282
4381
  return trimmed ? trimmed : undefined;
4283
4382
  }
4383
+ var MIN_BODY_CHARS = 8;
4284
4384
  var init_search = __esm(() => {
4385
+ init_constants();
4285
4386
  init_domains();
4286
4387
  init_format2();
4287
4388
  init_url();
@@ -4487,25 +4588,41 @@ var init_transport = __esm(() => {
4487
4588
  function effectiveTimeout(configuredMs, perCallMs) {
4488
4589
  return perCallMs === undefined ? configuredMs : Math.min(configuredMs, perCallMs);
4489
4590
  }
4591
+ function effectiveMaxChars(configured, perCall) {
4592
+ if (perCall === undefined || !Number.isFinite(perCall))
4593
+ return configured;
4594
+ return Math.min(MAX_FETCH_CHARS, Math.max(0, Math.trunc(perCall)));
4595
+ }
4596
+ function apiUrl(baseUrl, path) {
4597
+ return `${baseUrl ?? DEFAULT_API_BASE_URL}${path}`;
4598
+ }
4490
4599
  function oriQuestionValue(oriQuestion) {
4491
4600
  const trimmed = oriQuestion?.trim();
4492
4601
  return trimmed ? truncate(trimmed, MAX_ORI_QUESTION_CHARS) : undefined;
4493
4602
  }
4603
+ function requestContext(config, apiKey, signal, perCall) {
4604
+ return {
4605
+ signal,
4606
+ auth: { apiKey },
4607
+ retry: config.retry,
4608
+ baseUrl: config.baseUrl,
4609
+ ...perCall?.timeoutMs === undefined ? {} : { timeoutMs: perCall.timeoutMs },
4610
+ ...perCall?.maxChars === undefined ? {} : { maxChars: perCall.maxChars }
4611
+ };
4612
+ }
4494
4613
  async function searchWeb(query, options, request) {
4495
4614
  const trimmed = query.trim();
4496
4615
  if (!trimmed)
4497
4616
  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`);
4500
- }
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)`);
4617
+ const trimmedLength = Array.from(trimmed).length;
4618
+ if (trimmedLength > MAX_QUERY_CHARS) {
4619
+ throw new Error(`DeepSeek search query must be ${MAX_QUERY_CHARS} characters or fewer ` + `(got ${trimmedLength}: ${JSON.stringify(truncate(trimmed, 64))})`);
4505
4620
  }
4621
+ const composed = composeSearchQuery(trimmed, options);
4622
+ const builtQuery = composed.query;
4506
4623
  const timeout = effectiveTimeout(options.timeoutMs, request.timeoutMs);
4507
4624
  const oriQuestion = oriQuestionValue(request.oriQuestion);
4508
- const payload = await postJson(SEARCH_ENDPOINT, {
4625
+ const payload = await postJson(apiUrl(request.baseUrl, SEARCH_ENDPOINT_PATH), {
4509
4626
  query: builtQuery,
4510
4627
  count: options.count,
4511
4628
  need_content: false,
@@ -4515,13 +4632,19 @@ async function searchWeb(query, options, request) {
4515
4632
  if (!Array.isArray(payload.results)) {
4516
4633
  throw new Error("DeepSeek Search returned a response without a results array");
4517
4634
  }
4518
- return processResults(payload.results, options);
4635
+ const results = processResults(payload.results, options);
4636
+ return {
4637
+ results,
4638
+ inputQuery: trimmed,
4639
+ effectiveQuery: builtQuery,
4640
+ notes: composed.notes
4641
+ };
4519
4642
  }
4520
4643
  async function fetchPage(url, options, request) {
4521
4644
  const target = parseFetchUrl(url.href);
4522
4645
  assertFetchHostPolicy(target, options);
4523
4646
  const timeout = effectiveTimeout(options.timeoutMs, request.timeoutMs);
4524
- const payload = await postJson(FETCH_ENDPOINT, { url: target.href }, "DeepSeek Fetch", {
4647
+ const payload = await postJson(apiUrl(request.baseUrl, FETCH_ENDPOINT_PATH), { url: target.href }, "DeepSeek Fetch", {
4525
4648
  ...request,
4526
4649
  timeoutMs: timeout
4527
4650
  });
@@ -4536,7 +4659,7 @@ async function fetchDocument(url, options, request) {
4536
4659
  if (typeof url !== "string" && !(url instanceof URL))
4537
4660
  throw new Error('"url" must be a string or URL');
4538
4661
  const page = await fetchPage(url instanceof URL ? url : parseFetchUrl(url), options, request);
4539
- const formatted = formatFetchedContent(page.title, page.content, options.maxChars);
4662
+ const formatted = formatFetchedContent(page.title, page.content, effectiveMaxChars(options.maxChars, request.maxChars));
4540
4663
  return {
4541
4664
  url: page.url,
4542
4665
  title: page.title,
@@ -4591,6 +4714,7 @@ function envOptionsReport(env = process.env) {
4591
4714
  putFalseOnly(options, "fetchEnabled", env, warnings);
4592
4715
  putNumber(options, "maxRetries", env, warnings);
4593
4716
  putNumber(options, "retryBaseDelayMs", env, warnings);
4717
+ putText(options, "baseUrl", env);
4594
4718
  const known = new Set(OPTION_ENV_VARS);
4595
4719
  for (const key of Object.keys(env)) {
4596
4720
  if (!key.startsWith("DEEPSEEK_") || key === API_KEY_ENV || known.has(key))
@@ -4675,7 +4799,8 @@ var init_env = __esm(() => {
4675
4799
  };
4676
4800
  TEXT_ENV = {
4677
4801
  language: "DEEPSEEK_SEARCH_LANGUAGE",
4678
- searchLocalization: SEARCH_LOCALIZATION_ENV
4802
+ searchLocalization: SEARCH_LOCALIZATION_ENV,
4803
+ baseUrl: BASE_URL_ENV
4679
4804
  };
4680
4805
  DOMAINS_ENV = {
4681
4806
  blockedDomains: "DEEPSEEK_SEARCH_BLOCKED_DOMAINS",
@@ -4725,15 +4850,16 @@ async function runConfigCommand(query, io, deps = {}) {
4725
4850
  const client = deps.client ?? createRetrievalClient();
4726
4851
  const startedAt = Date.now();
4727
4852
  try {
4728
- const results = await client.searchWeb(query, resolved.search, {
4853
+ const outcome = await client.searchWeb(query, resolved.search, {
4729
4854
  signal: new AbortController().signal,
4730
4855
  auth: { apiKey },
4731
- retry: resolved.retry
4856
+ retry: resolved.retry,
4857
+ baseUrl: resolved.baseUrl
4732
4858
  });
4733
4859
  const elapsedMs = Date.now() - startedAt;
4734
- io.out(`search "${query.trim()}" — ${results.length} ${results.length === 1 ? "result" : "results"} in ${elapsedMs} ms`);
4860
+ io.out(`search "${query.trim()}" — ${outcome.results.length} ${outcome.results.length === 1 ? "result" : "results"} in ${elapsedMs} ms`);
4735
4861
  io.out("");
4736
- for (const line of formatSearchResults(results).split(`
4862
+ for (const line of formatSearchResults(outcome.results, outcome).split(`
4737
4863
  `))
4738
4864
  io.out(line);
4739
4865
  return 0;
@@ -4846,6 +4972,7 @@ var init_rulesContent = __esm(() => {
4846
4972
  });
4847
4973
 
4848
4974
  // src/installer/hosts/shared.ts
4975
+ import { spawnSync } from "node:child_process";
4849
4976
  function planHeader(input, paths, loaded, dialect) {
4850
4977
  const notes = [];
4851
4978
  const errors = [];
@@ -4866,24 +4993,26 @@ function planHeader(input, paths, loaded, dialect) {
4866
4993
  notes.push(competitorSteeringNote(paths.rules, steering));
4867
4994
  return { notes, errors, abort: false };
4868
4995
  }
4996
+ function probeHostVersion(command, parse4, err) {
4997
+ const result = spawnSync(command, ["--version"], { timeout: 5000, encoding: "utf8", env: process.env, shell: true });
4998
+ const version = result.status === 0 ? parse4(result.stdout ?? "") : undefined;
4999
+ if (version === undefined) {
5000
+ err(`note: ${command} was not found on PATH or printed no version; the version-drift check was skipped`);
5001
+ }
5002
+ return version;
5003
+ }
4869
5004
  var init_shared = __esm(() => {
4870
5005
  init_detect();
4871
5006
  init_formats();
4872
5007
  });
4873
5008
 
4874
5009
  // src/installer/hosts/claude-code.ts
4875
- import { spawnSync } from "node:child_process";
4876
5010
  import { existsSync as existsSync9 } from "node:fs";
4877
5011
  function valueKind(value) {
4878
5012
  return Array.isArray(value) ? "an array" : `a ${typeof value}`;
4879
5013
  }
4880
5014
  function probeClaudeCodeVersion(err) {
4881
- const result = spawnSync("claude", ["--version"], { timeout: 5000, encoding: "utf8", env: process.env, shell: true });
4882
- const version = result.status === 0 ? parseClaudeCodeVersion(result.stdout ?? "") : undefined;
4883
- if (version === undefined) {
4884
- err("note: claude was not found on PATH or printed no version; the version-drift check was skipped");
4885
- }
4886
- return version;
5015
+ return probeHostVersion("claude", parseClaudeCodeVersion, err);
4887
5016
  }
4888
5017
  function parseClaudeCodeVersion(output) {
4889
5018
  return [...output.matchAll(/\b\d+\.\d+\.\d+\b/g)].map((match) => match[0]).at(-1);
@@ -5089,14 +5218,8 @@ var init_claude_code = __esm(() => {
5089
5218
  });
5090
5219
 
5091
5220
  // src/installer/hosts/codex.ts
5092
- import { spawnSync as spawnSync2 } from "node:child_process";
5093
5221
  function probeCodexVersion(err) {
5094
- const result = spawnSync2("codex", ["--version"], { timeout: 5000, encoding: "utf8", env: process.env, shell: true });
5095
- const version = result.status === 0 ? parseCodexVersion(result.stdout ?? "") : undefined;
5096
- if (version === undefined) {
5097
- err("note: codex was not found on PATH or printed no version; the version-drift check was skipped");
5098
- }
5099
- return version;
5222
+ return probeHostVersion("codex", parseCodexVersion, err);
5100
5223
  }
5101
5224
  function parseCodexVersion(output) {
5102
5225
  return /\bcodex(?:-cli)?\s+(\d+\.\d+\.\d+)\b/.exec(output)?.[1];
@@ -5299,8 +5422,8 @@ var init_opencode = __esm(() => {
5299
5422
  init_value();
5300
5423
  init_ownership();
5301
5424
  init_paths();
5425
+ init_record();
5302
5426
  init_rulesContent();
5303
- init_state();
5304
5427
  init_shared();
5305
5428
  OPENCODE_TOOL_IDS = { search: "websearch (provider deepseek)", fetch: "deepseek_fetch" };
5306
5429
  });
@@ -5405,10 +5528,10 @@ function planPiExtension(input, previous) {
5405
5528
  format: "rules",
5406
5529
  block: buildRulesBlock(input.host, rulesLines(RAW_MCP_TOOL_IDS, input.search, input.fetch))
5407
5530
  });
5408
- notes.push(`Rules block upserted in ${paths.rules}; Pi loads AGENTS.md context files from the working directory and its parents.`);
5531
+ notes.push(rulesUpsertNote(paths.rules));
5409
5532
  }
5410
5533
  if (forced) {
5411
- notes.push("Forced tier: Pi 1.3.0 ships no built-in web tool, so the registered tools are the only web path (de-facto). " + FORCED_COST_NOTE);
5534
+ notes.push(FORCED_NO_BUILTIN_NOTE);
5412
5535
  }
5413
5536
  return { host: "pi", scope: input.scope, tiers, edits, notes, errors };
5414
5537
  }
@@ -5515,16 +5638,17 @@ function planPiMcp(input) {
5515
5638
  format: "rules",
5516
5639
  block: buildRulesBlock(input.host, rulesLines(MCP_TOOL_IDS, input.search, input.fetch))
5517
5640
  });
5518
- notes.push(`Rules block upserted in ${paths.rules}; Pi loads AGENTS.md context files from the working directory and its parents.`);
5641
+ notes.push(rulesUpsertNote(paths.rules));
5519
5642
  }
5520
5643
  if (input.search === "forced" || input.fetch === "forced") {
5521
- notes.push("Forced tier: Pi 1.3.0 ships no built-in web tool, so the registered tools are the only web path (de-facto). " + FORCED_COST_NOTE);
5644
+ notes.push(FORCED_NO_BUILTIN_NOTE);
5522
5645
  }
5523
5646
  return { host: "pi", scope: input.scope, tiers, edits, notes, errors };
5524
5647
  }
5525
5648
  function collectSources(value) {
5526
5649
  return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
5527
5650
  }
5651
+ var rulesUpsertNote = (rules) => `Rules block upserted in ${rules}; Pi loads AGENTS.md context files from the working directory and its parents.`, FORCED_NO_BUILTIN_NOTE;
5528
5652
  var init_pi = __esm(() => {
5529
5653
  init_constants();
5530
5654
  init_settings();
@@ -5534,9 +5658,10 @@ var init_pi = __esm(() => {
5534
5658
  init_value();
5535
5659
  init_ownership();
5536
5660
  init_paths();
5661
+ init_record();
5537
5662
  init_rulesContent();
5538
- init_state();
5539
5663
  init_shared();
5664
+ FORCED_NO_BUILTIN_NOTE = "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;
5540
5665
  });
5541
5666
 
5542
5667
  // src/installer/hosts/index.ts
@@ -5623,7 +5748,7 @@ function withSecretGuard(plan) {
5623
5748
  reverts: undefined,
5624
5749
  errors: [
5625
5750
  {
5626
- message: `${edit2.file}: the existing value at ${op.path.join(".")} contains a credential-looking key ` + `(${offender}). The installer refuses to snapshot secrets into its state file. Move the key to ` + `environment indirection ("\${${API_KEY_ENV}}" expansion or env_vars) and re-run.`
5751
+ message: `${edit2.file}: the existing value at ${op.path.join(".")} contains a credential-looking key ` + `(${offender}). The installer refuses to snapshot secrets into its install record. Move the key to ` + `environment indirection ("\${${API_KEY_ENV}}" expansion or env_vars) and re-run.`
5627
5752
  }
5628
5753
  ]
5629
5754
  };
@@ -5680,7 +5805,7 @@ var init_hosts = __esm(() => {
5680
5805
  init_constants();
5681
5806
  init_value();
5682
5807
  init_ownership();
5683
- init_state();
5808
+ init_record();
5684
5809
  init_claude_code();
5685
5810
  init_codex();
5686
5811
  init_opencode();
@@ -5758,7 +5883,7 @@ function preview(value) {
5758
5883
 
5759
5884
  // src/installer/uninstall.ts
5760
5885
  function uninstall(input) {
5761
- return withOperationLock([statePathFor(input)], () => {
5886
+ return withOperationLock([recordPathFor(input)], () => {
5762
5887
  const record = readRecord(input);
5763
5888
  if (record === undefined) {
5764
5889
  throw new Error(`No ${input.host} install is recorded for the ${input.scope} scope.`);
@@ -5786,7 +5911,7 @@ function uninstallLocked(input, record) {
5786
5911
  }
5787
5912
  releaseFileOwnership(input, record.files);
5788
5913
  deleteRecord(input);
5789
- pruneEmptyDir(stateDir(input, input.scope));
5914
+ pruneEmptyDir(recordDir(input, input.scope));
5790
5915
  lines.push(`removed install record (${input.host}, ${input.scope})`);
5791
5916
  return lines;
5792
5917
  }
@@ -5795,8 +5920,8 @@ var init_uninstall = __esm(() => {
5795
5920
  init_operation_lock();
5796
5921
  init_ownership();
5797
5922
  init_paths();
5923
+ init_record();
5798
5924
  init_revert();
5799
- init_state();
5800
5925
  });
5801
5926
 
5802
5927
  // src/installer/cli.ts
@@ -5896,7 +6021,7 @@ ${USAGE}`);
5896
6021
  return 0;
5897
6022
  }
5898
6023
  if (readingLegacyGlobalRecords({ ...roots, configHome: configHome2 })) {
5899
- out(`Global install records are being read from the legacy ${legacyGlobalStateDir(roots)} ` + "(written before the Windows config home moved to %APPDATA%).");
6024
+ out(`Global install records are being read from the legacy ${legacyGlobalRecordDir(roots)} ` + "(written before the Windows config home moved to %APPDATA%).");
5900
6025
  }
5901
6026
  for (const record of records) {
5902
6027
  const by = record.installerVersion ? ` by v${record.installerVersion}` : "";
@@ -6030,7 +6155,7 @@ var init_cli = __esm(() => {
6030
6155
  init_config_command();
6031
6156
  init_hosts();
6032
6157
  init_ownership();
6033
- init_state();
6158
+ init_record();
6034
6159
  init_uninstall();
6035
6160
  HOSTS = Object.keys(HOST_PLANNERS);
6036
6161
  HOST_CHOICES = `<${HOSTS.join("|")}>`;
@@ -6070,17 +6195,6 @@ re-running.`;
6070
6195
  PATH_CHOICES = ["extension", "mcp"];
6071
6196
  });
6072
6197
 
6073
- // src/mcp/auth.ts
6074
- function requireApiKey(env) {
6075
- const apiKey = apiKeyFromEnv(env);
6076
- if (!apiKey)
6077
- throw new Error(MISSING_API_KEY_MESSAGE);
6078
- return apiKey;
6079
- }
6080
- var init_auth2 = __esm(() => {
6081
- init_auth();
6082
- });
6083
-
6084
6198
  // src/mcp/client.ts
6085
6199
  import { CLIENT_INFO_META_KEY } from "@modelcontextprotocol/server";
6086
6200
  function clientNameOf(server, ctx) {
@@ -6108,7 +6222,7 @@ var init_package = __esm(() => {
6108
6222
  package_default = {
6109
6223
  $schema: "https://json.schemastore.org/package.json",
6110
6224
  name: "deepseek-web-retrieval",
6111
- version: "0.8.0",
6225
+ version: "0.9.0",
6112
6226
  description: "Token-efficient DeepSeek Search and Fetch for coding agents, starting with OpenCode v2.",
6113
6227
  type: "module",
6114
6228
  license: "MIT",
@@ -6164,7 +6278,8 @@ var init_package = __esm(() => {
6164
6278
  prepublishOnly: "bun run check:release",
6165
6279
  test: "bun test",
6166
6280
  "test:package": "node scripts/test-package.mjs",
6167
- "test:e2e:keyless": "bun run test:e2e pi pi-extension installer reinit upgrade pkg capability-off",
6281
+ "test:e2e:keyless": "bun run test:e2e pi pi-extension pi-mock opencode-mock installer reinit upgrade pkg capability-off",
6282
+ "test:e2e:billed": "bun run test:e2e pi-billed opencode codex claude-code preferred",
6168
6283
  "check:release": "bun run check && bun run test:package && bun run test:e2e:keyless",
6169
6284
  "test:e2e": "bun run test/e2e/run.ts",
6170
6285
  "docs:matrix": "bun run scripts/render-matrix.ts",
@@ -6211,11 +6326,13 @@ var init_package = __esm(() => {
6211
6326
  });
6212
6327
 
6213
6328
  // src/core/descriptions.ts
6214
- var FETCH_SERVICE_LIMITS = "DeepSeek does not render JavaScript and cannot reach private networks; PDFs, images, and other binary files are unsupported.", FETCH_OUTPUT_TRUNCATED = "Output is truncated to the configured size.", SEARCH_RANKED_FILTERED = "Results are ranked by relevance and source authority, deduplicated by URL, and capped at the configured maximum.", SEARCH_EMPTY_IS_NORMAL = "An empty result list is normal, not an error.", SEARCH_QUERY_OPERATORS = "Use site:/-site: operators in the query to include or exclude domains, and after:/before: with a year or date (e.g. after:2025-06-01) to filter by publish time; the full query must stay within 128 characters.", SEARCH_TOOL_DESCRIPTION, FETCH_TOOL_DESCRIPTION, SEARCH_QUERY_PARAMETER = "Search query, 128 characters or fewer including operators (site:, -site:, quoted phrases, lang:, after:, before:).", SEARCH_ORI_QUESTION_PARAMETER, FETCH_URL_PARAMETER = "Absolute http:// or https:// URL to fetch. Credentials are rejected.";
6329
+ 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; the optional maxChars parameter raises or lowers that size for one call.", 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.", FETCH_MAX_CHARS_PARAMETER, SEARCH_COUNT_PARAMETER;
6215
6330
  var init_descriptions = __esm(() => {
6216
6331
  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.";
6217
6332
  FETCH_TOOL_DESCRIPTION = "Fetch one web page as clean Markdown through DeepSeek's Fetch API. " + `${FETCH_SERVICE_LIMITS} ` + `${FETCH_OUTPUT_TRUNCATED}`;
6218
6333
  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.";
6334
+ FETCH_MAX_CHARS_PARAMETER = "Optional per-call character budget for this fetch, 0-1,000,000, title included. Overrides the configured cap " + "for this call; a [content truncated] marker is appended when the output is cut.";
6335
+ SEARCH_COUNT_PARAMETER = "Optional number of results for this call, 1-50. Overrides the configured count and result cap for this call; " + "billing is per request, not per result.";
6219
6336
  });
6220
6337
 
6221
6338
  // src/mcp/server.ts
@@ -6229,15 +6346,15 @@ function createDeepSeekServer(config, env = process.env, client = retrievalClien
6229
6346
  description: SEARCH_TOOL_DESCRIPTION,
6230
6347
  inputSchema: SEARCH_INPUT,
6231
6348
  annotations: { readOnlyHint: true }
6232
- }, async ({ query, original_question }, ctx) => {
6349
+ }, async ({ query, original_question, count }, ctx) => {
6233
6350
  try {
6234
- const request = requestContext(config, env, ctx.mcpReq.signal);
6235
- const results = await client.searchWeb(query, config.search, {
6351
+ const request = requestContext(config, requireApiKey(env), ctx.mcpReq.signal);
6352
+ const outcome = await client.searchWeb(query, withPerCallCount(config.search, count), {
6236
6353
  ...request,
6237
6354
  oriQuestion: original_question
6238
6355
  });
6239
- return callResult(server, ctx, formatSearchResults(results), {
6240
- results: results.map(toStructuredResult)
6356
+ return callResult(server, ctx, formatSearchResults(outcome.results, outcome), {
6357
+ results: outcome.results.map(toStructuredResult)
6241
6358
  });
6242
6359
  } catch (error) {
6243
6360
  return toErrorResult(error);
@@ -6250,9 +6367,9 @@ function createDeepSeekServer(config, env = process.env, client = retrievalClien
6250
6367
  description: FETCH_TOOL_DESCRIPTION,
6251
6368
  inputSchema: FETCH_INPUT,
6252
6369
  annotations: { readOnlyHint: true }
6253
- }, async ({ url }, ctx) => {
6370
+ }, async ({ url, maxChars }, ctx) => {
6254
6371
  try {
6255
- const request = requestContext(config, env, ctx.mcpReq.signal);
6372
+ const request = requestContext(config, requireApiKey(env), ctx.mcpReq.signal, { maxChars });
6256
6373
  const page = await client.fetchDocument(url, config.fetch, request);
6257
6374
  return callResult(server, ctx, page.content, {
6258
6375
  url: page.url,
@@ -6267,18 +6384,13 @@ function createDeepSeekServer(config, env = process.env, client = retrievalClien
6267
6384
  }
6268
6385
  return server;
6269
6386
  }
6270
- function requestContext(config, env, signal) {
6271
- return { signal, auth: { apiKey: requireApiKey(env) }, retry: config.retry };
6272
- }
6273
6387
  function callResult(server, ctx, text, structured) {
6274
6388
  if (omitStructuredContent(server, ctx))
6275
6389
  return { content: [{ type: "text", text }] };
6276
6390
  return { content: [{ type: "text", text }], structuredContent: structured };
6277
6391
  }
6278
6392
  function servePrecheck(config) {
6279
- if (config.search.enabled || config.fetch.enabled)
6280
- return;
6281
- return `Both tools are disabled (${ENABLE_SEARCH_ENV} and ${ENABLE_FETCH_ENV} are false); ` + "at least one must stay enabled.";
6393
+ return bothDisabledPrecheck(config, `${ENABLE_SEARCH_ENV} and ${ENABLE_FETCH_ENV} are false`);
6282
6394
  }
6283
6395
  function toErrorResult(error) {
6284
6396
  const message = error instanceof Error ? error.message : String(error);
@@ -6287,18 +6399,21 @@ function toErrorResult(error) {
6287
6399
  var MCP_SERVER_NAME = "deepseek-web-retrieval", SEARCH_INPUT, FETCH_INPUT;
6288
6400
  var init_server = __esm(() => {
6289
6401
  init_package();
6402
+ init_auth();
6290
6403
  init_client();
6291
6404
  init_constants();
6292
6405
  init_descriptions();
6293
6406
  init_format2();
6294
- init_auth2();
6407
+ init_options();
6295
6408
  init_client2();
6296
6409
  SEARCH_INPUT = z2.object({
6297
6410
  query: z2.string().min(1).describe(SEARCH_QUERY_PARAMETER),
6298
- original_question: z2.string().optional().describe(SEARCH_ORI_QUESTION_PARAMETER)
6411
+ original_question: z2.string().optional().describe(SEARCH_ORI_QUESTION_PARAMETER),
6412
+ count: z2.number().int().min(1).max(MAX_SEARCH_COUNT).optional().describe(SEARCH_COUNT_PARAMETER)
6299
6413
  });
6300
6414
  FETCH_INPUT = z2.object({
6301
- url: z2.string().min(1).describe(FETCH_URL_PARAMETER)
6415
+ url: z2.string().min(1).describe(FETCH_URL_PARAMETER),
6416
+ maxChars: z2.number().int().min(0).max(MAX_FETCH_CHARS).optional().describe(FETCH_MAX_CHARS_PARAMETER)
6302
6417
  });
6303
6418
  });
6304
6419
 
@@ -6346,7 +6461,6 @@ var init_serve = __esm(() => {
6346
6461
  init_config_file();
6347
6462
  init_config_summary();
6348
6463
  init_env();
6349
- init_auth2();
6350
6464
  init_client2();
6351
6465
  init_server();
6352
6466
  });