@herbertgao/pi-extensions 2026.9.13 → 2026.9.14

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 (61) hide show
  1. package/node_modules/@herbertgao/pi-bark/package.json +2 -2
  2. package/node_modules/@herbertgao/pi-cc-extensions/package.json +4 -4
  3. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
  4. package/node_modules/@herbertgao/pi-subagents/package.json +4 -4
  5. package/node_modules/@herbertgao/resume-from/package.json +2 -2
  6. package/node_modules/@herbertgao/sol-pi/package.json +5 -5
  7. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +2 -0
  8. package/node_modules/pi-mcp-adapter/CHANGELOG.md +26 -0
  9. package/node_modules/pi-mcp-adapter/README.md +39 -9
  10. package/node_modules/pi-mcp-adapter/cli.js +33 -22
  11. package/node_modules/pi-mcp-adapter/commands.ts +3 -3
  12. package/node_modules/pi-mcp-adapter/config.ts +9 -1
  13. package/node_modules/pi-mcp-adapter/dist/config.js +9 -1
  14. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
  15. package/node_modules/pi-mcp-adapter/dist/jev-client.d.ts +1 -2
  16. package/node_modules/pi-mcp-adapter/dist/jev-client.js +59 -32
  17. package/node_modules/pi-mcp-adapter/dist/jev-client.js.map +1 -1
  18. package/node_modules/pi-mcp-adapter/dist/jev-contracts.d.ts +1 -1
  19. package/node_modules/pi-mcp-adapter/dist/jev-key-store.d.ts +33 -4
  20. package/node_modules/pi-mcp-adapter/dist/jev-key-store.js +148 -28
  21. package/node_modules/pi-mcp-adapter/dist/jev-key-store.js.map +1 -1
  22. package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js +1 -2
  23. package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js.map +1 -1
  24. package/node_modules/pi-mcp-adapter/dist/types.d.ts +7 -1
  25. package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
  26. package/node_modules/pi-mcp-adapter/index.ts +51 -38
  27. package/node_modules/pi-mcp-adapter/jev-client.ts +58 -31
  28. package/node_modules/pi-mcp-adapter/jev-contracts.ts +2 -2
  29. package/node_modules/pi-mcp-adapter/jev-key-store.ts +144 -31
  30. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
  31. package/node_modules/pi-mcp-adapter/mcp-code.ts +6 -6
  32. package/node_modules/pi-mcp-adapter/package.json +5 -5
  33. package/node_modules/pi-mcp-adapter/semantic-search.ts +3 -3
  34. package/node_modules/pi-mcp-adapter/types.ts +7 -1
  35. package/node_modules/pi-multi-account/CHANGELOG.md +6 -0
  36. package/node_modules/pi-multi-account/README.md +6 -5
  37. package/node_modules/pi-multi-account/index.ts +1 -1
  38. package/node_modules/pi-multi-account/package.json +3 -3
  39. package/node_modules/pi-typesafe/dist/backends.d.ts +10 -0
  40. package/node_modules/pi-typesafe/dist/backends.js +26 -0
  41. package/node_modules/pi-typesafe/dist/client.d.ts +1 -1
  42. package/node_modules/pi-typesafe/dist/client.js +10 -6
  43. package/node_modules/pi-typesafe/dist/errors.d.ts +10 -2
  44. package/node_modules/pi-typesafe/dist/errors.js +28 -7
  45. package/node_modules/pi-typesafe/dist/extension.js +2 -1
  46. package/node_modules/pi-typesafe/package.json +4 -4
  47. package/node_modules/pi-web-access/CHANGELOG.md +22 -0
  48. package/node_modules/pi-web-access/README.md +21 -8
  49. package/node_modules/pi-web-access/baizhi.ts +124 -0
  50. package/node_modules/pi-web-access/crawl4ai.ts +2 -10
  51. package/node_modules/pi-web-access/curator-page.ts +3 -1
  52. package/node_modules/pi-web-access/curator-server.ts +1 -0
  53. package/node_modules/pi-web-access/dist/index.js +540 -220
  54. package/node_modules/pi-web-access/firecrawl.ts +2 -10
  55. package/node_modules/pi-web-access/gemini-search.ts +5 -10
  56. package/node_modules/pi-web-access/index.ts +134 -23
  57. package/node_modules/pi-web-access/package.json +5 -1
  58. package/node_modules/pi-web-access/storage.ts +1 -0
  59. package/node_modules/pi-web-access/tool-activation.ts +134 -0
  60. package/node_modules/pi-web-access/utils.ts +9 -1
  61. package/package.json +13 -13
@@ -18,6 +18,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
18
18
  import { spawn } from "node:child_process";
19
19
  import { existsSync, readFileSync } from "node:fs";
20
20
  import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
21
+ import { isIP } from "node:net";
21
22
  import { homedir, hostname, tmpdir } from "node:os";
22
23
  import { join } from "node:path";
