@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
@@ -0,0 +1,124 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { isIP } from "node:net";
3
+ import { activityMonitor } from "./activity.ts";
4
+ import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.ts";
5
+ import { normalizeDomain } from "./domain-filter-normalization.ts";
6
+ import type { SearchOptions, SearchResponse } from "./perplexity.ts";
7
+ import { normalizeSearchResultCount } from "./search-result-count-normalization.ts";
8
+ import { getWebSearchConfigPath } from "./utils.ts";
9
+
10
+ const MCP_URL = "https://agent-toolkit.app.baizhi.cloud/mcp";
11
+ const SEARCH_TIMEOUT_MS = 60_000;
12
+ const CONFIG_PATH = getWebSearchConfigPath();
13
+
14
+ function credentialOptions() {
15
+ let config: Record<string, unknown> = {};
16
+ if (existsSync(CONFIG_PATH)) {
17
+ try {
18
+ const parsed: unknown = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
19
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error();
20
+ config = parsed as Record<string, unknown>;
21
+ } catch {
22
+ // JSON parser errors may include a fragment containing a literal key.
23
+ throw new Error(`Invalid configuration in ${CONFIG_PATH}: expected a JSON object`);
24
+ }
25
+ }
26
+ return { provider: "Baizhi", configuredValue: config.baizhiApiKey, environmentValue: process.env.BAIZHI_API_KEY };
27
+ }
28
+
29
+ export function isBaizhiAvailable(): boolean {
30
+ return hasCredentialSource(credentialOptions());
31
+ }
32
+
33
+ function searchArguments(query: string, options: SearchOptions): Record<string, unknown> {
34
+ if (!query.trim()) throw new Error("Baizhi search query must not be empty");
35
+ const domains: string[] = [];
36
+ const excludeDomains: string[] = [];
37
+ for (const raw of options.domainFilter ?? []) {
38
+ const bare = raw.trim().replace(/^-/, "").trim();
39
+ const domain = isIP(bare) ? bare : normalizeDomain(raw);
40
+ if (!domain) throw new Error("Baizhi domain filter must contain a valid domain");
41
+ const target = raw.trim().startsWith("-") ? excludeDomains : domains;
42
+ if (!target.includes(domain)) target.push(domain);
43
+ }
44
+ return {
45
+ query: query.trim(),
46
+ count: normalizeSearchResultCount(options.numResults),
47
+ need_summary: false,
48
+ // The published discovery contract defaults to month, not unrestricted time.
49
+ time_range: options.recencyFilter ?? "month",
50
+ ...(domains.length || excludeDomains.length ? { filter: {
51
+ ...(domains.length ? { domains } : {}),
52
+ ...(excludeDomains.length ? { exclude_domains: excludeDomains } : {}),
53
+ } } : {}),
54
+ };
55
+ }
56
+
57
+ export async function searchWithBaizhi(query: string, options: SearchOptions = {}): Promise<SearchResponse> {
58
+ const args = searchArguments(query, options);
59
+ const timeoutSignal = AbortSignal.timeout(SEARCH_TIMEOUT_MS);
60
+ const signal = options.signal ? AbortSignal.any([timeoutSignal, options.signal]) : timeoutSignal;
61
+ if (signal.aborted) throw new Error("Aborted");
62
+ const apiKey = await resolveCredential({ ...credentialOptions(), signal });
63
+ 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/");
64
+ let headers: Headers;
65
+ try { headers = new Headers({ Authorization: `Bearer ${apiKey}` }); }
66
+ catch { throw new Error("Baizhi credential resolution failed: invalid-header-value"); }
67
+ // Load the protocol client only when this explicit provider is selected.
68
+ const [{ Client }, { StreamableHTTPClientTransport }] = await Promise.all([
69
+ import("@modelcontextprotocol/sdk/client/index.js"),
70
+ import("@modelcontextprotocol/sdk/client/streamableHttp.js"),
71
+ ]);
72
+ let cleaningUp = false;
73
+ const transport = new StreamableHTTPClientTransport(new URL(MCP_URL), {
74
+ requestInit: { headers },
75
+ fetch: async (url, init) => {
76
+ // Fixed endpoint, no redirects: neither credentials nor session IDs can
77
+ // be forwarded to another origin (including MCP discovery redirects).
78
+ if (String(url) !== MCP_URL) throw new Error("Baizhi endpoint changed unexpectedly");
79
+ const requestSignal = cleaningUp ? AbortSignal.timeout(1_500) : signal;
80
+ return fetch(url, { ...init, redirect: "error", signal: !cleaningUp && init?.signal ? AbortSignal.any([init.signal, requestSignal]) : requestSignal });
81
+ },
82
+ reconnectionOptions: { maxRetries: 0, initialReconnectionDelay: 1_000, maxReconnectionDelay: 1_000, reconnectionDelayGrowFactor: 1 },
83
+ });
84
+ const client = new Client({ name: "pi-web-access-baizhi", version: "1.0.0" });
85
+ const activityId = activityMonitor.logStart({ type: "api", query });
86
+ try {
87
+ await client.connect(transport, { signal, timeout: SEARCH_TIMEOUT_MS });
88
+ const result = await client.callTool({ name: "websearch_search", arguments: args }, undefined, { signal, timeout: SEARCH_TIMEOUT_MS });
89
+ if (result.isError) throw new Error("Baizhi search tool returned an error");
90
+ // The public discovery snapshot documents inputs but no outputSchema.
91
+ // Preserve the MCP payload instead of guessing result item field names.
92
+ const parts: string[] = [];
93
+ if (result.structuredContent !== undefined) parts.push(JSON.stringify(result.structuredContent, null, 2));
94
+ for (const item of Array.isArray(result.content) ? result.content : []) {
95
+ if (item.type === "text" && typeof item.text === "string" && item.text.trim()) parts.push(item.text);
96
+ }
97
+ if (!parts.length) throw new Error("Baizhi returned invalid response: no search text or structured content");
98
+ const answer = redactCredential(redactCredential(parts.join("\n\n"), apiKey), JSON.stringify(apiKey).slice(1, -1));
99
+ activityMonitor.logComplete(activityId, 200);
100
+ return { answer, results: [] };
101
+ } catch (err) {
102
+ let message: string;
103
+ if (options.signal?.aborted) message = "Aborted";
104
+ else if (timeoutSignal.aborted) message = "Baizhi request timed out after 60s";
105
+ else {
106
+ const code = err && typeof err === "object" && "code" in err ? err.code : undefined;
107
+ // Never expose remote HTTP/JSON-RPC bodies or nested SDK errors: they
108
+ // can echo credentials in plain, encoded or transformed form.
109
+ message = typeof code === "number" && Number.isInteger(code) && code >= 400 && code <= 599
110
+ ? `Baizhi HTTP ${code} request failed; check your API key, account and service status`
111
+ : err instanceof TypeError ? "Baizhi network request failed"
112
+ : err instanceof SyntaxError || code === -32700 || code === -32602 ? "Baizhi returned invalid response"
113
+ : err instanceof Error && /^Baizhi (search tool returned an error|returned invalid response:)/.test(err.message) ? err.message
114
+ : "Baizhi MCP request failed; check your connection, API key and account status";
115
+ }
116
+ activityMonitor.logError(activityId, message);
117
+ throw new Error(message);
118
+ } finally {
119
+ cleaningUp = true;
120
+ // Best effort, separately bounded DELETE; always close the local streams.
121
+ try { await transport.terminateSession(); } catch { /* Server may not support DELETE. */ }
122
+ await client.close();
123
+ }
124
+ }
@@ -1,10 +1,9 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import net from "node:net";
3
2
  import { activityMonitor } from "./activity.ts";
4
3
  import { redactCredential, resolveCredential } from "./credential-source.ts";
5
4
  import type { ExtractedContent, ExtractOptions } from "./extract.ts";
6
5
  import { fetchRemoteUrl, loadSsrfConfig, validateRemoteUrl, type Lookup, type SsrfConfig } from "./ssrf-protection.ts";
7
- import { getWebSearchConfigPath } from "./utils.ts";
6
+ import { getWebSearchConfigPath, isLoopbackHostname } from "./utils.ts";
8
7
 
9
8
  const CONFIG_PATH = getWebSearchConfigPath();
10
9
  const EXTRACT_TIMEOUT_MS = 60_000;
@@ -113,13 +112,6 @@ function ssrfOptions(options?: Crawl4aiExtractOptions): SsrfConfig & { lookup?:
113
112
  };
114
113
  }
115
114
 
116
- function isLoopbackApiUrl(url: URL): boolean {
117
- const hostname = url.hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
118
- if (hostname === "localhost" || hostname === "::1") return true;
119
- if (net.isIP(hostname) !== 4) return false;
120
- return hostname.split(".")[0] === "127";
121
- }
122
-
123
115
  function firstHeadingTitle(markdown: string): string {
124
116
  return /^[ \t]*#[ \t]+(\S.*?)[ \t\r]*$/m.exec(markdown)?.[1] ?? "";
125
117
  }