23
24
  function getWebSearchConfigDir() {
@@ -41,6 +42,10 @@ function getWebSearchConfigDir() {
41
42
  function getWebSearchConfigPath() {
42
43
  return join(getWebSearchConfigDir(), "web-search.json");
43
44
  }
45
+ function isLoopbackHostname(hostnameValue) {
46
+ const normalized = hostnameValue.replace(/^\[|\]$/g, "").replace(/\.$/, "").toLowerCase();
47
+ return normalized === "localhost" || normalized === "::1" || isIP(normalized) === 4 && normalized.split(".", 1)[0] === "127";
48
+ }
44
49
  function resolveApiBaseUrl(options) {
45
50
  const fromEnvironment = options.environmentValue !== void 0;
46
51
  const value = fromEnvironment ? options.environmentValue : options.configuredValue;
@@ -55,7 +60,7 @@ function resolveApiBaseUrl(options) {
55
60
  } catch {
56
61
  throw new Error(`${source} must be an absolute HTTP(S) URL`);
57
62
  }
58
- if (url.protocol !== "https:") {
63
+ if (url.protocol !== "https:" && (url.protocol !== "http:" || !isLoopbackHostname(url.hostname))) {
59
64
  throw new Error(`${source} must be an absolute HTTPS URL`);
60
65
  }
61
66
  if (url.username || url.password) {
@@ -5265,7 +5270,6 @@ var init_ssrf_protection = __esm({
5265
5270
 
5266
5271
  // firecrawl.ts
5267
5272
  import { existsSync as existsSync21, readFileSync as readFileSync22 } from "node:fs";
5268
- import net2 from "node:net";
5269
5273
  function loadConfig14() {
5270
5274
  if (cachedConfig15) return cachedConfig15;
5271
5275
  if (!existsSync21(CONFIG_PATH16)) {
@@ -5370,12 +5374,6 @@ function ssrfOptions(options) {
5370
5374
  ...options?.lookup ? { lookup: options.lookup } : {}
5371
5375
  };
5372
5376
  }
5373
- function isLoopbackApiUrl(url) {
5374
- const hostname2 = url.hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
5375
- if (hostname2 === "localhost" || hostname2 === "::1") return true;
5376
- if (net2.isIP(hostname2) !== 4) return false;
5377
- return hostname2.split(".")[0] === "127";
5378
- }
5379
5377
  function firecrawlApiSsrfOptions(options, allowLoopback) {
5380
5378
  return { ...ssrfOptions(options), allowLoopback };
5381
5379
  }
@@ -5390,7 +5388,7 @@ function withoutSensitiveHeaders(headers) {
5390
5388
  return next;
5391
5389
  }
5392
5390
  async function fetchFirecrawlApi(url, init, options) {
5393
- const allowLoopback = isLoopbackApiUrl(new URL(url));
5391
+ const allowLoopback = isLoopbackHostname(new URL(url).hostname);
5394
5392
  let current = await validateRemoteUrl(url, firecrawlApiSsrfOptions(options, allowLoopback));
5395
5393
  let headers = init.headers;
5396
5394
  for (let redirects = 0; redirects <= DEFAULT_MAX_REDIRECTS2; redirects++) {
@@ -6330,20 +6328,20 @@ var init_rsc_extract = __esm({
6330
6328
  });
6331
6329
 
6332
6330
  // datalab-pdf-extract.ts
6333
- import { existsSync as existsSync39, readFileSync as readFileSync41 } from "node:fs";
6331
+ import { existsSync as existsSync40, readFileSync as readFileSync43 } from "node:fs";
6334
6332
  function loadConfig31() {
6335
6333
  if (cachedConfig32) return cachedConfig32;
6336
- if (!existsSync39(CONFIG_PATH34)) {
6334
+ if (!existsSync40(CONFIG_PATH35)) {
6337
6335
  cachedConfig32 = {};
6338
6336
  return cachedConfig32;
6339
6337
  }
6340
- const rawText = readFileSync41(CONFIG_PATH34, "utf-8");
6338
+ const rawText = readFileSync43(CONFIG_PATH35, "utf-8");
6341
6339
  try {
6342
6340
  cachedConfig32 = JSON.parse(rawText);
6343
6341
  return cachedConfig32;
6344
6342
  } catch (err) {
6345
6343
  const message = err instanceof Error ? err.message : String(err);
6346
- throw new Error(`Failed to parse ${CONFIG_PATH34}: ${message}`);
6344
+ throw new Error(`Failed to parse ${CONFIG_PATH35}: ${message}`);
6347
6345
  }
6348
6346
  }
6349
6347
  function normalizeString(value) {
@@ -6690,7 +6688,7 @@ async function readResponseText(response) {
6690
6688
  function datalabFilename(title) {
6691
6689
  return title.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 60) || "document";
6692
6690
  }
6693
- var DEFAULT_API_HOST2, API_PREFIX, DEFAULT_TIMEOUT_MS, DEFAULT_POLL_INTERVAL_MS, MAX_RESPONSE_BYTES, MAX_ERROR_BODY_BYTES, CLEANUP_TIMEOUT_MS, CONFIG_PATH34, DATALAB_MODE_VALUES, DATALAB_LOCATION_VALUES, DEFAULT_DATALAB_MODE, DEFAULT_PROCESSING_LOCATION, DEFAULT_DATALAB_TIMEOUT_MS, cachedConfig32;
6691
+ var DEFAULT_API_HOST2, API_PREFIX, DEFAULT_TIMEOUT_MS, DEFAULT_POLL_INTERVAL_MS, MAX_RESPONSE_BYTES, MAX_ERROR_BODY_BYTES, CLEANUP_TIMEOUT_MS, CONFIG_PATH35, DATALAB_MODE_VALUES, DATALAB_LOCATION_VALUES, DEFAULT_DATALAB_MODE, DEFAULT_PROCESSING_LOCATION, DEFAULT_DATALAB_TIMEOUT_MS, cachedConfig32;
6694
6692
  var init_datalab_pdf_extract = __esm({
6695
6693
  "datalab-pdf-extract.ts"() {
6696
6694
  init_credential_source();
@@ -6702,7 +6700,7 @@ var init_datalab_pdf_extract = __esm({
6702
6700
  MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
6703
6701
  MAX_ERROR_BODY_BYTES = 300;
6704
6702
  CLEANUP_TIMEOUT_MS = 5e3;
6705
- CONFIG_PATH34 = getWebSearchConfigPath();
6703
+ CONFIG_PATH35 = getWebSearchConfigPath();
6706
6704
  DATALAB_MODE_VALUES = /* @__PURE__ */ new Set([
6707
6705
  "fast",
6708
6706
  "balanced",
@@ -6805,12 +6803,12 @@ var init_gemini_pdf_extract = __esm({
6805
6803
  });
6806
6804
 
6807
6805
  // pdf-extract.ts
6808
- import { existsSync as existsSync40, readFileSync as readFileSync42 } from "node:fs";
6806
+ import { existsSync as existsSync41, readFileSync as readFileSync44 } from "node:fs";
6809
6807
  import { writeFile as writeFile2, mkdir } from "node:fs/promises";
6810
- import { join as join7, basename as basename3 } from "node:path";
6808
+ import { join as join8, basename as basename3 } from "node:path";
6811
6809
  import { tmpdir as tmpdir3 } from "node:os";
6812
6810
  function loadPDFConfig() {
6813
- if (!existsSync40(CONFIG_PATH35)) {
6811
+ if (!existsSync41(CONFIG_PATH36)) {
6814
6812
  return {
6815
6813
  enabled: true,
6816
6814
  maxSizeMB: DEFAULT_PDF_MAX_SIZE_MB,
@@ -6820,13 +6818,13 @@ function loadPDFConfig() {
6820
6818
  datalabTimeoutMs: DEFAULT_DATALAB_TIMEOUT_MS
6821
6819
  };
6822
6820
  }
6823
- const rawText = readFileSync42(CONFIG_PATH35, "utf-8");
6821
+ const rawText = readFileSync44(CONFIG_PATH36, "utf-8");
6824
6822
  let raw;
6825
6823
  try {
6826
6824
  raw = JSON.parse(rawText);
6827
6825
  } catch (err) {
6828
6826
  const message = err instanceof Error ? err.message : String(err);
6829
- throw new Error(`Failed to parse ${CONFIG_PATH35}: ${message}`);
6827
+ throw new Error(`Failed to parse ${CONFIG_PATH36}: ${message}`);
6830
6828
  }
6831
6829
  const root = raw && typeof raw === "object" ? raw : {};
6832
6830
  const pdf = root.pdf && typeof root.pdf === "object" ? root.pdf : {};
@@ -6984,7 +6982,7 @@ async function writeMarkdownResult(options) {
6984
6982
  }
6985
6983
  const content = lines.join("\n");
6986
6984
  const outputFilename = options.filename || sanitizeFilename(options.title) + ".md";
6987
- const outputPath = join7(options.outputDir, outputFilename);
6985
+ const outputPath = join8(options.outputDir, outputFilename);
6988
6986
  await mkdir(options.outputDir, { recursive: true });
6989
6987
  await writeFile2(outputPath, content, "utf-8");
6990
6988
  return {
@@ -7035,7 +7033,7 @@ function isPDF(url, contentType) {
7035
7033
  return false;
7036
7034
  }
7037
7035
  }
7038
- var PDF_PROVIDER_VALUES, DEFAULT_PDF_MAX_SIZE_MB, MAX_PDF_MAX_SIZE_MB, MAX_DATALAB_TIMEOUT_MS, DEFAULT_MAX_PAGES, DEFAULT_OUTPUT_DIR, CONFIG_PATH35, PAGE_MARKER_PATTERN2;
7036
+ var PDF_PROVIDER_VALUES, DEFAULT_PDF_MAX_SIZE_MB, MAX_PDF_MAX_SIZE_MB, MAX_DATALAB_TIMEOUT_MS, DEFAULT_MAX_PAGES, DEFAULT_OUTPUT_DIR, CONFIG_PATH36, PAGE_MARKER_PATTERN2;
7039
7037
  var init_pdf_extract = __esm({
7040
7038
  "pdf-extract.ts"() {
7041
7039
  init_credential_source();
@@ -7053,33 +7051,33 @@ var init_pdf_extract = __esm({
7053
7051
  MAX_PDF_MAX_SIZE_MB = 50;
7054
7052
  MAX_DATALAB_TIMEOUT_MS = 3e5;
7055
7053
  DEFAULT_MAX_PAGES = 100;
7056
- DEFAULT_OUTPUT_DIR = join7(tmpdir3(), "pi-web-pdf");
7057
- CONFIG_PATH35 = getWebSearchConfigPath();
7054
+ DEFAULT_OUTPUT_DIR = join8(tmpdir3(), "pi-web-pdf");
7055
+ CONFIG_PATH36 = getWebSearchConfigPath();
7058
7056
  PAGE_MARKER_PATTERN2 = /^<!-- Page (\d+) -->$/gm;
7059
7057
  }
7060
7058
  });
7061
7059
 
7062
7060
  // github-issue-pr.ts
7063
7061
  import { execFile as execFile4 } from "node:child_process";
7064
- import { existsSync as existsSync41, readFileSync as readFileSync43 } from "node:fs";
7062
+ import { existsSync as existsSync42, readFileSync as readFileSync45 } from "node:fs";
7065
7063
  function loadConfig32() {
7066
7064
  if (cachedConfig33) return cachedConfig33;
7067
7065
  const defaults2 = { enabled: true };
7068
- if (!existsSync41(CONFIG_PATH36)) {
7066
+ if (!existsSync42(CONFIG_PATH37)) {
7069
7067
  cachedConfig33 = defaults2;
7070
7068
  return cachedConfig33;
7071
7069
  }
7072
7070
  let parsed;
7073
7071
  try {
7074
- parsed = JSON.parse(readFileSync43(CONFIG_PATH36, "utf-8"));
7072
+ parsed = JSON.parse(readFileSync45(CONFIG_PATH37, "utf-8"));
7075
7073
  } catch (err) {
7076
7074
  const message = err instanceof Error ? err.message : String(err);
7077
- throw new Error(`Failed to parse ${CONFIG_PATH36}: ${message}`);
7075
+ throw new Error(`Failed to parse ${CONFIG_PATH37}: ${message}`);
7078
7076
  }
7079
7077
  const root = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
7080
7078
  const value = root.githubPrIssue;
7081
7079
  if (value !== void 0 && (typeof value !== "object" || value === null || Array.isArray(value))) {
7082
- throw new Error(`githubPrIssue in ${CONFIG_PATH36} must be an object`);
7080
+ throw new Error(`githubPrIssue in ${CONFIG_PATH37} must be an object`);
7083
7081
  }
7084
7082
  const enabled = typeof value?.enabled === "boolean" ? value.enabled : true;
7085
7083
  cachedConfig33 = { enabled };
@@ -7647,13 +7645,13 @@ async function extractGitHubIssuePr(url, signal, options) {
7647
7645
  if ("data" in fallback) return renderGitHubPrIssue(fallback.data);
7648
7646
  return fallback;
7649
7647
  }
7650
- var CONFIG_PATH36, GH_TIMEOUT_MS, GH_TOTAL_TIMEOUT_MS, MAX_DOC_CHARS, BODY_INLINE_CHARS, COMMENT_INLINE_CHARS, MAX_INLINE_COMMENTS, MAX_INLINE_REVIEWS, MAX_INLINE_CHECKS, MAX_INLINE_FILES, MAX_INLINE_COMMITS, MAX_INLINE_REVIEW_THREADS, REST_HEADERS, PR_SUBPATHS, PR_FIELDS, PR_CORE_FIELDS, ISSUE_FIELDS, ISSUE_CORE_FIELDS, cachedConfig33;
7648
+ var CONFIG_PATH37, GH_TIMEOUT_MS, GH_TOTAL_TIMEOUT_MS, MAX_DOC_CHARS, BODY_INLINE_CHARS, COMMENT_INLINE_CHARS, MAX_INLINE_COMMENTS, MAX_INLINE_REVIEWS, MAX_INLINE_CHECKS, MAX_INLINE_FILES, MAX_INLINE_COMMITS, MAX_INLINE_REVIEW_THREADS, REST_HEADERS, PR_SUBPATHS, PR_FIELDS, PR_CORE_FIELDS, ISSUE_FIELDS, ISSUE_CORE_FIELDS, cachedConfig33;
7651
7649
  var init_github_issue_pr = __esm({
7652
7650
  "github-issue-pr.ts"() {
7653
7651
  init_github_api();
7654
7652
  init_ssrf_protection();
7655
7653
  init_utils();
7656
- CONFIG_PATH36 = getWebSearchConfigPath();
7654
+ CONFIG_PATH37 = getWebSearchConfigPath();
7657
7655
  GH_TIMEOUT_MS = 1e4;
7658
7656
  GH_TOTAL_TIMEOUT_MS = 2e4;
7659
7657
  MAX_DOC_CHARS = 15e4;
@@ -7739,15 +7737,15 @@ var init_github_issue_pr = __esm({
7739
7737
  });
7740
7738
 
7741
7739
  // feature-config.ts
7742
- import { existsSync as existsSync42, readFileSync as readFileSync44 } from "node:fs";
7740
+ import { existsSync as existsSync43, readFileSync as readFileSync46 } from "node:fs";
7743
7741
  function loadFeatureConfig() {
7744
- if (!existsSync42(CONFIG_PATH37)) return {};
7742
+ if (!existsSync43(CONFIG_PATH38)) return {};
7745
7743
  try {
7746
- const raw = JSON.parse(readFileSync44(CONFIG_PATH37, "utf-8"));
7744
+ const raw = JSON.parse(readFileSync46(CONFIG_PATH38, "utf-8"));
7747
7745
  return raw && typeof raw === "object" ? raw : {};
7748
7746
  } catch (err) {
7749
7747
  const message = err instanceof Error ? err.message : String(err);
7750
- throw new Error(`Failed to parse ${CONFIG_PATH37}: ${message}`);
7748
+ throw new Error(`Failed to parse ${CONFIG_PATH38}: ${message}`);
7751
7749
  }
7752
7750
  }
7753
7751
  function isImageEnabled() {
@@ -7760,17 +7758,17 @@ function canAttachImages() {
7760
7758
  return false;
7761
7759
  }
7762
7760
  }
7763
- var CONFIG_PATH37;
7761
+ var CONFIG_PATH38;
7764
7762
  var init_feature_config = __esm({
7765
7763
  "feature-config.ts"() {
7766
7764
  init_utils();
7767
- CONFIG_PATH37 = getWebSearchConfigPath();
7765
+ CONFIG_PATH38 = getWebSearchConfigPath();
7768
7766
  }
7769
7767
  });
7770
7768
 
7771
7769
  // youtube-extract.ts
7772
7770
  import { execFileSync } from "node:child_process";
7773
- import { existsSync as existsSync43, readFileSync as readFileSync45 } from "node:fs";
7771
+ import { existsSync as existsSync44, readFileSync as readFileSync47 } from "node:fs";
7774
7772
  function errorMessage25(err) {
7775
7773
  return err instanceof Error ? err.message : String(err);
7776
7774
  }
@@ -7791,17 +7789,17 @@ function normalizeEnabled2(value, fallback) {
7791
7789
  }
7792
7790
  function loadYouTubeConfig() {
7793
7791
  if (cachedConfig34) return cachedConfig34;
7794
- if (!existsSync43(CONFIG_PATH38)) {
7792
+ if (!existsSync44(CONFIG_PATH39)) {
7795
7793
  cachedConfig34 = { ...defaults };
7796
7794
  return cachedConfig34;
7797
7795
  }
7798
- const rawText = readFileSync45(CONFIG_PATH38, "utf-8");
7796
+ const rawText = readFileSync47(CONFIG_PATH39, "utf-8");
7799
7797
  let raw;
7800
7798
  try {
7801
7799
  raw = JSON.parse(rawText);
7802
7800
  } catch (err) {
7803
7801
  const message = err instanceof Error ? err.message : String(err);
7804
- throw new Error(`Failed to parse ${CONFIG_PATH38}: ${message}`);
7802
+ throw new Error(`Failed to parse ${CONFIG_PATH39}: ${message}`);
7805
7803
  }
7806
7804
  const yt = raw.youtube ?? {};
7807
7805
  cachedConfig34 = {
@@ -8003,7 +8001,7 @@ ${answer}
8003
8001
  return null;
8004
8002
  }
8005
8003
  }
8006
- var CONFIG_PATH38, YOUTUBE_PROMPT, YOUTUBE_REGEX, defaults, cachedConfig34;
8004
+ var CONFIG_PATH39, YOUTUBE_PROMPT, YOUTUBE_REGEX, defaults, cachedConfig34;
8007
8005
  var init_youtube_extract = __esm({
8008
8006
  "youtube-extract.ts"() {
8009
8007
  init_activity();
@@ -8013,7 +8011,7 @@ var init_youtube_extract = __esm({
8013
8011
  init_perplexity();
8014
8012
  init_extract();
8015
8013
  init_utils();
8016
- CONFIG_PATH38 = getWebSearchConfigPath();
8014
+ CONFIG_PATH39 = getWebSearchConfigPath();
8017
8015
  YOUTUBE_PROMPT = `Extract the complete content of this YouTube video. Include:
8018
8016
  1. Video title, channel name, and duration
8019
8017
  2. A brief summary (2-3 sentences)
@@ -8125,24 +8123,23 @@ URL: `;
8125
8123
  });
8126
8124
 
8127
8125
  // crawl4ai.ts
8128
- import { existsSync as existsSync44, readFileSync as readFileSync46 } from "node:fs";
8129
- import net3 from "node:net";
8126
+ import { existsSync as existsSync45, readFileSync as readFileSync48 } from "node:fs";
8130
8127
  function loadConfig33() {
8131
8128
  if (cachedConfig35) return cachedConfig35;
8132
- if (!existsSync44(CONFIG_PATH39)) {
8129
+ if (!existsSync45(CONFIG_PATH40)) {
8133
8130
  cachedConfig35 = {};
8134
8131
  return cachedConfig35;
8135
8132
  }
8136
- const raw = readFileSync46(CONFIG_PATH39, "utf8");
8133
+ const raw = readFileSync48(CONFIG_PATH40, "utf8");
8137
8134
  let parsed;
8138
8135
  try {
8139
8136
  parsed = JSON.parse(raw);
8140
8137
  } catch (err) {
8141
8138
  const message = err instanceof Error ? err.message : String(err);
8142
- throw new Error(`Failed to parse ${CONFIG_PATH39}: ${message}`);
8139
+ throw new Error(`Failed to parse ${CONFIG_PATH40}: ${message}`);
8143
8140
  }
8144
8141
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
8145
- throw new Error(`Invalid config in ${CONFIG_PATH39}: expected a JSON object`);
8142
+ throw new Error(`Invalid config in ${CONFIG_PATH40}: expected a JSON object`);
8146
8143
  }
8147
8144
  cachedConfig35 = parsed;
8148
8145
  return cachedConfig35;
@@ -8155,13 +8152,13 @@ function normalizeBaseUrl4(value) {
8155
8152
  try {
8156
8153
  parsed = new URL(trimmed);
8157
8154
  } catch {
8158
- throw new Error(`Invalid Crawl4AI base URL in ${CONFIG_PATH39}: expected an HTTP or HTTPS URL`);
8155
+ throw new Error(`Invalid Crawl4AI base URL in ${CONFIG_PATH40}: expected an HTTP or HTTPS URL`);
8159
8156
  }
8160
8157
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
8161
- throw new Error(`Invalid Crawl4AI base URL in ${CONFIG_PATH39}: expected an HTTP or HTTPS URL`);
8158
+ throw new Error(`Invalid Crawl4AI base URL in ${CONFIG_PATH40}: expected an HTTP or HTTPS URL`);
8162
8159
  }
8163
8160
  if (parsed.username || parsed.password) {
8164
- throw new Error(`Invalid Crawl4AI base URL in ${CONFIG_PATH39}: URL credentials are not allowed`);
8161
+ throw new Error(`Invalid Crawl4AI base URL in ${CONFIG_PATH40}: URL credentials are not allowed`);
8165
8162
  }
8166
8163
  parsed.pathname = parsed.pathname.replace(/\/+$/, "");
8167
8164
  parsed.search = "";
@@ -8176,7 +8173,7 @@ function requireBaseUrl3() {
8176
8173
  if (!baseUrl) {
8177
8174
  throw new Error(
8178
8175
  `Crawl4AI base URL not configured. Either:
8179
- 1. Set crawl4aiBaseUrl in ${CONFIG_PATH39}
8176
+ 1. Set crawl4aiBaseUrl in ${CONFIG_PATH40}
8180
8177
  2. Set CRAWL4AI_BASE_URL environment variable`
8181
8178
  );
8182
8179
  }
@@ -8206,12 +8203,6 @@ function ssrfOptions2(options) {
8206
8203
  ...options?.lookup ? { lookup: options.lookup } : {}
8207
8204
  };
8208
8205
  }
8209
- function isLoopbackApiUrl2(url) {
8210
- const hostname2 = url.hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
8211
- if (hostname2 === "localhost" || hostname2 === "::1") return true;
8212
- if (net3.isIP(hostname2) !== 4) return false;
8213
- return hostname2.split(".")[0] === "127";
8214
- }
8215
8206
  function firstHeadingTitle(markdown) {
8216
8207
  return /^[ \t]*#[ \t]+(\S.*?)[ \t\r]*$/m.exec(markdown)?.[1] ?? "";
8217
8208
  }
@@ -8237,7 +8228,7 @@ async function extractWithCrawl4ai(url, signal, options) {
8237
8228
  try {
8238
8229
  const response = await fetchRemoteUrl(requestUrl, init, {
8239
8230
  ...ssrf,
8240
- allowLoopback: isLoopbackApiUrl2(requestUrl),
8231
+ allowLoopback: isLoopbackHostname(requestUrl.hostname),
8241
8232
  onRedirect: ({ to, init: redirectInit, response: response2 }) => {
8242
8233
  if (to.origin !== requestUrl.origin) {
8243
8234
  throw new Error(`Crawl4AI refused cross-origin redirect to ${to.origin}`);
@@ -8281,14 +8272,14 @@ async function extractWithCrawl4ai(url, signal, options) {
8281
8272
  throw err;
8282
8273
  }
8283
8274
  }
8284
- var CONFIG_PATH39, EXTRACT_TIMEOUT_MS2, MARKDOWN_FILTER, cachedConfig35;
8275
+ var CONFIG_PATH40, EXTRACT_TIMEOUT_MS2, MARKDOWN_FILTER, cachedConfig35;
8285
8276
  var init_crawl4ai = __esm({
8286
8277
  "crawl4ai.ts"() {
8287
8278
  init_activity();
8288
8279
  init_credential_source();
8289
8280
  init_ssrf_protection();
8290
8281
  init_utils();
8291
- CONFIG_PATH39 = getWebSearchConfigPath();
8282
+ CONFIG_PATH40 = getWebSearchConfigPath();
8292
8283
  EXTRACT_TIMEOUT_MS2 = 6e4;
8293
8284
  MARKDOWN_FILTER = "fit";
8294
8285
  cachedConfig35 = null;
@@ -8296,7 +8287,7 @@ var init_crawl4ai = __esm({
8296
8287
  });
8297
8288
 
8298
8289
  // brightdata-unlocker.ts
8299
- import { existsSync as existsSync45, readFileSync as readFileSync47 } from "node:fs";
8290
+ import { existsSync as existsSync46, readFileSync as readFileSync49 } from "node:fs";
8300
8291
  function parseFailureDetail(err) {
8301
8292
  const message = err instanceof Error ? err.message : String(err);
8302
8293
  const position = message.match(/at position \d+(?: \(line \d+ column \d+\))?/);
@@ -8304,19 +8295,19 @@ function parseFailureDetail(err) {
8304
8295
  }
8305
8296
  function loadConfig34() {
8306
8297
  if (cachedConfig36) return cachedConfig36;
8307
- if (!existsSync45(CONFIG_PATH40)) {
8298
+ if (!existsSync46(CONFIG_PATH41)) {
8308
8299
  cachedConfig36 = {};
8309
8300
  return cachedConfig36;
8310
8301
  }
8311
- const raw = readFileSync47(CONFIG_PATH40, "utf8");
8302
+ const raw = readFileSync49(CONFIG_PATH41, "utf8");
8312
8303
  let parsed;
8313
8304
  try {
8314
8305
  parsed = JSON.parse(raw);
8315
8306
  } catch (err) {
8316
- throw new Error(`Failed to parse ${CONFIG_PATH40}: ${parseFailureDetail(err)}`);
8307
+ throw new Error(`Failed to parse ${CONFIG_PATH41}: ${parseFailureDetail(err)}`);
8317
8308
  }
8318
8309
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
8319
- throw new Error(`Invalid config in ${CONFIG_PATH40}: expected a JSON object`);
8310
+ throw new Error(`Invalid config in ${CONFIG_PATH41}: expected a JSON object`);
8320
8311
  }
8321
8312
  cachedConfig36 = parsed;
8322
8313
  return cachedConfig36;
@@ -8331,7 +8322,7 @@ function zoneSetting() {
8331
8322
  const fromEnv = process.env.BRIGHTDATA_UNLOCKER_ZONE;
8332
8323
  if (typeof fromEnv === "string" && fromEnv.trim()) return { raw: fromEnv.trim(), label: "BRIGHTDATA_UNLOCKER_ZONE" };
8333
8324
  const configured = loadConfig34().brightdataUnlockerZone;
8334
- if (typeof configured === "string" && configured.trim()) return { raw: configured.trim(), label: `brightdataUnlockerZone in ${CONFIG_PATH40}` };
8325
+ if (typeof configured === "string" && configured.trim()) return { raw: configured.trim(), label: `brightdataUnlockerZone in ${CONFIG_PATH41}` };
8335
8326
  return null;
8336
8327
  }
8337
8328
  function getZone() {
@@ -8349,7 +8340,7 @@ function requireZone() {
8349
8340
  }
8350
8341
  throw new Error(
8351
8342
  `Bright Data Web Unlocker zone not configured. Either:
8352
- 1. Set brightdataUnlockerZone in ${CONFIG_PATH40}
8343
+ 1. Set brightdataUnlockerZone in ${CONFIG_PATH41}
8353
8344
  2. Set BRIGHTDATA_UNLOCKER_ZONE environment variable
8354
8345
  The zone must be of type "unblocker"; a SERP zone will not serve Web Unlocker requests.`
8355
8346
  );
@@ -8364,7 +8355,7 @@ async function getApiKey26(signal) {
8364
8355
  if (!apiKey) {
8365
8356
  throw new Error(
8366
8357
  `Bright Data API key not found. Either:
8367
- 1. Create ${CONFIG_PATH40} with { "brightdataApiKey": "your-key" }
8358
+ 1. Create ${CONFIG_PATH41} with { "brightdataApiKey": "your-key" }
8368
8359
  2. Set BRIGHTDATA_API_KEY environment variable
8369
8360
  Get a key at https://brightdata.com/cp/setting/users`
8370
8361
  );
@@ -8473,14 +8464,14 @@ async function extractWithBrightDataUnlocker(url, signal, options) {
8473
8464
  if (!content) return null;
8474
8465
  return { url, title: headingTitle(content), content, error: null };
8475
8466
  }
8476
- var CONFIG_PATH40, BRIGHTDATA_REQUEST_URL, EXTRACT_TIMEOUT_MS3, DEFAULT_MAX_REDIRECTS3, REDIRECT_STATUSES3, ZONE_PATTERN2, cachedConfig36;
8467
+ var CONFIG_PATH41, BRIGHTDATA_REQUEST_URL, EXTRACT_TIMEOUT_MS3, DEFAULT_MAX_REDIRECTS3, REDIRECT_STATUSES3, ZONE_PATTERN2, cachedConfig36;
8477
8468
  var init_brightdata_unlocker = __esm({
8478
8469
  "brightdata-unlocker.ts"() {
8479
8470
  init_activity();
8480
8471
  init_credential_source();
8481
8472
  init_ssrf_protection();
8482
8473
  init_utils();
8483
- CONFIG_PATH40 = getWebSearchConfigPath();
8474
+ CONFIG_PATH41 = getWebSearchConfigPath();
8484
8475
  BRIGHTDATA_REQUEST_URL = "https://api.brightdata.com/request";
8485
8476
  EXTRACT_TIMEOUT_MS3 = 6e4;
8486
8477
  DEFAULT_MAX_REDIRECTS3 = 5;
@@ -8492,9 +8483,9 @@ var init_brightdata_unlocker = __esm({
8492
8483
 
8493
8484
  // video-extract.ts
8494
8485
  import { execFileSync as execFileSync2 } from "node:child_process";
8495
- import { existsSync as existsSync46, readFileSync as readFileSync48, readdirSync as readdirSync4, statSync as statSync3 } from "node:fs";
8486
+ import { existsSync as existsSync47, readFileSync as readFileSync50, readdirSync as readdirSync4, statSync as statSync3 } from "node:fs";
8496
8487
  import { readFile as readFile2 } from "node:fs/promises";
8497
- import { resolve, extname as extname2, basename as basename4, join as join8, dirname as dirname2 } from "node:path";
8488
+ import { resolve, extname as extname2, basename as basename4, join as join9, dirname as dirname3 } from "node:path";
8498
8489
  function shouldRethrow3(err) {
8499
8490
  const message = err instanceof Error ? err.message : String(err);
8500
8491
  return message.startsWith("Failed to parse ");
@@ -8513,17 +8504,17 @@ function normalizeMaxSizeMB(value, fallback) {
8513
8504
  }
8514
8505
  function loadVideoConfig() {
8515
8506
  if (cachedVideoConfig) return cachedVideoConfig;
8516
- if (!existsSync46(CONFIG_PATH41)) {
8507
+ if (!existsSync47(CONFIG_PATH42)) {
8517
8508
  cachedVideoConfig = { ...VIDEO_CONFIG_DEFAULTS };
8518
8509
  return cachedVideoConfig;
8519
8510
  }
8520
- const rawText = readFileSync48(CONFIG_PATH41, "utf-8");
8511
+ const rawText = readFileSync50(CONFIG_PATH42, "utf-8");
8521
8512
  let raw;
8522
8513
  try {
8523
8514
  raw = JSON.parse(rawText);
8524
8515
  } catch (err) {
8525
8516
  const message = err instanceof Error ? err.message : String(err);
8526
- throw new Error(`Failed to parse ${CONFIG_PATH41}: ${message}`);
8517
+ throw new Error(`Failed to parse ${CONFIG_PATH42}: ${message}`);
8527
8518
  }
8528
8519
  const v = raw.video ?? {};
8529
8520
  cachedVideoConfig = {
@@ -8569,14 +8560,14 @@ function isVideoFile(input) {
8569
8560
  }
8570
8561
  function resolveFilePath(filePath) {
8571
8562
  const absolutePath = resolve(filePath);
8572
- if (existsSync46(absolutePath)) return absolutePath;
8573
- const dir = dirname2(absolutePath);
8563
+ if (existsSync47(absolutePath)) return absolutePath;
8564
+ const dir = dirname3(absolutePath);
8574
8565
  const base = basename4(absolutePath);
8575
- if (!existsSync46(dir)) return null;
8566
+ if (!existsSync47(dir)) return null;
8576
8567
  try {
8577
8568
  const normalizedBase = normalizeSpaces(base);
8578
8569
  const match = readdirSync4(dir).find((f) => normalizeSpaces(f) === normalizedBase);
8579
- return match ? join8(dir, match) : null;
8570
+ return match ? join9(dir, match) : null;
8580
8571
  } catch {
8581
8572
  return null;
8582
8573
  }
@@ -8770,7 +8761,7 @@ function deleteGeminiFile(fileName, apiKey) {
8770
8761
  function extractVideoTitle(text2, filePath) {
8771
8762
  return extractHeadingTitle(text2) ?? basename4(filePath, extname2(filePath));
8772
8763
  }
8773
- var CONFIG_PATH41, DEFAULT_VIDEO_PROMPT, VIDEO_EXTENSIONS, VIDEO_CONFIG_DEFAULTS, cachedVideoConfig;
8764
+ var CONFIG_PATH42, DEFAULT_VIDEO_PROMPT, VIDEO_EXTENSIONS, VIDEO_CONFIG_DEFAULTS, cachedVideoConfig;
8774
8765
  var init_video_extract = __esm({
8775
8766
  "video-extract.ts"() {
8776
8767
  init_activity();
@@ -8779,7 +8770,7 @@ var init_video_extract = __esm({
8779
8770
  init_gemini_api();
8780
8771
  init_extract();
8781
8772
  init_utils();
8782
- CONFIG_PATH41 = getWebSearchConfigPath();
8773
+ CONFIG_PATH42 = getWebSearchConfigPath();
8783
8774
  DEFAULT_VIDEO_PROMPT = `Extract the complete content of this video. Include:
8784
8775
  1. Video title (infer from content if not explicit), duration
8785
8776
  2. A brief summary (2-3 sentences)
@@ -9264,13 +9255,13 @@ __export(extract_exports, {
9264
9255
  readPDFResponseBuffer: () => readPDFResponseBuffer,
9265
9256
  resolveFetchTimeoutMs: () => resolveFetchTimeoutMs
9266
9257
  });
9267
- import { existsSync as existsSync47, readFileSync as readFileSync49 } from "node:fs";
9258
+ import { existsSync as existsSync48, readFileSync as readFileSync51 } from "node:fs";
9268
9259
  import pLimit from "p-limit";
9269
9260
  function loadFetchTimeoutMs() {
9270
- if (!existsSync47(WEB_SEARCH_CONFIG_PATH3)) return DEFAULT_TIMEOUT_MS3;
9261
+ if (!existsSync48(WEB_SEARCH_CONFIG_PATH3)) return DEFAULT_TIMEOUT_MS3;
9271
9262
  let raw;
9272
9263
  try {
9273
- raw = JSON.parse(readFileSync49(WEB_SEARCH_CONFIG_PATH3, "utf-8"));
9264
+ raw = JSON.parse(readFileSync51(WEB_SEARCH_CONFIG_PATH3, "utf-8"));
9274
9265
  } catch (err) {
9275
9266
  const message = err instanceof Error ? err.message : String(err);
9276
9267
  throw new Error(`Failed to parse ${WEB_SEARCH_CONFIG_PATH3}: ${message}`);
@@ -9396,12 +9387,12 @@ async function fetchAuthenticatedRemoteUrl(url, init, validationOptions, profile
9396
9387
  throw new Error(`Too many redirects fetching ${current.toString()}`);
9397
9388
  }
9398
9389
  function loadFetchRouting() {
9399
- if (!existsSync47(WEB_SEARCH_CONFIG_PATH3)) {
9390
+ if (!existsSync48(WEB_SEARCH_CONFIG_PATH3)) {
9400
9391
  return { providers: DEFAULT_FETCH_PROVIDER_ORDER, allowRemoteHostedProviders: false };
9401
9392
  }
9402
9393
  let raw;
9403
9394
  try {
9404
- const parsed = JSON.parse(readFileSync49(WEB_SEARCH_CONFIG_PATH3, "utf-8"));
9395
+ const parsed = JSON.parse(readFileSync51(WEB_SEARCH_CONFIG_PATH3, "utf-8"));
9405
9396
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
9406
9397
  throw new Error("expected a JSON object");
9407
9398
  }
@@ -10461,7 +10452,7 @@ var init_extract = __esm({
10461
10452
 
10462
10453
  // index.ts
10463
10454
  import { Box, Text, truncateToWidth } from "@earendil-works/pi-tui";
10464
- import { Type } from "typebox";
10455
+ import { Type as Type2 } from "typebox";
10465
10456
  import pLimit2 from "p-limit";
10466
10457
 
10467
10458
  // fetch-params.ts
@@ -11048,7 +11039,7 @@ init_gemini_api();
11048
11039
  init_gemini_adc();
11049
11040
  init_gemini_web();
11050
11041
  init_perplexity();
11051
- import { existsSync as existsSync38, readFileSync as readFileSync39 } from "node:fs";
11042
+ import { existsSync as existsSync39, readFileSync as readFileSync40 } from "node:fs";
11052
11043
 
11053
11044
  // exa.ts
11054
11045
  init_activity();
@@ -15048,6 +15039,118 @@ async function searchWithSerply(query, options = {}) {
15048
15039
  return { answer: formatSearchResultsAsAnswer(results), results };
15049
15040
  }
15050
15041
 
15042
+ // baizhi.ts
15043
+ init_activity();
15044
+ init_credential_source();
15045
+ init_domain_filter_normalization();
15046
+ init_search_result_count_normalization();
15047
+ init_utils();
15048
+ import { existsSync as existsSync36, readFileSync as readFileSync37 } from "node:fs";
15049
+ import { isIP as isIP2 } from "node:net";
15050
+ var MCP_URL = "https://agent-toolkit.app.baizhi.cloud/mcp";
15051
+ var SEARCH_TIMEOUT_MS25 = 6e4;
15052
+ var CONFIG_PATH31 = getWebSearchConfigPath();
15053
+ function credentialOptions() {
15054
+ let config = {};
15055
+ if (existsSync36(CONFIG_PATH31)) {
15056
+ try {
15057
+ const parsed = JSON.parse(readFileSync37(CONFIG_PATH31, "utf8"));
15058
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error();
15059
+ config = parsed;
15060
+ } catch {
15061
+ throw new Error(`Invalid configuration in ${CONFIG_PATH31}: expected a JSON object`);
15062
+ }
15063
+ }
15064
+ return { provider: "Baizhi", configuredValue: config.baizhiApiKey, environmentValue: process.env.BAIZHI_API_KEY };
15065
+ }
15066
+ function isBaizhiAvailable() {
15067
+ return hasCredentialSource(credentialOptions());
15068
+ }
15069
+ function searchArguments(query, options) {
15070
+ if (!query.trim()) throw new Error("Baizhi search query must not be empty");
15071
+ const domains = [];
15072
+ const excludeDomains = [];
15073
+ for (const raw of options.domainFilter ?? []) {
15074
+ const bare = raw.trim().replace(/^-/, "").trim();
15075
+ const domain = isIP2(bare) ? bare : normalizeDomain(raw);
15076
+ if (!domain) throw new Error("Baizhi domain filter must contain a valid domain");
15077
+ const target = raw.trim().startsWith("-") ? excludeDomains : domains;
15078
+ if (!target.includes(domain)) target.push(domain);
15079
+ }
15080
+ return {
15081
+ query: query.trim(),
15082
+ count: normalizeSearchResultCount(options.numResults),
15083
+ need_summary: false,
15084
+ // The published discovery contract defaults to month, not unrestricted time.
15085
+ time_range: options.recencyFilter ?? "month",
15086
+ ...domains.length || excludeDomains.length ? { filter: {
15087
+ ...domains.length ? { domains } : {},
15088
+ ...excludeDomains.length ? { exclude_domains: excludeDomains } : {}
15089
+ } } : {}
15090
+ };
15091
+ }
15092
+ async function searchWithBaizhi(query, options = {}) {
15093
+ const args = searchArguments(query, options);
15094
+ const timeoutSignal = AbortSignal.timeout(SEARCH_TIMEOUT_MS25);
15095
+ const signal = options.signal ? AbortSignal.any([timeoutSignal, options.signal]) : timeoutSignal;
15096
+ if (signal.aborted) throw new Error("Aborted");
15097
+ const apiKey = await resolveCredential({ ...credentialOptions(), signal });
15098
+ if (!apiKey) throw new Error("Baizhi API key not found. Set BAIZHI_API_KEY or baizhiApiKey in web-search.json. Get your own key at https://agent-toolkit.app.baizhi.cloud/");
15099
+ let headers;
15100
+ try {
15101
+ headers = new Headers({ Authorization: `Bearer ${apiKey}` });
15102
+ } catch {
15103
+ throw new Error("Baizhi credential resolution failed: invalid-header-value");
15104
+ }
15105
+ const [{ Client }, { StreamableHTTPClientTransport }] = await Promise.all([
15106
+ import("@modelcontextprotocol/sdk/client/index.js"),
15107
+ import("@modelcontextprotocol/sdk/client/streamableHttp.js")
15108
+ ]);
15109
+ let cleaningUp = false;
15110
+ const transport = new StreamableHTTPClientTransport(new URL(MCP_URL), {
15111
+ requestInit: { headers },
15112
+ fetch: async (url, init) => {
15113
+ if (String(url) !== MCP_URL) throw new Error("Baizhi endpoint changed unexpectedly");
15114
+ const requestSignal15 = cleaningUp ? AbortSignal.timeout(1500) : signal;
15115
+ return fetch(url, { ...init, redirect: "error", signal: !cleaningUp && init?.signal ? AbortSignal.any([init.signal, requestSignal15]) : requestSignal15 });
15116
+ },
15117
+ reconnectionOptions: { maxRetries: 0, initialReconnectionDelay: 1e3, maxReconnectionDelay: 1e3, reconnectionDelayGrowFactor: 1 }
15118
+ });
15119
+ const client = new Client({ name: "pi-web-access-baizhi", version: "1.0.0" });
15120
+ const activityId = activityMonitor.logStart({ type: "api", query });
15121
+ try {
15122
+ await client.connect(transport, { signal, timeout: SEARCH_TIMEOUT_MS25 });
15123
+ const result = await client.callTool({ name: "websearch_search", arguments: args }, void 0, { signal, timeout: SEARCH_TIMEOUT_MS25 });
15124
+ if (result.isError) throw new Error("Baizhi search tool returned an error");
15125
+ const parts = [];
15126
+ if (result.structuredContent !== void 0) parts.push(JSON.stringify(result.structuredContent, null, 2));
15127
+ for (const item of Array.isArray(result.content) ? result.content : []) {
15128
+ if (item.type === "text" && typeof item.text === "string" && item.text.trim()) parts.push(item.text);
15129
+ }
15130
+ if (!parts.length) throw new Error("Baizhi returned invalid response: no search text or structured content");
15131
+ const answer = redactCredential(redactCredential(parts.join("\n\n"), apiKey), JSON.stringify(apiKey).slice(1, -1));
15132
+ activityMonitor.logComplete(activityId, 200);
15133
+ return { answer, results: [] };
15134
+ } catch (err) {
15135
+ let message;
15136
+ if (options.signal?.aborted) message = "Aborted";
15137
+ else if (timeoutSignal.aborted) message = "Baizhi request timed out after 60s";
15138
+ else {
15139
+ const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
15140
+ message = typeof code === "number" && Number.isInteger(code) && code >= 400 && code <= 599 ? `Baizhi HTTP ${code} request failed; check your API key, account and service status` : err instanceof TypeError ? "Baizhi network request failed" : err instanceof SyntaxError || code === -32700 || code === -32602 ? "Baizhi returned invalid response" : err instanceof Error && /^Baizhi (search tool returned an error|returned invalid response:)/.test(err.message) ? err.message : "Baizhi MCP request failed; check your connection, API key and account status";
15141
+ }
15142
+ activityMonitor.logError(activityId, message);
15143
+ throw new Error(message);
15144
+ } finally {
15145
+ cleaningUp = true;
15146
+ try {
15147
+ await transport.terminateSession();
15148
+ } catch {
15149
+ }
15150
+ await client.close();
15151
+ }
15152
+ }
15153
+
15051
15154
  // valyu.ts
15052
15155
  init_activity();
15053
15156
  init_domain_filter_normalization();
@@ -15055,29 +15158,29 @@ init_search_answer_formatting();
15055
15158
  init_search_result_count_normalization();
15056
15159
  init_credential_source();
15057
15160
  init_utils();
15058
- import { existsSync as existsSync36, readFileSync as readFileSync37 } from "node:fs";
15161
+ import { existsSync as existsSync37, readFileSync as readFileSync38 } from "node:fs";
15059
15162
  var VALYU_SEARCH_URL = "https://api.valyu.ai/v1/search";
15060
- var CONFIG_PATH31 = getWebSearchConfigPath();
15061
- var SEARCH_TIMEOUT_MS25 = 6e4;
15163
+ var CONFIG_PATH32 = getWebSearchConfigPath();
15164
+ var SEARCH_TIMEOUT_MS26 = 6e4;
15062
15165
  var MAX_SNIPPET_CHARS = 2500;
15063
15166
  var MAX_CONTENT_CHARS = 4e3;
15064
15167
  var cachedConfig30 = null;
15065
15168
  function loadConfig29() {
15066
15169
  if (cachedConfig30) return cachedConfig30;
15067
- if (!existsSync36(CONFIG_PATH31)) {
15170
+ if (!existsSync37(CONFIG_PATH32)) {
15068
15171
  cachedConfig30 = {};
15069
15172
  return cachedConfig30;
15070
15173
  }
15071
- const raw = readFileSync37(CONFIG_PATH31, "utf-8");
15174
+ const raw = readFileSync38(CONFIG_PATH32, "utf-8");
15072
15175
  let parsed;
15073
15176
  try {
15074
15177
  parsed = JSON.parse(raw);
15075
15178
  } catch (err) {
15076
15179
  const message = err instanceof Error ? err.message : String(err);
15077
- throw new Error(`Failed to parse ${CONFIG_PATH31}: ${message}`);
15180
+ throw new Error(`Failed to parse ${CONFIG_PATH32}: ${message}`);
15078
15181
  }
15079
15182
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
15080
- throw new Error(`Invalid config in ${CONFIG_PATH31}: expected a JSON object`);
15183
+ throw new Error(`Invalid config in ${CONFIG_PATH32}: expected a JSON object`);
15081
15184
  }
15082
15185
  cachedConfig30 = parsed;
15083
15186
  return cachedConfig30;
@@ -15095,7 +15198,7 @@ async function requireApiKey12(signal) {
15095
15198
  if (!apiKey) {
15096
15199
  throw new Error(
15097
15200
  `Valyu API key not found. Either:
15098
- 1. Create ${CONFIG_PATH31} with { "valyuApiKey": "your-key" }
15201
+ 1. Create ${CONFIG_PATH32} with { "valyuApiKey": "your-key" }
15099
15202
  2. Set VALYU_API_KEY environment variable
15100
15203
  Get a key at https://platform.valyu.ai`
15101
15204
  );
@@ -15152,7 +15255,7 @@ async function searchWithValyu(query, options = {}) {
15152
15255
  method: "POST",
15153
15256
  headers: { "x-api-key": apiKey, "Content-Type": "application/json", Accept: "application/json" },
15154
15257
  body: JSON.stringify({ query, max_num_results: numResults, ...mapDomainFilter8(options.domainFilter), ...startDate ? { start_date: startDate } : {} }),
15155
- signal: options.signal ? AbortSignal.any([AbortSignal.timeout(SEARCH_TIMEOUT_MS25), options.signal]) : AbortSignal.timeout(SEARCH_TIMEOUT_MS25)
15258
+ signal: options.signal ? AbortSignal.any([AbortSignal.timeout(SEARCH_TIMEOUT_MS26), options.signal]) : AbortSignal.timeout(SEARCH_TIMEOUT_MS26)
15156
15259
  });
15157
15260
  } catch (err) {
15158
15261
  const message = errorMessage21(err);
@@ -15209,7 +15312,7 @@ init_search_result_count_normalization();
15209
15312
  import { randomUUID as randomUUID2 } from "node:crypto";
15210
15313
  var KIMI_SEARCH_URL = "https://api.kimi.com/coding/v1/search";
15211
15314
  var KIMI_PROVIDERS = ["kimi-coding", "kimi-code"];
15212
- var SEARCH_TIMEOUT_MS26 = 3e4;
15315
+ var SEARCH_TIMEOUT_MS27 = 3e4;
15213
15316
  function buildRequestHeaders(auth) {
15214
15317
  const headers = new Headers();
15215
15318
  for (const [name, value] of Object.entries(auth.headers)) {
@@ -15324,7 +15427,7 @@ async function searchWithKimi(query, options = {}, ctx) {
15324
15427
  throw new Error("Kimi Code web search unavailable. Run /login kimi-coding to authenticate a Kimi Code Plan account.");
15325
15428
  }
15326
15429
  const activityId = activityMonitor.logStart({ type: "api", query });
15327
- const timeoutSignal = AbortSignal.timeout(SEARCH_TIMEOUT_MS26);
15430
+ const timeoutSignal = AbortSignal.timeout(SEARCH_TIMEOUT_MS27);
15328
15431
  const requestSignal15 = options.signal ? AbortSignal.any([timeoutSignal, options.signal]) : timeoutSignal;
15329
15432
  try {
15330
15433
  const response = await fetch(KIMI_SEARCH_URL, {
@@ -15376,30 +15479,30 @@ init_activity();
15376
15479
  init_domain_filter_normalization();
15377
15480
  init_credential_source();
15378
15481
  init_utils();
15379
- import { existsSync as existsSync37, readFileSync as readFileSync38 } from "node:fs";
15482
+ import { existsSync as existsSync38, readFileSync as readFileSync39 } from "node:fs";
15380
15483
  var MISTRAL_CONVERSATIONS_URL = "https://api.mistral.ai/v1/conversations";
15381
- var CONFIG_PATH32 = getWebSearchConfigPath();
15382
- var SEARCH_TIMEOUT_MS27 = 6e4;
15484
+ var CONFIG_PATH33 = getWebSearchConfigPath();
15485
+ var SEARCH_TIMEOUT_MS28 = 6e4;
15383
15486
  var DEFAULT_SEARCH_MODEL = "mistral-small-latest";
15384
15487
  var DEFAULT_SEARCH_TOOL = "web_search";
15385
15488
  var MAX_RESULTS = 20;
15386
15489
  var cachedConfig31 = null;
15387
15490
  function loadConfig30() {
15388
15491
  if (cachedConfig31) return cachedConfig31;
15389
- if (!existsSync37(CONFIG_PATH32)) {
15492
+ if (!existsSync38(CONFIG_PATH33)) {
15390
15493
  cachedConfig31 = {};
15391
15494
  return cachedConfig31;
15392
15495
  }
15393
- const raw = readFileSync38(CONFIG_PATH32, "utf-8");
15496
+ const raw = readFileSync39(CONFIG_PATH33, "utf-8");
15394
15497
  let parsed;
15395
15498
  try {
15396
15499
  parsed = JSON.parse(raw);
15397
15500
  } catch (err) {
15398
15501
  const message = err instanceof Error ? err.message : String(err);
15399
- throw new Error(`Failed to parse ${CONFIG_PATH32}: ${message}`);
15502
+ throw new Error(`Failed to parse ${CONFIG_PATH33}: ${message}`);
15400
15503
  }
15401
15504
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
15402
- throw new Error(`Invalid config in ${CONFIG_PATH32}: expected a JSON object`);
15505
+ throw new Error(`Invalid config in ${CONFIG_PATH33}: expected a JSON object`);
15403
15506
  }
15404
15507
  cachedConfig31 = parsed;
15405
15508
  return cachedConfig31;
@@ -15407,14 +15510,14 @@ function loadConfig30() {
15407
15510
  function resolveSearchModel(value) {
15408
15511
  if (value === void 0) return DEFAULT_SEARCH_MODEL;
15409
15512
  if (typeof value !== "string" || value.trim().length === 0) {
15410
- throw new Error(`mistralSearchModel in ${CONFIG_PATH32} must be a non-empty string`);
15513
+ throw new Error(`mistralSearchModel in ${CONFIG_PATH33} must be a non-empty string`);
15411
15514
  }
15412
15515
  return value.trim();
15413
15516
  }
15414
15517
  function resolveSearchTool(value) {
15415
15518
  if (value === void 0) return DEFAULT_SEARCH_TOOL;
15416
15519
  if (value !== "web_search" && value !== "web_search_premium") {
15417
- throw new Error(`mistralSearchTool in ${CONFIG_PATH32} must be either web_search or web_search_premium`);
15520
+ throw new Error(`mistralSearchTool in ${CONFIG_PATH33} must be either web_search or web_search_premium`);
15418
15521
  }
15419
15522
  return value;
15420
15523
  }
@@ -15528,7 +15631,7 @@ async function getApiKey25(signal) {
15528
15631
  if (!apiKey) {
15529
15632
  throw new Error(
15530
15633
  `Mistral API key not found. Either:
15531
- 1. Create ${CONFIG_PATH32} with { "mistralApiKey": "your-key" }
15634
+ 1. Create ${CONFIG_PATH33} with { "mistralApiKey": "your-key" }
15532
15635
  2. Set MISTRAL_API_KEY environment variable
15533
15636
  Get a key at https://console.mistral.ai/api-keys`
15534
15637
  );
@@ -15555,7 +15658,7 @@ async function searchWithMistral(query, options = {}) {
15555
15658
  const tool = resolveSearchTool(config.mistralSearchTool);
15556
15659
  const numResults = normalizeCount4(options.numResults);
15557
15660
  const apiKey = await getApiKey25(options.signal);
15558
- const requestSignal15 = options.signal ? AbortSignal.any([AbortSignal.timeout(SEARCH_TIMEOUT_MS27), options.signal]) : AbortSignal.timeout(SEARCH_TIMEOUT_MS27);
15661
+ const requestSignal15 = options.signal ? AbortSignal.any([AbortSignal.timeout(SEARCH_TIMEOUT_MS28), options.signal]) : AbortSignal.timeout(SEARCH_TIMEOUT_MS28);
15559
15662
  const activityId = activityMonitor.logStart({ type: "api", query });
15560
15663
  try {
15561
15664
  const response = await fetch(MISTRAL_CONVERSATIONS_URL, {
@@ -15602,7 +15705,7 @@ async function searchWithMistral(query, options = {}) {
15602
15705
 
15603
15706
  // gemini-search.ts
15604
15707
  init_utils();
15605
- var RESOLVED_SEARCH_PROVIDERS = ["openai", "brave", "parallel", "parallel-mcp", "tinyfish", "search1api", "searchinfinity", "querit", "tavily", "firecrawl", "jina", "searxng", "duckduckgo", "perplexity", "gemini", "kimi", "exa", "serpdive", "kagi", "ollama", "anysearch", "xai", "mistral", "brightdata", "serpbase", "serpapi", "serper", "serply", "valyu", "bocha", "xcrawl"];
15708
+ var RESOLVED_SEARCH_PROVIDERS = ["openai", "brave", "parallel", "parallel-mcp", "tinyfish", "search1api", "searchinfinity", "querit", "tavily", "firecrawl", "jina", "searxng", "duckduckgo", "perplexity", "gemini", "kimi", "exa", "serpdive", "kagi", "ollama", "anysearch", "xai", "mistral", "brightdata", "serpbase", "serpapi", "serper", "serply", "valyu", "bocha", "xcrawl", "baizhi"];
15606
15709
  var SEARCH_PROVIDERS = ["auto", "all", ...RESOLVED_SEARCH_PROVIDERS];
15607
15710
  var SearchProviderError = class extends Error {
15608
15711
  provider;
@@ -15618,18 +15721,18 @@ var SearchProviderError = class extends Error {
15618
15721
  this.causeError = cause;
15619
15722
  }
15620
15723
  };
15621
- var CONFIG_PATH33 = getWebSearchConfigPath();
15724
+ var CONFIG_PATH34 = getWebSearchConfigPath();
15622
15725
  var DEFAULT_SEARCH_MODEL2 = "gemini-3.6-flash";
15623
15726
  var ALL_SEARCH_PROVIDERS = ["searxng", "openai", "exa", "brave", "parallel", "tinyfish", "search1api", "searchinfinity", "querit", "tavily", "firecrawl", "jina", "serpdive", "kagi", "ollama", "perplexity", "gemini", "bocha"];
15624
15727
  var VALID_ROUTING_KINDS = ["transient", "quota", "network", "invalid-response", "unsupported"];
15625
15728
  var cachedSearchConfig = null;
15626
15729
  function getSearchConfig() {
15627
15730
  if (cachedSearchConfig) return cachedSearchConfig;
15628
- if (!existsSync38(CONFIG_PATH33)) {
15731
+ if (!existsSync39(CONFIG_PATH34)) {
15629
15732
  cachedSearchConfig = { searchProvider: "auto", searchProviderConfigured: false };
15630
15733
  return cachedSearchConfig;
15631
15734
  }
15632
- const rawText = readFileSync39(CONFIG_PATH33, "utf-8");
15735
+ const rawText = readFileSync40(CONFIG_PATH34, "utf-8");
15633
15736
  let raw;
15634
15737
  try {
15635
15738
  const parsed = JSON.parse(rawText);
@@ -15639,25 +15742,25 @@ function getSearchConfig() {
15639
15742
  raw = parsed;
15640
15743
  } catch (err) {
15641
15744
  const message = err instanceof Error ? err.message : String(err);
15642
- throw new Error(`Failed to parse ${CONFIG_PATH33}: ${message}`);
15745
+ throw new Error(`Failed to parse ${CONFIG_PATH34}: ${message}`);
15643
15746
  }
15644
15747
  const searchModel = normalizeSearchModel(raw.searchModel);
15645
15748
  const webSearch = raw.webSearch;
15646
15749
  if (webSearch !== void 0 && (!webSearch || typeof webSearch !== "object" || Array.isArray(webSearch))) {
15647
- throw new Error(`webSearch in ${CONFIG_PATH33} must be an object`);
15750
+ throw new Error(`webSearch in ${CONFIG_PATH34} must be an object`);
15648
15751
  }
15649
- const allowedProviders = webSearch && Object.hasOwn(webSearch, "allowedProviders") ? normalizeResolvedProviderList(webSearch.allowedProviders, `webSearch.allowedProviders in ${CONFIG_PATH33}`) : void 0;
15752
+ const allowedProviders = webSearch && Object.hasOwn(webSearch, "allowedProviders") ? normalizeResolvedProviderList(webSearch.allowedProviders, `webSearch.allowedProviders in ${CONFIG_PATH34}`) : void 0;
15650
15753
  const searchProviderConfigured = Object.hasOwn(raw, "searchProvider") || Object.hasOwn(raw, "provider");
15651
- const searchProvider = normalizeSearchProviderSelection(raw.searchProvider ?? raw.provider, `provider in ${CONFIG_PATH33}`);
15754
+ const searchProvider = normalizeSearchProviderSelection(raw.searchProvider ?? raw.provider, `provider in ${CONFIG_PATH34}`);
15652
15755
  const searchRouting = Object.hasOwn(raw, "searchRouting") ? normalizeSearchRouting(raw.searchRouting) : void 0;
15653
15756
  if (allowedProviders) {
15654
15757
  if (Object.hasOwn(raw, "searchProvider")) {
15655
- assertSearchProviderSelectionAllowed(normalizeSearchProviderSelection(raw.searchProvider), `searchProvider in ${CONFIG_PATH33}`, allowedProviders);
15758
+ assertSearchProviderSelectionAllowed(normalizeSearchProviderSelection(raw.searchProvider), `searchProvider in ${CONFIG_PATH34}`, allowedProviders);
15656
15759
  }
15657
15760
  if (Object.hasOwn(raw, "provider")) {
15658
- assertSearchProviderSelectionAllowed(normalizeSearchProviderSelection(raw.provider), `provider in ${CONFIG_PATH33}`, allowedProviders);
15761
+ assertSearchProviderSelectionAllowed(normalizeSearchProviderSelection(raw.provider), `provider in ${CONFIG_PATH34}`, allowedProviders);
15659
15762
  }
15660
- if (searchRouting) assertSearchProviderSelectionAllowed(searchRouting.providers, `searchRouting.providers in ${CONFIG_PATH33}`, allowedProviders);
15763
+ if (searchRouting) assertSearchProviderSelectionAllowed(searchRouting.providers, `searchRouting.providers in ${CONFIG_PATH34}`, allowedProviders);
15661
15764
  }
15662
15765
  cachedSearchConfig = {
15663
15766
  searchProvider,
@@ -15670,21 +15773,21 @@ function getSearchConfig() {
15670
15773
  }
15671
15774
  function normalizeSearchRouting(value) {
15672
15775
  if (!value || typeof value !== "object" || Array.isArray(value)) {
15673
- throw new Error(`searchRouting in ${CONFIG_PATH33} must be an object`);
15776
+ throw new Error(`searchRouting in ${CONFIG_PATH34} must be an object`);
15674
15777
  }
15675
15778
  const raw = value;
15676
- const providers = normalizeResolvedProviderList(raw.providers, `searchRouting.providers in ${CONFIG_PATH33}`);
15779
+ const providers = normalizeResolvedProviderList(raw.providers, `searchRouting.providers in ${CONFIG_PATH34}`);
15677
15780
  const useCurrentModel = raw.useCurrentModel;
15678
15781
  if (useCurrentModel !== void 0 && typeof useCurrentModel !== "boolean") {
15679
- throw new Error(`searchRouting.useCurrentModel in ${CONFIG_PATH33} must be a boolean`);
15782
+ throw new Error(`searchRouting.useCurrentModel in ${CONFIG_PATH34} must be a boolean`);
15680
15783
  }
15681
15784
  if (!Array.isArray(raw.fallbackOn) || raw.fallbackOn.length === 0) {
15682
- throw new Error(`searchRouting.fallbackOn in ${CONFIG_PATH33} must be a non-empty array`);
15785
+ throw new Error(`searchRouting.fallbackOn in ${CONFIG_PATH34} must be a non-empty array`);
15683
15786
  }
15684
15787
  const fallbackOn = [];
15685
15788
  for (const kind of raw.fallbackOn) {
15686
15789
  if (typeof kind !== "string" || !VALID_ROUTING_KINDS.includes(kind)) {
15687
- throw new Error(`searchRouting.fallbackOn in ${CONFIG_PATH33} may only contain transient, quota, network, invalid-response, or unsupported`);
15790
+ throw new Error(`searchRouting.fallbackOn in ${CONFIG_PATH34} may only contain transient, quota, network, invalid-response, or unsupported`);
15688
15791
  }
15689
15792
  if (!fallbackOn.includes(kind)) {
15690
15793
  fallbackOn.push(kind);
@@ -15744,18 +15847,10 @@ function errorMessage23(err) {
15744
15847
  function isAbortError2(err) {
15745
15848
  return errorMessage23(err).toLowerCase().includes("abort");
15746
15849
  }
15747
- function shouldTryOpenAIInAuto(options) {
15748
- if (options.recencyFilter) return false;
15749
- if (typeof options.numResults === "number" && Number.isFinite(options.numResults) && Math.floor(options.numResults) !== 5) {
15750
- return false;
15751
- }
15752
- return true;
15753
- }
15754
15850
  function isOpenAICodexSelected(ctx) {
15755
15851
  return ctx?.model?.provider === "openai-codex";
15756
15852
  }
15757
15853
  async function tryOpenAIInAuto(query, options, fallbackErrors) {
15758
- if (!shouldTryOpenAIInAuto(options)) return null;
15759
15854
  try {
15760
15855
  if (await isOpenAISearchAvailable(options.extensionContext)) {
15761
15856
  const result = await searchWithOpenAI(query, options, options.extensionContext);
@@ -15865,6 +15960,7 @@ async function searchWithResolvedProvider(provider, query, options, useCurrentMo
15865
15960
  if (provider === "serpapi") return { ...await searchWithSerpApi(query, options), provider };
15866
15961
  if (provider === "serper") return { ...await searchWithSerper(query, options), provider };
15867
15962
  if (provider === "serply") return { ...await searchWithSerply(query, options), provider };
15963
+ if (provider === "baizhi") return { ...await searchWithBaizhi(query, options), provider };
15868
15964
  if (provider === "valyu") return { ...await searchWithValyu(query, options), provider };
15869
15965
  if (provider === "xcrawl") return { ...await searchWithXCrawl(query, options), provider };
15870
15966
  if (provider === "perplexity") return { ...await searchWithPerplexity(query, options), provider };
@@ -15876,7 +15972,7 @@ async function searchWithResolvedProvider(provider, query, options, useCurrentMo
15876
15972
  if (result2) return { ...result2, provider };
15877
15973
  throw new Error(
15878
15974
  `Gemini search unavailable. Either:
15879
- 1. Configure geminiApiKey in ${CONFIG_PATH33} or set GEMINI_API_KEY
15975
+ 1. Configure geminiApiKey in ${CONFIG_PATH34} or set GEMINI_API_KEY
15880
15976
  2. Set GOOGLE_GEMINI_BASE_URL + CLOUDFLARE_API_KEY for Cloudflare AI Gateway routing
15881
15977
  3. Set geminiAuth to "adc" in web-search.json with a Google Cloud ADC + project/location
15882
15978
  4. Sign into gemini.google.com in a supported Chromium-based browser`
@@ -15912,6 +16008,7 @@ async function isResolvedProviderAvailable(provider, options, useCurrentModel =
15912
16008
  if (provider === "serpapi") return isSerpApiAvailable();
15913
16009
  if (provider === "serper") return isSerperAvailable();
15914
16010
  if (provider === "serply") return isSerplyAvailable();
16011
+ if (provider === "baizhi") return isBaizhiAvailable();
15915
16012
  if (provider === "valyu") return isValyuAvailable();
15916
16013
  if (provider === "xcrawl") return isXcrawlAvailable();
15917
16014
  if (provider === "perplexity") return isPerplexityAvailable();
@@ -15951,6 +16048,7 @@ function providerLabel(provider) {
15951
16048
  if (provider === "serpapi") return "SerpApi";
15952
16049
  if (provider === "serper") return "Serper";
15953
16050
  if (provider === "serply") return "Serply";
16051
+ if (provider === "baizhi") return "Baizhi";
15954
16052
  if (provider === "valyu") return "Valyu";
15955
16053
  return provider.charAt(0).toUpperCase() + provider.slice(1);
15956
16054
  }
@@ -16228,7 +16326,7 @@ async function search(query, options = {}) {
16228
16326
  throw new Error(
16229
16327
  `No search provider available. Either:
16230
16328
  1. Use /login to sign in with a Codex subscription for OpenAI web search
16231
- 2. Set openaiApiKey, braveApiKey, parallelApiKey, tinyfishApiKey, search1apiApiKey, searchinfinityApiKey, queritApiKey, tavilyApiKey, firecrawlBaseUrl, jinaApiKey, serpdiveApiKey, kagiApiKey, ollamaApiKey, searxngBaseUrl, perplexityApiKey, exaApiKey, geminiApiKey, bochaApiKey, or cloudflareApiKey in ${CONFIG_PATH33}
16329
+ 2. Set openaiApiKey, braveApiKey, parallelApiKey, tinyfishApiKey, search1apiApiKey, searchinfinityApiKey, queritApiKey, tavilyApiKey, firecrawlBaseUrl, jinaApiKey, serpdiveApiKey, kagiApiKey, ollamaApiKey, searxngBaseUrl, perplexityApiKey, exaApiKey, geminiApiKey, bochaApiKey, or cloudflareApiKey in ${CONFIG_PATH34}
16232
16330
  3. Set OPENAI_API_KEY, BRAVE_API_KEY, PARALLEL_API_KEY, TINYFISH_API_KEY, SEARCH1API_KEY, SEARCHINFINITY_API_KEY, QUERIT_API_KEY, TAVILY_API_KEY, FIRECRAWL_BASE_URL, JINA_API_KEY, SERPDIVE_API_KEY, KAGI_API_KEY, BOCHA_API_KEY, OLLAMA_API_KEY, SEARXNG_BASE_URL, EXA_API_KEY, PERPLEXITY_API_KEY, GEMINI_API_KEY, or CLOUDFLARE_API_KEY env vars
16233
16331
  4. Set GOOGLE_GEMINI_BASE_URL with CLOUDFLARE_API_KEY for Cloudflare AI Gateway routing
16234
16332
  5. Sign into gemini.google.com in a supported Chromium-based browser
@@ -16377,7 +16475,7 @@ init_utils();
16377
16475
 
16378
16476
  // storage.ts
16379
16477
  init_utils();
16380
- import { closeSync as closeSync2, constants, fchmodSync, fstatSync as fstatSync2, fsyncSync, lstatSync as lstatSync2, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as readFileSync40, readdirSync as readdirSync3, renameSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
16478
+ import { closeSync as closeSync2, constants, fchmodSync, fstatSync as fstatSync2, fsyncSync, lstatSync as lstatSync2, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as readFileSync41, readdirSync as readdirSync3, renameSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
16381
16479
  import { randomBytes } from "node:crypto";
16382
16480
  import { join as join6 } from "node:path";
16383
16481
  var CACHE_TTL_MS = 60 * 60 * 1e3;
@@ -16697,7 +16795,7 @@ function readCachedFetchData(data, now = Date.now()) {
16697
16795
  const opened = openRegularFile(path);
16698
16796
  fd = opened.fd;
16699
16797
  enforceFileMode(fd);
16700
- const parsed = JSON.parse(readFileSync40(fd, "utf8"));
16798
+ const parsed = JSON.parse(readFileSync41(fd, "utf8"));
16701
16799
  if (!isValidStoredData(parsed) || parsed.type !== "fetch" || parsed.id !== data.id || !isInlineFetchData(parsed)) {
16702
16800
  return unavailableFetchData(data, "Cached fetched content is invalid");
16703
16801
  }
@@ -16847,6 +16945,7 @@ function buildProviderButtons(available, selected, hasInitialQueries) {
16847
16945
  { value: "serpapi", label: "SerpApi", available: available.serpapi },
16848
16946
  { value: "serper", label: "Serper", available: available.serper },
16849
16947
  { value: "serply", label: "Serply", available: available.serply },
16948
+ { value: "baizhi", label: "Baizhi", available: available.baizhi },
16850
16949
  { value: "valyu", label: "Valyu", available: available.valyu }
16851
16950
  ];
16852
16951
  return providers.filter((p) => p.available).map((p) => {
@@ -18257,7 +18356,7 @@ var SCRIPT = `(function() {
18257
18356
  var token = DATA.sessionToken;
18258
18357
  var timeoutSec = DATA.timeout;
18259
18358
  var queries = Array.isArray(DATA.queries) ? DATA.queries : [];
18260
- var providers = ["auto", "all", "openai", "exa", "brave", "parallel", "parallel-mcp", "tinyfish", "search1api", "searchinfinity", "querit", "tavily", "firecrawl", "jina", "serpdive", "kagi", "bocha", "ollama", "searxng", "duckduckgo", "perplexity", "gemini", "kimi", "anysearch", "xcrawl", "xai", "mistral", "brightdata", "serpbase", "serpapi", "serper", "serply", "valyu"];
18359
+ var providers = ["auto", "all", "openai", "exa", "brave", "parallel", "parallel-mcp", "tinyfish", "search1api", "searchinfinity", "querit", "tavily", "firecrawl", "jina", "serpdive", "kagi", "bocha", "ollama", "searxng", "duckduckgo", "perplexity", "gemini", "kimi", "anysearch", "xcrawl", "xai", "mistral", "brightdata", "serpbase", "serpapi", "serper", "serply", "valyu", "baizhi"];
18261
18360
  var availProviders = DATA.availableProviders && typeof DATA.availableProviders === "object" ? DATA.availableProviders : {};
18262
18361
  var workflow = "summary-review";
18263
18362
  var initialDefaultProvider = typeof DATA.defaultProvider === "string" ? DATA.defaultProvider : "exa";
@@ -18492,6 +18591,7 @@ var SCRIPT = `(function() {
18492
18591
  if (provider === "serpapi") return "SerpApi";
18493
18592
  if (provider === "serper") return "Serper";
18494
18593
  if (provider === "serply") return "Serply";
18594
+ if (provider === "baizhi") return "Baizhi";
18495
18595
  if (provider === "valyu") return "Valyu";
18496
18596
  return "Unknown";
18497
18597
  }
@@ -20599,6 +20699,7 @@ function startCuratorServer(options, callbacks) {
20599
20699
  if (provider === "serpbase") return availableProviders.serpbase;
20600
20700
  if (provider === "serper") return availableProviders.serper;
20601
20701
  if (provider === "serply") return availableProviders.serply;
20702
+ if (provider === "baizhi") return availableProviders.baizhi;
20602
20703
  if (provider === "valyu") return availableProviders.valyu;
20603
20704
  return false;
20604
20705
  }
@@ -21378,8 +21479,8 @@ import { randomUUID as randomUUID3 } from "node:crypto";
21378
21479
  import { execFileSync as execFileSync3, spawn as spawn3 } from "node:child_process";
21379
21480
  import { createRequire } from "node:module";
21380
21481
  import { platform } from "node:os";
21381
- import { existsSync as existsSync48, readFileSync as readFileSync50, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "node:fs";
21382
- import { join as join9 } from "node:path";
21482
+ import { existsSync as existsSync49, readFileSync as readFileSync52, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "node:fs";
21483
+ import { join as join10 } from "node:path";
21383
21484
  init_gemini_api();
21384
21485
  init_gemini_web();
21385
21486
  init_gemini_web_config();
@@ -21659,9 +21760,118 @@ function getResearchArtifact(id) {
21659
21760
  return data.artifact;
21660
21761
  }
21661
21762
 
21763
+ // tool-activation.ts
21764
+ import { buildSessionContext } from "@earendil-works/pi-coding-agent";
21765
+ import { dirname as dirname2, join as join7 } from "node:path";
21766
+ import { readFileSync as readFileSync42 } from "node:fs";
21767
+ import { fileURLToPath } from "node:url";
21768
+ import { Type } from "typebox";
21769
+ var LOADER_NAME = "web_enable";
21770
+ var CAPABILITY_LABELS = {
21771
+ search: "web search",
21772
+ "source-check": "source checking",
21773
+ fetch: "content fetching",
21774
+ "stored-content": "stored-result retrieval"
21775
+ };
21776
+ function supportsDynamicTools(pi) {
21777
+ if (typeof pi.getAllTools !== "function" || typeof pi.getActiveTools !== "function" || typeof pi.setActiveTools !== "function") return false;
21778
+ try {
21779
+ const packagePath = join7(dirname2(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"))), "..", "package.json");
21780
+ const [major, minor, patch] = JSON.parse(readFileSync42(packagePath, "utf8")).version.split(".").map(Number);
21781
+ return major > 0 || minor > 86 || minor === 86 && patch >= 1;
21782
+ } catch {
21783
+ return false;
21784
+ }
21785
+ }
21786
+ function hasToolDeclarations(messages) {
21787
+ return messages.some((message) => message && typeof message === "object" && ("toolsAdded" in message || "toolsRemoved" in message));
21788
+ }
21789
+ async function currentTranscriptToolNames(messages) {
21790
+ const moduleName = "@earendil-works/pi-ai/utils/transcript";
21791
+ const { getCurrentTools } = await import(moduleName);
21792
+ return getCurrentTools(messages).map((tool) => tool.name);
21793
+ }
21794
+ function registerWebToolActivation(pi, tools) {
21795
+ if (tools.length === 0) return;
21796
+ if (!supportsDynamicTools(pi)) {
21797
+ console.warn("[pi-web-access] Dynamic tool activation requires Pi 0.86.1 or newer; web tools remain eagerly available.");
21798
+ return;
21799
+ }
21800
+ const names = tools.map((tool) => tool.name);
21801
+ const capabilities = tools.map((tool) => CAPABILITY_LABELS[tool.capability]).join(", ");
21802
+ const parameters = Type.Object({}, { additionalProperties: false });
21803
+ pi.registerTool({
21804
+ name: LOADER_NAME,
21805
+ label: "Enable Web Access",
21806
+ description: "Enable configured pi-web-access tools for web research and content retrieval. Does not search or fetch. Enabled tools are available on the next model request; disabled capabilities remain unavailable.",
21807
+ promptSnippet: `pi-web-access is configured for ${capabilities}. Call web_enable to activate these tools; use them on the next model request.`,
21808
+ parameters,
21809
+ async execute() {
21810
+ const registered = new Set(pi.getAllTools().map((tool) => tool.name));
21811
+ const unavailable = names.filter((name) => !registered.has(name));
21812
+ if (unavailable.length > 0) {
21813
+ return {
21814
+ isError: true,
21815
+ content: [{ type: "text", text: `Cannot enable unavailable tools: ${unavailable.join(", ")}.` }],
21816
+ details: { unavailable }
21817
+ };
21818
+ }
21819
+ try {
21820
+ pi.setActiveTools([.../* @__PURE__ */ new Set([...pi.getActiveTools(), ...names])]);
21821
+ } catch (error) {
21822
+ const message = error instanceof Error ? error.message : String(error);
21823
+ return {
21824
+ isError: true,
21825
+ content: [{ type: "text", text: `Activation failed: ${message}.` }],
21826
+ details: { error: message }
21827
+ };
21828
+ }
21829
+ const active = new Set(pi.getActiveTools());
21830
+ const missing = names.filter((name) => !active.has(name));
21831
+ return missing.length > 0 ? {
21832
+ isError: true,
21833
+ content: [{ type: "text", text: `Tools still inactive after activation: ${missing.join(", ")}.` }],
21834
+ details: { missing }
21835
+ } : {
21836
+ content: [{ type: "text", text: `Enabled: ${names.join(", ")}.` }],
21837
+ details: { enabled: names }
21838
+ };
21839
+ }
21840
+ });
21841
+ function loaderAvailable() {
21842
+ return pi.getAllTools().some((tool) => tool.name === LOADER_NAME);
21843
+ }
21844
+ let warned = false;
21845
+ async function selectFromSession(ctx) {
21846
+ if (!loaderAvailable()) return;
21847
+ try {
21848
+ const messages = buildSessionContext(ctx.sessionManager.getBranch()).messages;
21849
+ const recorded = hasToolDeclarations(messages) ? new Set(await currentTranscriptToolNames(messages)) : messages.length > 0 ? new Set(names) : /* @__PURE__ */ new Set();
21850
+ const heavy = new Set(names);
21851
+ const active = pi.getActiveTools().filter((name) => !heavy.has(name));
21852
+ for (const name of names) if (recorded.has(name)) active.push(name);
21853
+ pi.setActiveTools([.../* @__PURE__ */ new Set([...active, LOADER_NAME])]);
21854
+ } catch (error) {
21855
+ if (!warned) {
21856
+ warned = true;
21857
+ console.warn(`[pi-web-access] Keeping web tools eagerly available because activation setup failed: ${error instanceof Error ? error.message : String(error)}`);
21858
+ }
21859
+ }
21860
+ }
21861
+ pi.on("session_start", (_event, ctx) => selectFromSession(ctx));
21862
+ pi.on("session_tree", (_event, ctx) => selectFromSession(ctx));
21863
+ pi.on("before_agent_start", () => {
21864
+ if (!loaderAvailable() || pi.getActiveTools().includes(LOADER_NAME)) return;
21865
+ try {
21866
+ pi.setActiveTools([...pi.getActiveTools(), LOADER_NAME]);
21867
+ } catch {
21868
+ }
21869
+ });
21870
+ }
21871
+
21662
21872
  // index.ts
21663
21873
  function StringEnum(values, options) {
21664
- return Type.Unsafe({
21874
+ return Type2.Unsafe({
21665
21875
  type: "string",
21666
21876
  enum: values,
21667
21877
  ...options?.description && { description: options.description },
@@ -21712,17 +21922,17 @@ function parseConfigRoot(raw) {
21712
21922
  return parsed;
21713
21923
  }
21714
21924
  function loadConfig35() {
21715
- if (!existsSync48(WEB_SEARCH_CONFIG_PATH4)) return {};
21716
- return parseConfigRoot(readFileSync50(WEB_SEARCH_CONFIG_PATH4, "utf-8"));
21925
+ if (!existsSync49(WEB_SEARCH_CONFIG_PATH4)) return {};
21926
+ return parseConfigRoot(readFileSync52(WEB_SEARCH_CONFIG_PATH4, "utf-8"));
21717
21927
  }
21718
21928
  function saveConfig(updates) {
21719
21929
  let config = {};
21720
- if (existsSync48(WEB_SEARCH_CONFIG_PATH4)) {
21721
- config = parseConfigRoot(readFileSync50(WEB_SEARCH_CONFIG_PATH4, "utf-8"));
21930
+ if (existsSync49(WEB_SEARCH_CONFIG_PATH4)) {
21931
+ config = parseConfigRoot(readFileSync52(WEB_SEARCH_CONFIG_PATH4, "utf-8"));
21722
21932
  }
21723
21933
  Object.assign(config, updates);
21724
21934
  const dir = getWebSearchConfigDir();
21725
- if (!existsSync48(dir)) mkdirSync3(dir, { recursive: true });
21935
+ if (!existsSync49(dir)) mkdirSync3(dir, { recursive: true });
21726
21936
  writeFileSync3(WEB_SEARCH_CONFIG_PATH4, JSON.stringify(config, null, 2) + "\n");
21727
21937
  }
21728
21938
  var DEFAULT_TOOL_NAMES = {
@@ -21771,9 +21981,9 @@ function curatorResultIndexCapacity(queryCount) {
21771
21981
  return queryCount * RESOLVED_SEARCH_PROVIDERS.length;
21772
21982
  }
21773
21983
  function searchProviderSchema(description, allowedProviders) {
21774
- return Type.Union([
21984
+ return Type2.Union([
21775
21985
  StringEnum(["auto", "all", ...allowedProviders]),
21776
- Type.Array(StringEnum([...allowedProviders]), { minItems: 1 })
21986
+ Type2.Array(StringEnum([...allowedProviders]), { minItems: 1 })
21777
21987
  ], { description });
21778
21988
  }
21779
21989
  function isToolEnabled(config, key) {
@@ -21809,6 +22019,7 @@ function resolveToolNames(config) {
21809
22019
  const seen = /* @__PURE__ */ new Map();
21810
22020
  for (const key of registeredKeys) {
21811
22021
  const name = names[key];
22022
+ if (name === "web_enable") throw new Error(`toolNames.${key} in ${WEB_SEARCH_CONFIG_PATH4} uses reserved loader name web_enable`);
21812
22023
  const previous = seen.get(name);
21813
22024
  if (previous) throw new Error(`toolNames.${key} duplicates toolNames.${previous} in ${WEB_SEARCH_CONFIG_PATH4}`);
21814
22025
  seen.set(name, key);
@@ -21933,6 +22144,7 @@ async function getProviderAvailability(ctx) {
21933
22144
  serpapi: allowedProviders.has("serpapi") && isSerpApiAvailable(),
21934
22145
  serper: allowedProviders.has("serper") && isSerperAvailable(),
21935
22146
  serply: allowedProviders.has("serply") && isSerplyAvailable(),
22147
+ baizhi: allowedProviders.has("baizhi") && isBaizhiAvailable(),
21936
22148
  valyu: allowedProviders.has("valyu") && isValyuAvailable()
21937
22149
  };
21938
22150
  return {
@@ -22011,10 +22223,11 @@ var pendingCurates = /* @__PURE__ */ new Map();
22011
22223
  var activeCurators = /* @__PURE__ */ new Map();
22012
22224
  var glimpseWins = /* @__PURE__ */ new Map();
22013
22225
  var DEFAULT_MAX_INLINE_CONTENT_CHARS = 3e4;
22226
+ var MIN_INLINE_CONTENT_CHARS = 1e3;
22014
22227
  var MAX_INLINE_CONTENT_CHARS = 2e5;
22015
22228
  function getMaxInlineContentChars(config = loadConfig35()) {
22016
22229
  const value = config.maxInlineContentChars;
22017
- if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
22230
+ if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value < MIN_INLINE_CONTENT_CHARS) {
22018
22231
  return DEFAULT_MAX_INLINE_CONTENT_CHARS;
22019
22232
  }
22020
22233
  return Math.min(value, MAX_INLINE_CONTENT_CHARS);
@@ -22137,6 +22350,10 @@ function hasFullInlineCoverage(urls, inlineContent) {
22137
22350
  function formatFullResults(queryData) {
22138
22351
  let output = `## Results for: "${queryData.query}"
22139
22352
 
22353
+ `;
22354
+ const providers = queryData.providers ?? (queryData.provider ? [queryData.provider] : []);
22355
+ if (providers.length > 0) output += `**Provider${providers.length === 1 ? "" : "s"}:** ${providers.join(", ")}
22356
+
22140
22357
  `;
22141
22358
  if (queryData.answer) {
22142
22359
  output += `${queryData.answer}
@@ -22147,12 +22364,33 @@ function formatFullResults(queryData) {
22147
22364
  }
22148
22365
  for (const r of queryData.results) {
22149
22366
  output += `### ${r.title}
22150
- ${r.url}
22367
+ ${r.url}${r.snippet ? `
22368
+
22369
+ ${r.snippet}` : ""}
22151
22370
 
22152
22371
  `;
22153
22372
  }
22154
22373
  return output;
22155
22374
  }
22375
+ function boundSearchPresentation(text2, guidance, truncationGuidance, maxChars) {
22376
+ const fullText = `${text2}${guidance}`;
22377
+ if (fullText.length <= maxChars) {
22378
+ return { text: fullText, truncated: false, originalChars: text2.length, returnedChars: text2.length, omittedChars: 0 };
22379
+ }
22380
+ const marker = `
22381
+
22382
+ ---
22383
+ [Output truncated.]${truncationGuidance}`;
22384
+ const prefixLength = maxChars - marker.length;
22385
+ const bounded = `${text2.slice(0, prefixLength)}${marker}`;
22386
+ return {
22387
+ text: bounded,
22388
+ truncated: true,
22389
+ originalChars: text2.length,
22390
+ returnedChars: prefixLength,
22391
+ omittedChars: text2.length - prefixLength
22392
+ };
22393
+ }
22156
22394
  function abortPendingFetches() {
22157
22395
  for (const controller of pendingFetches.values()) {
22158
22396
  controller.abort();
@@ -22232,8 +22470,8 @@ function findGlimpseMjs() {
22232
22470
  }
22233
22471
  try {
22234
22472
  const globalRoot = execFileSync3("npm", ["root", "-g"], { encoding: "utf-8" }).trim();
22235
- const entry = join9(globalRoot, "glimpseui", "src", "glimpse.mjs");
22236
- if (existsSync48(entry)) return entry;
22473
+ const entry = join10(globalRoot, "glimpseui", "src", "glimpse.mjs");
22474
+ if (existsSync49(entry)) return entry;
22237
22475
  } catch {
22238
22476
  }
22239
22477
  return null;
@@ -22627,6 +22865,7 @@ function index_default(pi) {
22627
22865
  const sc = opts.results.filter((r) => !r.error).length;
22628
22866
  const tr = opts.results.reduce((sum, r) => sum + r.results.length, 0);
22629
22867
  const hasApprovedSummary = typeof opts.approvedSummary === "string" && opts.approvedSummary.trim().length > 0;
22868
+ const maxInlineContentChars = getMaxInlineContentChars(initConfig);
22630
22869
  let output = "";
22631
22870
  if (hasApprovedSummary) {
22632
22871
  output = opts.approvedSummary.trim();
@@ -22634,6 +22873,15 @@ function index_default(pi) {
22634
22873
  if (opts.curated) {
22635
22874
  output += "[These results were manually curated by the user in the browser. Use them as-is \u2014 do not re-search or discard.]\n\n";
22636
22875
  }
22876
+ const providerNames = opts.results.map((result) => {
22877
+ const providers = result.providers ?? (result.provider ? [result.provider] : []);
22878
+ return providers.join(", ") || "unknown";
22879
+ });
22880
+ output += opts.results.length === 1 ? `**Provider:** ${providerNames[0]}
22881
+
22882
+ ` : `**Providers used:** ${providerNames.map((name, index) => `Query ${index + 1}: ${name}`).join("; ")}
22883
+
22884
+ `;
22637
22885
  const duplicateQueries = opts.curated ? duplicateQuerySet(opts.results) : /* @__PURE__ */ new Set();
22638
22886
  for (const { query, answer, results, error, provider } of opts.results) {
22639
22887
  if (opts.queryList.length > 1) {
@@ -22658,26 +22906,35 @@ function index_default(pi) {
22658
22906
  urls: opts.inlineContent
22659
22907
  };
22660
22908
  pi.appendEntry("web-search-results", storeFetchedContentResult(fetchId, data));
22661
- if (!hasApprovedSummary) {
22662
- output += `---
22663
- Full content for ${opts.inlineContent.length} sources available [${fetchId}].`;
22664
- }
22665
22909
  } else if (opts.includeContent) {
22666
22910
  fetchId = startBackgroundFetch(opts.urls, opts.proxy);
22667
- if (fetchId && !hasApprovedSummary) {
22668
- output += `---
22669
- Content fetching in background [${fetchId}]. Will notify when ready.`;
22670
- }
22671
22911
  }
22672
22912
  const searchId = storeAndPublishSearch(opts.results);
22673
22913
  const isBackgroundFetch = fetchId !== null && !hasInlineReady;
22674
- if (getSearchContentEnabled && !hasApprovedSummary) {
22675
- output += `
22914
+ const unboundedPresentation = output.trim();
22915
+ const buildGuidance = (forTruncation) => {
22916
+ let value = "";
22917
+ if (hasInlineReady && opts.inlineContent && fetchId) {
22918
+ value += `
22676
22919
  ---
22677
- Results stored as responseId "${searchId}". Use ${toolNames.getSearchContent}({ responseId: "${searchId}", queryIndex: 0 }) to retrieve them.`;
22678
- }
22920
+ Full content for ${opts.inlineContent.length} sources is ready as responseId "${fetchId}". `;
22921
+ value += getSearchContentEnabled ? `Use ${toolNames.getSearchContent}({ responseId: "${fetchId}", urlIndex: 0, offset: 0, limit: ${maxInlineContentChars} }) to retrieve the first bounded page.` : forTruncation ? `Enable ${toolNames.getSearchContent} to retrieve the full stored content.` : "";
22922
+ } else if (isBackgroundFetch && fetchId) {
22923
+ value += `
22924
+ ---
22925
+ Content fetching in background as responseId "${fetchId}". Will notify when ready.`;
22926
+ }
22927
+ if (getSearchContentEnabled || forTruncation) {
22928
+ value += `
22929
+ ---
22930
+ Full search results are stored as responseId "${searchId}". `;
22931
+ value += getSearchContentEnabled ? `Use ${toolNames.getSearchContent}({ responseId: "${searchId}", queryIndex: 0, offset: 0, limit: ${maxInlineContentChars} }) to retrieve the first bounded page${opts.results.length > 1 ? `; repeat with queryIndex 1 through ${opts.results.length - 1}` : ""}.` : `Enable ${toolNames.getSearchContent} to retrieve the full stored results.`;
22932
+ }
22933
+ return value;
22934
+ };
22935
+ const presentation = hasApprovedSummary ? { text: unboundedPresentation, truncated: false, originalChars: unboundedPresentation.length, returnedChars: unboundedPresentation.length, omittedChars: 0 } : boundSearchPresentation(unboundedPresentation, buildGuidance(false), buildGuidance(true), maxInlineContentChars);
22679
22936
  return {
22680
- content: [{ type: "text", text: output.trim() }],
22937
+ content: [{ type: "text", text: presentation.text }],
22681
22938
  details: {
22682
22939
  queries: opts.queryList,
22683
22940
  queryCount: opts.queryList.length,
@@ -22687,6 +22944,14 @@ Results stored as responseId "${searchId}". Use ${toolNames.getSearchContent}({
22687
22944
  fetchId,
22688
22945
  fetchUrls: isBackgroundFetch ? opts.urls : void 0,
22689
22946
  searchId,
22947
+ queryProviders: opts.results.map((result) => ({
22948
+ query: result.query,
22949
+ providers: result.providers ?? (result.provider ? [result.provider] : [])
22950
+ })),
22951
+ truncated: presentation.truncated,
22952
+ originalChars: presentation.originalChars,
22953
+ returnedChars: presentation.returnedChars,
22954
+ omittedChars: presentation.omittedChars,
22690
22955
  ...opts.curated ? {
22691
22956
  curated: true,
22692
22957
  curatedFrom: opts.curatedFrom,
@@ -23011,24 +23276,24 @@ Open manually: ${handle.url}` }],
23011
23276
  if (webSearchEnabled) pi.registerTool({
23012
23277
  name: toolNames.webSearch,
23013
23278
  label: "Web Search",
23014
- description: `Search the web with ${allowedSearchProviders.map(providerLabel).join(", ")}. Provider arrays run simultaneously; ${allPolicyDescription}. By default, returns source-linked search results or provider answers. For comprehensive research, prefer queries (plural) with 2-4 varied angles over a single query. When includeContent is true, full page content is fetched in the background. Searches return without the interactive browser curator by default; set workflow to "summary-review" to open the curator with an auto-generated summary draft or "auto-summary" to generate a summary without the browser curator. The configured provider is used when provider is omitted or set to auto; omit provider unless explicitly overriding it.`,
23279
+ description: `Search the web with ${allowedSearchProviders.map(providerLabel).join(", ")}. Provider arrays run simultaneously; ${allPolicyDescription}. The default workflow is none: it returns bounded source-linked search results or provider answers without a curator or generated summary, identifies the providers used, and stores full results for retrieval by responseId. For comprehensive research, prefer queries (plural) with 2-4 varied angles over a single query. When includeContent is true, full page content is fetched in the background. Set workflow to "summary-review" to open the curator with an auto-generated summary draft or "auto-summary" to generate a summary without the browser curator. The configured provider is used when provider is omitted or set to auto; omit provider unless explicitly overriding it.`,
23015
23280
  promptSnippet: "Use for web research questions. Prefer {queries:[...]} with 2-4 varied angles over a single query for broader coverage. Omit provider unless explicitly overriding the configured default.",
23016
- parameters: Type.Object({
23017
- query: Type.Optional(Type.String({ description: "Single search query. For research tasks, prefer 'queries' with multiple varied angles instead." })),
23018
- queries: Type.Optional(Type.Array(Type.String(), { description: "Multiple queries searched concurrently (up to three at a time), each returning source-linked search results or a provider answer. Prefer this for research \u2014 vary phrasing, scope, and angle across 2-4 queries to maximize coverage. Good: ['React vs Vue performance benchmarks 2026', 'React vs Vue developer experience comparison', 'React ecosystem size vs Vue ecosystem']. Bad: ['React vs Vue', 'React vs Vue comparison', 'React vs Vue review'] (too similar, redundant results)." })),
23019
- numResults: Type.Optional(Type.Integer({ minimum: 1, maximum: 20, description: "Results per query (default: 5, max: 20)" })),
23020
- includeContent: Type.Optional(Type.Boolean({ description: "Fetch full page content (async)" })),
23021
- recencyFilter: Type.Optional(
23281
+ parameters: Type2.Object({
23282
+ query: Type2.Optional(Type2.String({ description: "Single search query. For research tasks, prefer 'queries' with multiple varied angles instead." })),
23283
+ queries: Type2.Optional(Type2.Array(Type2.String(), { description: "Multiple queries searched concurrently (up to three at a time), each returning source-linked search results or a provider answer. Prefer this for research \u2014 vary phrasing, scope, and angle across 2-4 queries to maximize coverage. Good: ['React vs Vue performance benchmarks 2026', 'React vs Vue developer experience comparison', 'React ecosystem size vs Vue ecosystem']. Bad: ['React vs Vue', 'React vs Vue comparison', 'React vs Vue review'] (too similar, redundant results)." })),
23284
+ numResults: Type2.Optional(Type2.Integer({ minimum: 1, maximum: 20, description: "Results per query (default: 5, max: 20)" })),
23285
+ includeContent: Type2.Optional(Type2.Boolean({ description: "Fetch full page content (async)" })),
23286
+ recencyFilter: Type2.Optional(
23022
23287
  StringEnum(["day", "week", "month", "year"], { description: "Filter by recency" })
23023
23288
  ),
23024
- domainFilter: Type.Optional(Type.Array(Type.String(), { description: "Limit to domains (prefix with - to exclude)" })),
23025
- provider: Type.Optional(searchProviderSchema(`Search provider or non-empty list of allowed providers to search simultaneously; ${allPolicyDescription}; omit this field to use the configured provider, or use auto when none is configured`, allowedSearchProviders)),
23026
- workflow: Type.Optional(
23289
+ domainFilter: Type2.Optional(Type2.Array(Type2.String(), { description: "Limit to domains (prefix with - to exclude)" })),
23290
+ provider: Type2.Optional(searchProviderSchema(`Search provider or non-empty list of allowed providers to search simultaneously; ${allPolicyDescription}; omit this field to use the configured provider, or use auto when none is configured`, allowedSearchProviders)),
23291
+ workflow: Type2.Optional(
23027
23292
  StringEnum(["none", "summary-review", "auto-summary"], {
23028
23293
  description: "Search workflow mode: none = no curator (default), summary-review = open curator with auto summary draft, auto-summary = generate summary without opening curator"
23029
23294
  })
23030
23295
  ),
23031
- proxy: Type.Optional(Type.String({
23296
+ proxy: Type2.Optional(Type2.String({
23032
23297
  description: "http(s) or socks proxy URL (e.g. http://host:port or socks5h://host:port) used for every outbound request in this call (search APIs and content fetches). Node fetch ignores HTTP(S)_PROXY env vars, so set this (or `proxy` in web-search.json) when direct access is blocked; empty string forces direct access."
23033
23298
  }))
23034
23299
  }),
@@ -23245,7 +23510,7 @@ Open manually: ${handle.url}` }],
23245
23510
  details: { phase: "search", progress: completedSearches / queryList.length, currentQuery: query }
23246
23511
  });
23247
23512
  try {
23248
- const { answer, results, inlineContent, provider } = await search(query, {
23513
+ const { answer, results, inlineContent, provider, providerResponses } = await search(query, {
23249
23514
  provider: resolvedProvider,
23250
23515
  numResults: params.numResults,
23251
23516
  recencyFilter,
@@ -23254,7 +23519,8 @@ Open manually: ${handle.url}` }],
23254
23519
  signal,
23255
23520
  extensionContext: ctx
23256
23521
  });
23257
- return { result: { query, answer, results, error: null, provider }, inlineContent };
23522
+ const providers = providerResponses?.map((response) => response.provider) ?? [provider];
23523
+ return { result: { query, answer, results, error: null, provider, providers }, inlineContent };
23258
23524
  } catch (err) {
23259
23525
  if (signal?.aborted || isAbortError7(err)) throw err;
23260
23526
  const message = err instanceof Error ? err.message : String(err);
@@ -23524,15 +23790,15 @@ Open manually: ${handle.url}` }],
23524
23790
  label: "Source Check",
23525
23791
  description: "Gather web sources for a claim and return a bounded machine-readable research artifact with exact passage citations for manual review.",
23526
23792
  promptSnippet: "Gather structured source evidence and passage-level citations for manual semantic review of a claim.",
23527
- parameters: Type.Object({
23528
- claim: Type.String({ description: "The assertion to gather web sources for." }),
23529
- queries: Type.Optional(Type.Array(Type.String(), { description: "Search queries (default: the claim)." })),
23530
- numResults: Type.Optional(Type.Integer({ minimum: 1, maximum: 20, description: "Results per query (default: 5, max: 20)." })),
23531
- fetchContent: Type.Optional(Type.Boolean({ description: "Fetch up to 5 result pages for exact passage extraction." })),
23532
- recencyFilter: Type.Optional(StringEnum(["day", "week", "month", "year"], { description: "Filter by recency." })),
23533
- domainFilter: Type.Optional(Type.Array(Type.String(), { description: "Limit to domains; prefix with - to exclude." })),
23534
- provider: Type.Optional(searchProviderSchema(`Search provider or non-empty list of allowed providers to search simultaneously; ${allPolicyDescription}`, allowedSearchProviders)),
23535
- proxy: Type.Optional(Type.String({
23793
+ parameters: Type2.Object({
23794
+ claim: Type2.String({ description: "The assertion to gather web sources for." }),
23795
+ queries: Type2.Optional(Type2.Array(Type2.String(), { description: "Search queries (default: the claim)." })),
23796
+ numResults: Type2.Optional(Type2.Integer({ minimum: 1, maximum: 20, description: "Results per query (default: 5, max: 20)." })),
23797
+ fetchContent: Type2.Optional(Type2.Boolean({ description: "Fetch up to 5 result pages for exact passage extraction." })),
23798
+ recencyFilter: Type2.Optional(StringEnum(["day", "week", "month", "year"], { description: "Filter by recency." })),
23799
+ domainFilter: Type2.Optional(Type2.Array(Type2.String(), { description: "Limit to domains; prefix with - to exclude." })),
23800
+ provider: Type2.Optional(searchProviderSchema(`Search provider or non-empty list of allowed providers to search simultaneously; ${allPolicyDescription}`, allowedSearchProviders)),
23801
+ proxy: Type2.Optional(Type2.String({
23536
23802
  description: "http(s) or socks proxy URL (e.g. http://host:port or socks5h://host:port) used for every outbound request in this call (search APIs and result-page fetches). Empty string forces direct access."
23537
23803
  }))
23538
23804
  }),
@@ -23613,38 +23879,38 @@ Open manually: ${handle.url}` }],
23613
23879
  label: "Fetch Content",
23614
23880
  description: `Fetch URL(s). Available modes: ${fetchModeDescription}. Direct image URLs return resized image content when supported by the selected mode. Supports YouTube transcripts, GitHub repositories, PDFs, and local videos when supported by the selected mode. ${fetchContentStorageNote}`,
23615
23881
  promptSnippet: "Use to fetch URL content, direct images, GitHub repos, and videos.",
23616
- parameters: Type.Object({
23617
- url: Type.Optional(Type.String({ description: "Single URL to fetch" })),
23618
- urls: Type.Optional(Type.Array(Type.String(), { description: "Multiple URLs (parallel)" })),
23619
- forceClone: Type.Optional(Type.Boolean({
23882
+ parameters: Type2.Object({
23883
+ url: Type2.Optional(Type2.String({ description: "Single URL to fetch" })),
23884
+ urls: Type2.Optional(Type2.Array(Type2.String(), { description: "Multiple URLs (parallel)" })),
23885
+ forceClone: Type2.Optional(Type2.Boolean({
23620
23886
  description: "Force cloning large GitHub repositories that exceed the size threshold"
23621
23887
  })),
23622
- prompt: Type.Optional(Type.String({
23888
+ prompt: Type2.Optional(Type2.String({
23623
23889
  description: fetchModeConfig.allowedModes.includes("answer") ? "Question or instruction for video analysis, or the page-local question required by answer mode." : "Question or instruction for video analysis."
23624
23890
  })),
23625
- mode: Type.Optional(StringEnum(fetchModeConfig.allowedModes, {
23891
+ mode: Type2.Optional(StringEnum(fetchModeConfig.allowedModes, {
23626
23892
  description: `Fetch mode. ${fetchModeDescription}.`
23627
23893
  })),
23628
23894
  ...fetchModeConfig.allowedModes.includes("answer") ? {
23629
- answerModel: Type.Optional(Type.String({
23895
+ answerModel: Type2.Optional(Type2.String({
23630
23896
  description: "Optional provider/model-id override for answer mode. Defaults to fetch.answerProvider + fetch.answerModel when configured, otherwise the current Pi model."
23631
23897
  }))
23632
23898
  } : {},
23633
- timestamp: Type.Optional(Type.String({
23899
+ timestamp: Type2.Optional(Type2.String({
23634
23900
  description: "Extract video frame(s) at a timestamp or time range. Single: '1:23:45', '23:45', or '85' (seconds). Range: '23:41-25:00' extracts evenly-spaced frames across that span (default 6). Use frames with ranges to control density; single+frames uses a fixed 5s interval. YouTube requires yt-dlp + ffmpeg; local videos require ffmpeg. Use a range when you know the approximate area but not the exact moment \u2014 you'll get a contact sheet to visually identify the right frame."
23635
23901
  })),
23636
- frames: Type.Optional(Type.Integer({
23902
+ frames: Type2.Optional(Type2.Integer({
23637
23903
  minimum: 1,
23638
23904
  maximum: 12,
23639
23905
  description: "Number of frames to extract. Use with timestamp range for custom density, with single timestamp to get N frames at 5s intervals, or alone to sample across the entire video. Requires yt-dlp + ffmpeg for YouTube, ffmpeg for local video."
23640
23906
  })),
23641
- model: Type.Optional(Type.String({
23907
+ model: Type2.Optional(Type2.String({
23642
23908
  description: "Override the Gemini model for video/YouTube analysis (e.g. 'gemini-3.6-flash'). Defaults to config or gemini-3.6-flash."
23643
23909
  })),
23644
- auth: Type.Optional(Type.Union([Type.String(), Type.Boolean()], {
23910
+ auth: Type2.Optional(Type2.Union([Type2.String(), Type2.Boolean()], {
23645
23911
  description: "Opt into an authFetch profile for local browser-cookie fetching. Use a profile name, or true only when exactly one profile exists."
23646
23912
  })),
23647
- proxy: Type.Optional(Type.String({
23913
+ proxy: Type2.Optional(Type2.String({
23648
23914
  description: "http(s) or socks proxy URL (e.g. http://host:port or socks5h://host:port) used for this fetch. Needed when the target is unreachable directly; localhost and NO_PROXY hosts always bypass the proxy. Empty string forces direct access."
23649
23915
  }))
23650
23916
  }),
@@ -23924,21 +24190,21 @@ Use ${toolNames.getSearchContent}({ responseId: "${responseId}", urlIndex: 0 })
23924
24190
  pi.registerTool({
23925
24191
  name: toolNames.getSearchContent,
23926
24192
  label: "Get Search Content",
23927
- description: `Retrieve bounded content slices or find matching passages in a previous ${storedContentSources} call.`,
24193
+ description: `Retrieve bounded pages of full stored search results or fetched content, or find matching passages, from a previous ${storedContentSources} call.`,
23928
24194
  promptSnippet: `Use after ${storedContentSources} to retrieve stored content via responseId. Use findText to locate passages without paging through the full content.`,
23929
- parameters: Type.Object({
23930
- responseId: Type.String({ description: `The responseId from ${storedContentSources}` }),
23931
- query: Type.Optional(Type.String({ description: searchQueryDescription })),
23932
- queryIndex: Type.Optional(Type.Integer({ minimum: 0, description: "Get content for query at index" })),
23933
- url: Type.Optional(Type.String({ description: "Get content for this URL" })),
23934
- urlIndex: Type.Optional(Type.Integer({ minimum: 0, description: "Get content for URL at index" })),
23935
- offset: Type.Optional(Type.Integer({ minimum: 0, description: "Character offset for fetched URL content slices (default 0). Ignored when findText is supplied." })),
23936
- limit: Type.Optional(Type.Integer({ minimum: 1, maximum: maxInlineContentChars, description: "Maximum characters to return for fetched URL content slices (default and max are set by maxInlineContentChars). Ignored when findText is supplied." })),
23937
- findText: Type.Optional(Type.Union([
23938
- Type.String({ minLength: 1, maxLength: 500 }),
23939
- Type.Array(Type.String({ minLength: 1, maxLength: 500 }), { minItems: 1, maxItems: 10 })
24195
+ parameters: Type2.Object({
24196
+ responseId: Type2.String({ description: `The responseId from ${storedContentSources}` }),
24197
+ query: Type2.Optional(Type2.String({ description: searchQueryDescription })),
24198
+ queryIndex: Type2.Optional(Type2.Integer({ minimum: 0, description: "Get content for query at index" })),
24199
+ url: Type2.Optional(Type2.String({ description: "Get content for this URL" })),
24200
+ urlIndex: Type2.Optional(Type2.Integer({ minimum: 0, description: "Get content for URL at index" })),
24201
+ offset: Type2.Optional(Type2.Integer({ minimum: 0, description: "Character offset in stored search or fetched URL content (default 0). Ignored when findText is supplied." })),
24202
+ limit: Type2.Optional(Type2.Integer({ minimum: 1, maximum: maxInlineContentChars, description: "Requested maximum stored-content characters (default and max use maxInlineContentChars). Search-page continuation guidance shares the global output cap and may reduce returnedChars. Ignored when findText is supplied." })),
24203
+ findText: Type2.Optional(Type2.Union([
24204
+ Type2.String({ minLength: 1, maxLength: 500 }),
24205
+ Type2.Array(Type2.String({ minLength: 1, maxLength: 500 }), { minItems: 1, maxItems: 10 })
23940
24206
  ], { description: "Text or texts to find in the selected stored content. When supplied, offset and limit are ignored." })),
23941
- findMode: Type.Optional(StringEnum(["exact", "case-insensitive", "fuzzy"], { description: "Matching mode for findText (default: case-insensitive). Requires findText." }))
24207
+ findMode: Type2.Optional(StringEnum(["exact", "case-insensitive", "fuzzy"], { description: "Matching mode for findText (default: case-insensitive). Requires findText." }))
23942
24208
  }),
23943
24209
  async execute(_toolCallId, rawParams) {
23944
24210
  const params = normalizeGetSearchContentParams(rawParams);
@@ -24045,10 +24311,10 @@ Use ${toolNames.getSearchContent}({ responseId: "${responseId}", urlIndex: 0 })
24045
24311
  if (params.findText !== void 0) {
24046
24312
  try {
24047
24313
  const found = findContent(fullResults, normalizeFindQueries(params.findText), params.findMode ?? "case-insensitive");
24048
- const { text: text2, ...findDetails } = found;
24314
+ const { text: text3, ...findDetails } = found;
24049
24315
  return {
24050
- content: [{ type: "text", text: text2 }],
24051
- details: { query: queryData.query, resultCount: queryData.results.length, findMode: params.findMode ?? "case-insensitive", ...findDetails }
24316
+ content: [{ type: "text", text: text3 }],
24317
+ details: { responseId: params.responseId, query: queryData.query, resultCount: queryData.results.length, contentLength: fullResults.length, findMode: params.findMode ?? "case-insensitive", ...findDetails }
24052
24318
  };
24053
24319
  } catch (err) {
24054
24320
  const error = err instanceof Error ? err.message : String(err);
@@ -24058,9 +24324,56 @@ Use ${toolNames.getSearchContent}({ responseId: "${responseId}", urlIndex: 0 })
24058
24324
  };
24059
24325
  }
24060
24326
  }
24327
+ const offset = params.offset ?? 0;
24328
+ const limit = params.limit ?? maxInlineContentChars;
24329
+ if (!Number.isInteger(offset) || offset < 0) {
24330
+ return {
24331
+ content: [{ type: "text", text: `Invalid offset: received ${formatInputValue(offset)} for query ${formatInputValue(queryData.query)}; offset must be a non-negative integer. Use 0 or a larger integer.` }],
24332
+ details: { error: "Invalid offset", offset }
24333
+ };
24334
+ }
24335
+ if (!Number.isInteger(limit) || limit <= 0 || limit > maxInlineContentChars) {
24336
+ return {
24337
+ content: [{ type: "text", text: `Invalid limit: received ${formatInputValue(limit)} for query ${formatInputValue(queryData.query)}; limit must be an integer from 1 to ${maxInlineContentChars}. Use a value in that range.` }],
24338
+ details: { error: "Invalid limit", limit, maxLimit: maxInlineContentChars }
24339
+ };
24340
+ }
24341
+ if (offset > fullResults.length) {
24342
+ return {
24343
+ content: [{ type: "text", text: `Offset ${offset} is out of range for query ${formatInputValue(queryData.query)} in responseId ${formatInputValue(params.responseId)}. Received offset ${offset}; valid range is 0-${fullResults.length}. Use an offset within that range.` }],
24344
+ details: { error: "Offset out of range", offset, contentLength: fullResults.length }
24345
+ };
24346
+ }
24347
+ const queryIndex = data.queries.indexOf(queryData);
24348
+ let returnedChars = Math.min(limit, fullResults.length - offset);
24349
+ let endOffset = offset + returnedChars;
24350
+ let continuation = "";
24351
+ while (endOffset < fullResults.length) {
24352
+ continuation = `
24353
+
24354
+ ---
24355
+ Showing chars ${offset}-${endOffset} of ${fullResults.length}. Use ${toolNames.getSearchContent}({ responseId: "${params.responseId}", queryIndex: ${queryIndex}, offset: ${endOffset}, limit: ${limit} }) for the next slice.`;
24356
+ const overflow = returnedChars + continuation.length - maxInlineContentChars;
24357
+ if (overflow <= 0) break;
24358
+ returnedChars -= overflow;
24359
+ endOffset = offset + returnedChars;
24360
+ }
24361
+ const resultSlice = fullResults.slice(offset, endOffset);
24362
+ const hasMore = endOffset < fullResults.length;
24363
+ const text2 = `${resultSlice}${hasMore ? continuation : ""}`;
24061
24364
  return {
24062
- content: [{ type: "text", text: fullResults }],
24063
- details: { query: queryData.query, resultCount: queryData.results.length }
24365
+ content: [{ type: "text", text: text2 }],
24366
+ details: {
24367
+ responseId: params.responseId,
24368
+ query: queryData.query,
24369
+ resultCount: queryData.results.length,
24370
+ contentLength: fullResults.length,
24371
+ offset,
24372
+ limit,
24373
+ returnedChars,
24374
+ nextOffset: hasMore ? endOffset : null,
24375
+ truncated: hasMore
24376
+ }
24064
24377
  };
24065
24378
  }
24066
24379
  if (data.type === "fetch" && data.urls) {
@@ -24222,6 +24535,13 @@ Showing chars ${offset}-${endOffset} of ${urlData.content.length}.`;
24222
24535
  }
24223
24536
  });
24224
24537
  }
24538
+ const activationTools = [
24539
+ ...webSearchEnabled ? [{ name: toolNames.webSearch, capability: "search" }] : [],
24540
+ ...sourceCheckEnabled ? [{ name: toolNames.sourceCheck, capability: "source-check" }] : [],
24541
+ ...fetchContentEnabled ? [{ name: toolNames.fetchContent, capability: "fetch" }] : [],
24542
+ ...getSearchContentEnabled ? [{ name: toolNames.getSearchContent, capability: "stored-content" }] : []
24543
+ ];
24544
+ registerWebToolActivation(pi, activationTools);
24225
24545
  if (isCommandEnabled(initConfig, "websearch")) pi.registerCommand("websearch", {
24226
24546
  description: "Open web search curator",
24227
24547
  handler: async (args, ctx) => {