@@ -151,7 +143,7 @@ export async function extractWithCrawl4ai(
151
143
  try {
152
144
  const response = await fetchRemoteUrl(requestUrl, init, {
153
145
  ...ssrf,
154
- allowLoopback: isLoopbackApiUrl(requestUrl),
146
+ allowLoopback: isLoopbackHostname(requestUrl.hostname),
155
147
  onRedirect: ({ to, init: redirectInit, response }) => {
156
148
  if (to.origin !== requestUrl.origin) {
157
149
  throw new Error(`Crawl4AI refused cross-origin redirect to ${to.origin}`);
@@ -46,6 +46,7 @@ function buildProviderButtons(
46
46
  { value: "serpapi", label: "SerpApi", available: available.serpapi },
47
47
  { value: "serper", label: "Serper", available: available.serper },
48
48
  { value: "serply", label: "Serply", available: available.serply },
49
+ { value: "baizhi", label: "Baizhi", available: available.baizhi },
49
50
  { value: "valyu", label: "Valyu", available: available.valyu },
50
51
  ];
51
52
 
@@ -1473,7 +1474,7 @@ const SCRIPT = `(function() {
1473
1474
  var token = DATA.sessionToken;
1474
1475
  var timeoutSec = DATA.timeout;
1475
1476
  var queries = Array.isArray(DATA.queries) ? DATA.queries : [];
1476
- 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"];
1477
+ 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"];
1477
1478
  var availProviders = DATA.availableProviders && typeof DATA.availableProviders === "object" ? DATA.availableProviders : {};
1478
1479
  var workflow = "summary-review";
1479
1480
  var initialDefaultProvider = typeof DATA.defaultProvider === "string" ? DATA.defaultProvider : "exa";
@@ -1708,6 +1709,7 @@ const SCRIPT = `(function() {
1708
1709
  if (provider === "serpapi") return "SerpApi";
1709
1710
  if (provider === "serper") return "Serper";
1710
1711
  if (provider === "serply") return "Serply";
1712
+ if (provider === "baizhi") return "Baizhi";
1711
1713
  if (provider === "valyu") return "Valyu";
1712
1714
  return "Unknown";
1713
1715
  }
@@ -305,6 +305,7 @@ export function startCuratorServer(
305
305
  if (provider === "serpbase") return availableProviders.serpbase;
306
306
  if (provider === "serper") return availableProviders.serper;
307
307
  if (provider === "serply") return availableProviders.serply;
308
+ if (provider === "baizhi") return availableProviders.baizhi;
308
309
  if (provider === "valyu") return availableProviders.valyu;
309
310
  return false;
310
311
  }