@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mcp-adapter",
3
- "version": "2.36.0",
3
+ "version": "2.37.0",
4
4
  "description": "MCP (Model Context Protocol) adapter extension for Pi coding agent",
5
5
  "type": "module",
6
6
  "types": "./index.ts",
@@ -185,7 +185,7 @@
185
185
  "zod": "^3.25.0 || ^4.0.0"
186
186
  },
187
187
  "peerDependencies": {
188
- "@earendil-works/pi-ai": "^0.84.1 || ^0.85.0 || ^0.86.0",
188
+ "@earendil-works/pi-ai": "^0.84.1 || ^0.85.0 || ^0.86.0 || ^0.87.0",
189
189
  "@earendil-works/pi-tui": "*",
190
190
  "typebox": "*",
191
191
  "zod": "^3.25.0 || ^4.0.0"
@@ -202,9 +202,9 @@
202
202
  }
203
203
  },
204
204
  "devDependencies": {
205
- "@earendil-works/pi-ai": "0.86.0",
206
- "@earendil-works/pi-coding-agent": "0.86.0",
207
- "@earendil-works/pi-tui": "0.86.0",
205
+ "@earendil-works/pi-ai": "0.87.0",
206
+ "@earendil-works/pi-coding-agent": "0.87.0",
207
+ "@earendil-works/pi-tui": "0.87.0",
208
208
  "@modelcontextprotocol/conformance": "0.1.16",
209
209
  "@types/bun": "^1.0.0",
210
210
  "@types/cross-spawn": "6.0.6",
@@ -114,10 +114,10 @@ export async function semanticSearch(
114
114
  try {
115
115
  settings = resolveSemanticJevSettings(state);
116
116
  } catch {
117
- return { ok: false, error: { code: "invalid_request", message: "Invalid TypeSafe semantic search settings." } };
117
+ return { ok: false, error: { code: "invalid_request", message: "Invalid Jev semantic search settings." } };
118
118
  }
119
119
  if (!settings.semanticSearch) {
120
- return { ok: false, error: { code: "disabled", message: "TypeSafe semantic search is disabled." } };
120
+ return { ok: false, error: { code: "disabled", message: "Jev semantic search is disabled." } };
121
121
  }
122
122
  if (settings.allowedServers.length === 0) {
123
123
  return { ok: false, error: { code: "data_policy_denied", message: "Semantic search is enabled, but settings.jev.allowedServers is empty. Run /mcp jev setup or allow specific MCP servers." } };
@@ -166,7 +166,7 @@ export async function semanticSearch(
166
166
  }
167
167
  const answer = envelope.data.answers.match;
168
168
  if (!answer || answer.type !== "choice") {
169
- return { ok: false, error: { code: "invalid_response", message: "TypeSafe returned an invalid semantic search response." } };
169
+ return { ok: false, error: { code: "invalid_response", message: "Jev returned an invalid semantic search response." } };
170
170
  }
171
171
  const ranked = candidates
172
172
  .map(candidate => ({ candidate, probability: answer.probabilities[candidate.id] ?? 0 }))
@@ -589,6 +589,8 @@ export type { JevAnswer, JevErrorCode, JevEvaluateInput, JevEvaluationData, JevE
589
589
 
590
590
  export interface McpSettings {
591
591
  toolPrefix?: ToolPrefix;
592
+ /** Allow agents to persist remote MCP endpoints with the install action. Defaults to true. */
593
+ allowInstall?: boolean;
592
594
  /** Show the plug prefix in MCP status and connection text (default: true). Set to false to disable it. */
593
595
  showStatusIcon?: boolean;
594
596
  /** Footer status verbosity: full details, compact connected/enabled count, or no footer status. Defaults to full. */
@@ -603,6 +605,8 @@ export interface McpSettings {
603
605
  agentPluginPaths?: string[];
604
606
  idleTimeout?: number; // minutes, default 10, 0 to disable
605
607
  requestTimeoutMs?: number; // milliseconds, overrides the SDK request timeout when > 0
608
+ /** Defer lazy runtime startup even when persisted metadata is missing or invalid. Defaults to false. */
609
+ deferWithMissingMetadata?: boolean;
606
610
  directTools?: boolean | "search";
607
611
  /** Register per-server mcp__<server> namespace proxies. Defaults to true. */
608
612
  namespaceProxyTools?: boolean;
@@ -620,7 +624,9 @@ export interface McpSettings {
620
624
  warnOnLargeDirectTools?: boolean;
621
625
  /** Register the trusted MCP-only JavaScript scripting tool. Defaults to true; set false to hide it. */
622
626
  scriptMode?: boolean;
623
- /** Optional TypeSafe Jev integrations. A valid key enables semantic search; script evaluation remains disabled by default. */
627
+ /** Expose MCP resources as tools (default: true). Set to false to disable globally across all servers. */
628
+ exposeResources?: boolean;
629
+ /** Optional Jev (System One) integrations. A valid key enables semantic search; script evaluation remains disabled by default. */
624
630
  jev?: false | {
625
631
  semanticSearch?: boolean;
626
632
  scriptEvaluation?: boolean;
@@ -5,6 +5,12 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.23.1] — 2026-09-23
9
+
10
+ ### Fixed
11
+
12
+ - Restore `pi-ai` as an installed runtime dependency for the OAuth/catalog bridge (#75). Pi-managed npm, bun and pnpm extension installs deliberately omit peer dependencies; v1.23.0 made pi-ai peer-only, so a fresh install could not log into a numbered subscription account. Native provider transports remain bound to the running Pi host, not this bridge copy. The package check now rejects future peer-only regressions.
13
+
8
14
  ## [1.23.0] — 2026-09-23
9
15
 
10
16
  ### Added
@@ -27,11 +27,12 @@ pi install npm:pi-multi-account
27
27
  Restart Pi or run `/reload` after installation.
28
28
 
29
29
  Requires Node 22+ and Agent Pi / pi-ai **>=0.85.1, <0.88.0** (CI covers 0.85.1, 0.86.1 and 0.87.1).
30
- Pi core packages are peers, not a private pinned transport dependency. Provider wrappers use Pi's
31
- host-bound imports, so the request adapter and host context format stay together; a stale nested
32
- pi-ai cannot silently remove tools or system instructions. Standalone SDK callers must pair their
33
- context with the installed pi-ai version. The OAuth/catalog bridge remains a separate, best-effort
34
- compatibility layer: unavailable subscription login does not prevent API-key account discovery.
30
+ Pi's host package is a peer. `pi-ai` is a runtime dependency for the OAuth/catalog bridge:
31
+ Pi-managed installs intentionally do not install extension peer dependencies, but subscription
32
+ login must be able to locate pi-ai on disk. Provider transports still use Pi's **host-bound**
33
+ imports, not that filesystem-resolved bridge copy, so a stale nested pi-ai cannot remove tools or
34
+ system instructions. Standalone SDK callers must pair their context with the installed pi-ai
35
+ version. If the OAuth bridge is unavailable, API-key account discovery still works.
35
36
 
36
37
  Kimi OAuth spares remain available in `/login`, but are not written to `models.json`: without a
37
38
  Kimi child OAuth proxy those aliases would be resolvable but unauthenticated. Only real API-key
@@ -1195,7 +1195,7 @@ const ANTI_PINGPONG_MS = 60 * 1000; // don't switch straight back to the account
1195
1195
  // Bumped on every release. Printed at startup and in `/multi-account status` so you can verify
1196
1196
  // which version Pi actually loaded (a running Pi keeps the version it started with — /login and
1197
1197
  // /reload do NOT reload extension code; only a full restart does).
1198
- const VERSION = "1.23.0";
1198
+ const VERSION = "1.23.1";
1199
1199
  function sourceFingerprint(): string {
1200
1200
  try {
1201
1201
  const root = dirname(fileURLToPath(import.meta.url));
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-multi-account",
3
- "version": "1.23.0",
3
+ "version": "1.23.1",
4
4
  "description": "Automatic multi-account failover & rotation for Pi Agent across Anthropic (Claude), OpenAI/ChatGPT Codex, Kimi For Coding, Cursor, Qwen/Alibaba, and Ollama. Auto-discovers authenticated accounts, grows the rotation on login, and drops accounts on logout, expiry, or quota/rate-limit errors",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -78,8 +78,7 @@
78
78
  "access": "public"
79
79
  },
80
80
  "peerDependencies": {
81
- "@earendil-works/pi-coding-agent": ">=0.85.1 <0.88.0",
82
- "@earendil-works/pi-ai": ">=0.85.1 <0.88.0"
81
+ "@earendil-works/pi-coding-agent": ">=0.85.1 <0.88.0"
83
82
  },
84
83
  "devDependencies": {
85
84
  "@earendil-works/pi-coding-agent": "0.85.1",
@@ -94,6 +93,7 @@
94
93
  },
95
94
  "dependencies": {
96
95
  "@bufbuild/protobuf": "^2.14.0",
96
+ "@earendil-works/pi-ai": ">=0.85.1 <0.88.0",
97
97
  "proper-lockfile": "4.1.2"
98
98
  },
99
99
  "overrides": {
@@ -24,6 +24,16 @@ export declare const TYPESAFE_KEY_ENV = "TYPESAFE_API_KEY";
24
24
  export declare const DECISIONS_BACKENDS: Record<TypeSafeBackend, BackendConfig>;
25
25
  /** The registry entry for a backend name; a `configuration` error for a name the registry does not know. */
26
26
  export declare function backendConfig(name: TypeSafeBackend): BackendConfig;
27
+ /**
28
+ * The model id to send for a caller's `model` on this backend. OpenRouter routes a bare Jev id under the `typesafe`
29
+ * author: `jev-latest` becomes its alias form `~typesafe/jev-latest`, and a bare `jev-<major>.<minor>` — with or
30
+ * without TypeSafe direct's optional `.<patch>` segment — becomes `typesafe/jev-<major>.<minor>`. An id that already
31
+ * carries an author (`vendor/model`), a bare id this rule does not know, and every model on a backend without a
32
+ * mapping go through unchanged. Every mapped id contains `/`, so mapping an already-mapped id changes nothing.
33
+ */
34
+ export declare function backendModelId(backend: TypeSafeBackend, model: string): string;
35
+ /** The model a client sends when the caller names none: the backend's own default, in the form that backend accepts. */
36
+ export declare function defaultModelId(backend: TypeSafeBackend): string;
27
37
  /**
28
38
  * Whether a backend's key comes from the TypeSafe resolution (`TYPESAFE_API_KEY`, then the login store) or only from
29
39
  * its own environment variable. Only the TypeSafe backend has a login store; every other backend is environment-only.
@@ -24,6 +24,32 @@ export function backendConfig(name) {
24
24
  throw new TypeSafeIntegrationError("configuration", `Unknown judgment backend "${name}". Valid backends: ${Object.keys(DECISIONS_BACKENDS).join(", ")}.`);
25
25
  return backend;
26
26
  }
27
+ /** Each backend's default model id as the caller writes it, before mapping: OpenRouter pins a version, TypeSafe follows latest. */
28
+ const DEFAULT_MODEL = {
29
+ typesafe: "jev-latest",
30
+ openrouter: "typesafe/jev-1.13",
31
+ };
32
+ /**
33
+ * The model id to send for a caller's `model` on this backend. OpenRouter routes a bare Jev id under the `typesafe`
34
+ * author: `jev-latest` becomes its alias form `~typesafe/jev-latest`, and a bare `jev-<major>.<minor>` — with or
35
+ * without TypeSafe direct's optional `.<patch>` segment — becomes `typesafe/jev-<major>.<minor>`. An id that already
36
+ * carries an author (`vendor/model`), a bare id this rule does not know, and every model on a backend without a
37
+ * mapping go through unchanged. Every mapped id contains `/`, so mapping an already-mapped id changes nothing.
38
+ */
39
+ export function backendModelId(backend, model) {
40
+ if (model.includes("/"))
41
+ return model;
42
+ if (backend !== "openrouter")
43
+ return model;
44
+ if (model === "jev-latest")
45
+ return "~typesafe/jev-latest";
46
+ const version = /^jev-(\d+)\.(\d+)(?:\.\d+)?$/.exec(model);
47
+ return version === null ? model : `typesafe/jev-${version[1]}.${version[2]}`;
48
+ }
49
+ /** The model a client sends when the caller names none: the backend's own default, in the form that backend accepts. */
50
+ export function defaultModelId(backend) {
51
+ return backendModelId(backend, DEFAULT_MODEL[backend]);
52
+ }
27
53
  /**
28
54
  * Whether a backend's key comes from the TypeSafe resolution (`TYPESAFE_API_KEY`, then the login store) or only from
29
55
  * its own environment variable. Only the TypeSafe backend has a login store; every other backend is environment-only.
@@ -9,7 +9,7 @@ export interface TypeSafeOptions {
9
9
  apiKey?: string;
10
10
  /** Judgment backend. When omitted, routes to the default TypeSafe host. */
11
11
  backend?: TypeSafeBackend;
12
- /** Defaults to jev-latest. No model is inferred from submitted content. */
12
+ /** Defaults to the backend's own default (`jev-latest`, `typesafe/jev-1.13` on OpenRouter); a bare Jev id is mapped to the backend's id form before sending. No model is inferred from submitted content. */
13
13
  model?: string;
14
14
  /** Per request. Default: 15 seconds. No automatic retries. */
15
15
  timeoutMs?: number;
@@ -1,6 +1,6 @@
1
1
  import { TypeSafeClient } from "@typesafe-ai/sdk";
2
2
  import { recordAuthFailure, recordAuthVerified } from "./auth.js";
3
- import { DEFAULT_BACKEND, TYPESAFE_KEY_ENV, backendConfig, usesTypesafeKey } from "./backends.js";
3
+ import { DEFAULT_BACKEND, TYPESAFE_KEY_ENV, backendConfig, backendModelId, defaultModelId, usesTypesafeKey } from "./backends.js";
4
4
  import { evaluateAll, evaluateMany } from "./batch.js";
5
5
  import { keySituation } from "./credentials.js";
6
6
  import { TypeSafeIntegrationError, safeError } from "./errors.js";
@@ -146,9 +146,12 @@ export function createTypeSafe(options = {}) {
146
146
  }, capsFromEnvironment());
147
147
  // A backend that serves its own paths gets a transport that rewrites them; the default backend keeps the caller's.
148
148
  const transport = backend.path !== undefined || backend.modelsPath !== undefined ? backendFetch(backend, options.fetch) : options.fetch;
149
- const model = options.model ?? (backendName === "openrouter" ? "typesafe/jev-1.13" : "jev-latest");
150
- if (typeof model !== "string" || !model.trim() || model.length > 100)
149
+ // The caller's input is validated as written, then mapped to the backend's id form; omitting it sends the backend's
150
+ // own default, which the mapping leaves unchanged.
151
+ const requested = options.model ?? defaultModelId(backendName);
152
+ if (typeof requested !== "string" || !requested.trim() || requested.length > 100)
151
153
  throw new TypeSafeIntegrationError("configuration", "model must be a nonempty string of at most 100 characters.");
154
+ const model = backendModelId(backendName, requested);
152
155
  // Do not inherit SDK debug logging or alternate destinations from the environment.
153
156
  const client = new TypeSafeClient({
154
157
  apiKey,
@@ -191,12 +194,13 @@ export function createTypeSafe(options = {}) {
191
194
  return models.map(card => card?.name).filter((name) => typeof name === "string" && name.length > 0 && name.length <= 100);
192
195
  }
193
196
  catch (error) {
194
- throw safeError(error);
197
+ throw safeError(error, backend);
195
198
  }
196
199
  },
197
200
  async evaluate(input, callOptions = {}) {
198
201
  const validated = prepareEvaluationRequest(input, { maxInputBytes });
199
- const body = JSON.stringify({ ...validated, model: validated.model ?? model });
202
+ // A per-request model meets the same mapping as the client default; the schema already limited the caller's own id.
203
+ const body = JSON.stringify({ ...validated, model: validated.model === undefined ? model : backendModelId(backendName, validated.model) });
200
204
  assertWithinByteLimit(body, maxInputBytes);
201
205
  if (callOptions.signal?.aborted)
202
206
  throw new TypeSafeIntegrationError("aborted", "TypeSafe request cancelled before submission.");
@@ -228,7 +232,7 @@ export function createTypeSafe(options = {}) {
228
232
  return { ...result, elapsedMs: Math.round(performance.now() - start) };
229
233
  }
230
234
  catch (error) {
231
- const safe = safeError(error);
235
+ const safe = safeError(error, backend);
232
236
  // The request was submitted, so it counts even when it fails; the reason stays visible in `authState()`.
233
237
  usage.requestsFailed += 1;
234
238
  ledger.recordFailure();
@@ -1,9 +1,17 @@
1
+ import type { BackendConfig, TypeSafeBackend } from "./backends.js";
1
2
  export type IntegrationErrorCode = "configuration" | "validation" | "budget" | "aborted" | "timeout" | "http" | "connection" | "response";
2
- /** Safe to display: never contains upstream bodies, headers, keys, or submitted state. */
3
+ /**
4
+ * Safe to display: never contains upstream bodies, keys, or submitted state. The only header value a message can
5
+ * quote is a numeric Retry-After count in seconds, which cannot carry a secret.
6
+ */
3
7
  export declare class TypeSafeIntegrationError extends Error {
4
8
  readonly code: IntegrationErrorCode;
5
9
  readonly status?: number | undefined;
6
10
  readonly name = "TypeSafeIntegrationError";
7
11
  constructor(code: IntegrationErrorCode, message: string, status?: number | undefined);
8
12
  }
9
- export declare function safeError(error: unknown): TypeSafeIntegrationError;
13
+ /**
14
+ * Classify an error into a message safe to display. `backend` names the key variable the 401 advice tells the user
15
+ * to check and selects the 402 wording; omitting it keeps the one-argument call and assumes the default TypeSafe key.
16
+ */
17
+ export declare function safeError(error: unknown, backend?: TypeSafeBackend | BackendConfig): TypeSafeIntegrationError;
@@ -1,5 +1,9 @@
1
1
  import { APIError, APIConnectionError, APITimeoutError, APIUserAbortError } from "@typesafe-ai/sdk";
2
- /** Safe to display: never contains upstream bodies, headers, keys, or submitted state. */
2
+ import { DECISIONS_BACKENDS, TYPESAFE_KEY_ENV, backendConfig } from "./backends.js";
3
+ /**
4
+ * Safe to display: never contains upstream bodies, keys, or submitted state. The only header value a message can
5
+ * quote is a numeric Retry-After count in seconds, which cannot carry a secret.
6
+ */
3
7
  export class TypeSafeIntegrationError extends Error {
4
8
  code;
5
9
  status;
@@ -10,7 +14,19 @@ export class TypeSafeIntegrationError extends Error {
10
14
  this.status = status;
11
15
  }
12
16
  }
13
- export function safeError(error) {
17
+ /** A numeric Retry-After delay in seconds; dates, blanks, and anything else stay out of the message. */
18
+ function retryAfterSeconds(error) {
19
+ const raw = error.headers?.get("retry-after")?.trim();
20
+ if (!raw)
21
+ return undefined;
22
+ const seconds = Number(raw);
23
+ return Number.isSafeInteger(seconds) && seconds >= 0 ? seconds : undefined;
24
+ }
25
+ /**
26
+ * Classify an error into a message safe to display. `backend` names the key variable the 401 advice tells the user
27
+ * to check and selects the 402 wording; omitting it keeps the one-argument call and assumes the default TypeSafe key.
28
+ */
29
+ export function safeError(error, backend) {
14
30
  if (error instanceof TypeSafeIntegrationError)
15
31
  return error;
16
32
  if (error instanceof APIUserAbortError)
@@ -18,11 +34,16 @@ export function safeError(error) {
18
34
  if (error instanceof APITimeoutError)
19
35
  return new TypeSafeIntegrationError("timeout", "TypeSafe request timed out; it was not retried and may still be billed.");
20
36
  if (error instanceof APIError) {
21
- const advice = error.status === 401 ? "Check TYPESAFE_API_KEY."
22
- : error.status === 403 ? "Check your account access and model permissions."
23
- : error.status === 429 ? "Check your account quota and try again later."
24
- : error.status === 400 || error.status === 422 ? "Check the question format and model limits."
25
- : "Try again later or check the service status.";
37
+ const config = backend === undefined ? undefined : typeof backend === "string" ? backendConfig(backend) : backend;
38
+ const keyEnv = config?.keyEnv ?? TYPESAFE_KEY_ENV;
39
+ const openrouter = config !== undefined && config.host === DECISIONS_BACKENDS.openrouter.host;
40
+ const retry = error.status === 429 ? retryAfterSeconds(error) : undefined;
41
+ const advice = error.status === 401 ? `Check ${keyEnv}.`
42
+ : error.status === 402 ? (openrouter ? "Insufficient credits. Add credits at https://openrouter.ai/credits." : "Check your account balance.")
43
+ : error.status === 403 ? "Check your account access and model permissions."
44
+ : error.status === 429 ? `Check your account quota and try again later.${retry === undefined ? "" : ` Retry after ${retry} seconds.`}`
45
+ : error.status === 400 || error.status === 422 ? "Check the question format and model limits."
46
+ : "Try again later or check the service status.";
26
47
  return new TypeSafeIntegrationError("http", `TypeSafe returned HTTP ${error.status}. ${advice} No automatic retry was made.`, error.status);
27
48
  }
28
49
  if (error instanceof APIConnectionError)
@@ -1,4 +1,5 @@
1
1
  import { Text } from "@earendil-works/pi-tui";
2
+ import { DEFAULT_BACKEND, defaultModelId } from "./backends.js";
2
3
  import { createTypeSafe, DEFAULT_MAX_REQUESTS } from "./client.js";
3
4
  import { authState, clearAuthState, describeAuth } from "./auth.js";
4
5
  import { clearStoredApiKey, credentialsPath, keySituation, keySourceLabel } from "./credentials.js";
@@ -136,7 +137,7 @@ export default function typesafeExtension(pi) {
136
137
  ? `Today ${spend.today.requestsStarted} requests (${spend.today.requestsSucceeded} ok, ${spend.today.requestsFailed} failed), ${spend.today.inputTokens} input tokens, ~$${spend.today.estimatedUsd.toFixed(4)}.`
137
138
  : "";
138
139
  const blocked = spend?.blocked ? ` Cap reached: ${spend.blocked.cap} ${spend.blocked.used}/${spend.blocked.limit} on ${spend.blocked.day}; no request will be submitted until the local day rolls over.` : "";
139
- report(`TypeSafe: ${enabled ? "enabled" : "disabled"}. ${auth.text} ${session} ${today}${blocked} Model: jev-latest. Session limits reset on session start/reload; daily counters persist and caps come from client options or PI_TYPESAFE_MAX_* environment variables. ${disclosure}`, auth.level === "error" && enabled ? "warning" : "info");
140
+ report(`TypeSafe: ${enabled ? "enabled" : "disabled"}. ${auth.text} ${session} ${today}${blocked} Model: ${defaultModelId(DEFAULT_BACKEND)}. Session limits reset on session start/reload; daily counters persist and caps come from client options or PI_TYPESAFE_MAX_* environment variables. ${disclosure}`, auth.level === "error" && enabled ? "warning" : "info");
140
141
  return;
141
142
  }
142
143
  if (action === "logout") {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-typesafe",
3
- "version": "0.7.1",
3
+ "version": "0.7.4",
4
4
  "description": "TypeSafe AI (Jev) decisions for Pi: batched Choice/Score/Noul evaluation tool, terminal playground, and a typed API other extensions build on.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -80,10 +80,10 @@
80
80
  }
81
81
  },
82
82
  "devDependencies": {
83
- "@earendil-works/pi-coding-agent": "0.85.1",
84
- "@earendil-works/pi-tui": "^0.85.1",
83
+ "@earendil-works/pi-coding-agent": "0.86.1",
84
+ "@earendil-works/pi-tui": "^0.86.1",
85
85
  "@types/node": "^22.0.0",
86
86
  "tsx": "^4.23.13",
87
- "typescript": "^6.0.0"
87
+ "typescript": "^7.0.2"
88
88
  }
89
89
  }
@@ -4,6 +4,28 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.31.0] - 2026-09-22
8
+
9
+ ### Highlights
10
+
11
+ - Start fresh sessions with a smaller web-tool list, then enable the tools when you need them.
12
+ - Keep long search responses manageable without losing access to the full results.
13
+ - Search with Baizhi MCP when you explicitly configure it.
14
+ - Connect Brave, Exa, and Tavily to local HTTP services on loopback addresses.
15
+
16
+ ### Added
17
+
18
+ - Fresh sessions now show `web_enable` instead of the full web-tool list when supported. Call it to make your configured web tools available. Thanks to [@Knimoms](https://github.com/Knimoms) for [PR #424](https://github.com/nicobailon/pi-web-access/pull/424).
19
+ - Added Baizhi MCP as an optional search provider, with search routing and Curator support. It is used only when explicitly selected. Thanks to [@ct-jaryn](https://github.com/ct-jaryn) for [PR #422](https://github.com/nicobailon/pi-web-access/pull/422).
20
+
21
+ ### Changed
22
+
23
+ - Long `web_search` responses now show a shorter result and the providers used. Full results remain available through `get_search_content`. Thanks to [@theSprog](https://github.com/theSprog) for [issue #423](https://github.com/nicobailon/pi-web-access/issues/423).
24
+
25
+ ### Fixed
26
+
27
+ - Allow explicitly configured Brave, Exa, and Tavily API base URLs to use HTTP on true loopback hosts while continuing to require HTTPS remotely. Thanks to [@aaschmid](https://github.com/aaschmid) for [issue #421](https://github.com/nicobailon/pi-web-access/issues/421).
28
+
7
29
  ## [0.30.0] - 2026-09-19
8
30
 
9
31
  ### Highlights
@@ -163,7 +163,7 @@ fetch_content({ url: "/path/to/recording.mp4", prompt: "What error appears on sc
163
163
 
164
164
  ### web_search
165
165
 
166
- Search the web via OpenAI, Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Firecrawl, Jina, SERPdive, Kagi, Bocha, Ollama, AnySearch, XCrawl, Valyu, xAI, Mistral, Bright Data SERP, SerpBase, SerpApi, Serper, Serply, self-hosted SearXNG, keyless DuckDuckGo, Exa, Perplexity AI, Gemini, or Kimi. By default, returns source-linked search results or provider answers.
166
+ Search the web via OpenAI, Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Firecrawl, Jina, SERPdive, Kagi, Bocha, Ollama, AnySearch, XCrawl, Valyu, xAI, Mistral, Bright Data SERP, SerpBase, SerpApi, Serper, Serply, self-hosted SearXNG, keyless DuckDuckGo, Exa, Perplexity AI, Gemini, or Kimi. The default `none` workflow makes no summary-generation model call and opens no curator/browser: it returns bounded raw results, identifies the provider used for each query, and stores the full results for retrieval by responseId. The selected search provider may itself be model-backed.
167
167
 
168
168
  ```typescript
169
169
  web_search({ query: "rust async programming" })
@@ -186,7 +186,7 @@ web_search({ queries: ["query 1", "query 2"], workflow: "auto-summary" })
186
186
  | `numResults` | Results per query (default: 5, max: 20) |
187
187
  | `recencyFilter` | `day`, `week`, `month`, or `year` |
188
188
  | `domainFilter` | Limit to domains (prefix with `-` to exclude) |
189
- | `provider` | Configured provider when omitted or set to `auto`; `all` searches every eligible provider except Parallel MCP, DuckDuckGo, Kimi, AnySearch, XCrawl, Valyu, xAI, Mistral, Bright Data, SerpBase, SerpApi, Serper, and Serply simultaneously; otherwise `openai`, `brave`, `parallel`, `parallel-mcp`, `tinyfish`, `search1api`, `searchinfinity`, `querit`, `tavily`, `firecrawl`, `jina`, `serpdive`, `kagi`, `bocha`, `ollama`, `anysearch`, `xcrawl`, `valyu`, `xai`, `mistral`, `brightdata`, `serpbase`, `serpapi`, `serper`, `serply`, `searxng`, `duckduckgo`, `exa`, `perplexity`, `gemini`, or `kimi` (auto-selects when no provider or routing is configured; Parallel MCP, DuckDuckGo, Kimi, AnySearch, XCrawl, Valyu, xAI, Mistral, Bright Data, SerpBase, SerpApi, Serper, and Serply are explicit-only) |
189
+ | `provider` | Configured provider when omitted or set to `auto`; `all` searches every eligible provider except Parallel MCP, DuckDuckGo, Kimi, AnySearch, XCrawl, Valyu, xAI, Mistral, Bright Data, SerpBase, SerpApi, Serper, Serply, and Baizhi simultaneously; otherwise `openai`, `brave`, `parallel`, `parallel-mcp`, `tinyfish`, `search1api`, `searchinfinity`, `querit`, `tavily`, `firecrawl`, `jina`, `serpdive`, `kagi`, `bocha`, `ollama`, `anysearch`, `xcrawl`, `valyu`, `xai`, `mistral`, `brightdata`, `serpbase`, `serpapi`, `serper`, `serply`, `baizhi`, `searxng`, `duckduckgo`, `exa`, `perplexity`, `gemini`, or `kimi` (auto-selects when no provider or routing is configured; Parallel MCP, DuckDuckGo, Kimi, AnySearch, XCrawl, Valyu, xAI, Mistral, Bright Data, SerpBase, SerpApi, Serper, Serply, and Baizhi are explicit-only) |
190
190
  | `includeContent` | Fetch full page content from sources in background |
191
191
  | `workflow` | `none` (skip curator; fresh-install default), `summary-review` (open curator and auto-generate a summary draft), or `auto-summary` (generate a summary without opening the curator) |
192
192
 
@@ -226,17 +226,18 @@ Thanks to [@linuxtextadventurer](https://github.com/linuxtextadventurer) for PR
226
226
 
227
227
  ### get_search_content
228
228
 
229
- Retrieve stored content from previous searches or fetches. Fetched URL content is stored in full in a private `web-search-cache` directory under the Pi config directory, not in the session JSONL. This includes `fetch_content` answer mode, which stores the original page content. The cache has a one-hour lifetime and fixed limits of 128 entries and 128 MiB; when either limit is reached, the oldest entries are removed first. On macOS and Linux the cache directory and files are kept at permissions `0700` and `0600`, respectively. Use `findText` to locate bounded matching passages without paging through a large page, or use `offset` and `limit` to retrieve slices intentionally.
229
+ Retrieve stored content from previous searches or fetches. Search provider answers and every result remain available in full and can be paged with `offset` and `limit` or searched with `findText`. Fetched URL content is stored in full in a private `web-search-cache` directory under the Pi config directory, not in the session JSONL. This includes `fetch_content` answer mode, which stores the original page content. The cache has a one-hour lifetime and fixed limits of 128 entries and 128 MiB; when either limit is reached, the oldest entries are removed first. On macOS and Linux the cache directory and files are kept at permissions `0700` and `0600`, respectively. Use `findText` to locate bounded matching passages without paging through a large page, or use `offset` and `limit` to retrieve slices intentionally.
230
230
 
231
231
  ```typescript
232
232
  get_search_content({ responseId: "abc123", urlIndex: 0 })
233
233
  get_search_content({ responseId: "abc123", url: "https://...", offset: 30000 })
234
234
  get_search_content({ responseId: "abc123", query: "original query" })
235
+ get_search_content({ responseId: "abc123", queryIndex: 0, offset: 30000 })
235
236
  get_search_content({ responseId: "abc123", urlIndex: 0, findText: "installation" })
236
237
  get_search_content({ responseId: "abc123", urlIndex: 0, findText: ["timeout", "retry"], findMode: "fuzzy" })
237
238
  ```
238
239
 
239
- `findMode` supports `exact`, `case-insensitive` (default), and `fuzzy`. Finder output is capped at 20,000 characters with match counts and nearby context. Fitting responses retain their existing format and document order. Overflow responses identify queries as `Q1`, `Q2`, and so on, list each full query once, and reserve a representative excerpt for each query whose discovered match span fits the output budget. Queries with oversized spans are listed as having no representative excerpt. `findText` cannot be combined with `offset` or `limit`. The default `limit` and maximum permitted `limit` use `maxInlineContentChars`.
240
+ `findMode` supports `exact`, `case-insensitive` (default), and `fuzzy`. Finder output is capped at 20,000 characters with match counts and nearby context. Fitting responses retain their existing format and document order. Overflow responses identify queries as `Q1`, `Q2`, and so on, list each full query once, and reserve a representative excerpt for each query whose discovered match span fits the output budget. Queries with oversized spans are listed as having no representative excerpt. `findText` cannot be combined with `offset` or `limit`. The default `limit` and maximum permitted `limit` use `maxInlineContentChars`; search-page continuation guidance shares that overall output budget and may reduce the returned stored-content characters.
240
241
 
241
242
  ### source_check
242
243
 
@@ -442,6 +443,7 @@ Config defaults to `~/.pi/agent/web-search.json` when neither `PI_CODING_AGENT_D
442
443
  "serpapiApiKey": "$SERPAPI_KEY",
443
444
  "serperApiKey": "$SERPER_API_KEY",
444
445
  "serplyApiKey": "$SERPLY_API_KEY",
446
+ "baizhiApiKey": "$BAIZHI_API_KEY",
445
447
  "brightdataApiKey": "$BRIGHTDATA_API_KEY",
446
448
  "brightdataSerpZone": "pi_serp",
447
449
  "xaiApiKey": "xai-...",
@@ -560,11 +562,13 @@ Config defaults to `~/.pi/agent/web-search.json` when neither `PI_CODING_AGENT_D
560
562
  }
561
563
  ```
562
564
 
565
+ `maxInlineContentChars` accepts integers from 1,000 through 200,000. Missing, invalid, or smaller values use the 30,000-character default so truncation and retrieval guidance always fit.
566
+
563
567
  `webSearch.allowedProviders` is an optional, non-empty search-provider allowlist with no duplicate entries. When omitted, every current search provider remains permitted. When present, explicit scalar and array requests for providers outside the list fail before any provider request, while `auto`, `all`, configured routing, `source_check`, and Curator use only listed providers. The allowlist does not make explicit-only or paid providers eligible for automatic fallback or `all`; they must still be named explicitly or included in `searchRouting.providers`. A configured `provider`/`searchProvider` or `searchRouting.providers` entry outside the allowlist is rejected as invalid configuration. This setting affects search only: it does not restrict `fetchRouting`, fetch answer models, or summary models.
564
568
 
565
569
  `summaryModel` accepts an optional thinking-level suffix, such as `anthropic/claude-haiku-4-5:low`. Supported suffixes are `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`.
566
570
 
567
- All provider API-key fields (`openaiApiKey`, `braveApiKey`, `parallelApiKey`, `tinyfishApiKey`, `search1apiApiKey`, `searchinfinityApiKey`, `queritApiKey`, `tavilyApiKey`, `jinaApiKey`, `serpdiveApiKey`, `kagiApiKey`, `bochaApiKey`, `ollamaApiKey`, `valyuApiKey`, `serpbaseApiKey`, `serpapiApiKey`, `serperApiKey`, `serplyApiKey`, `anysearchApiKey`, `xcrawlApiKey`, `xaiApiKey`, `mistralApiKey`, `brightdataApiKey`, `firecrawlApiKey`, `crawl4aiApiToken`, `exaApiKey`, `perplexityApiKey`, `geminiApiKey`, `datalabApiKey`, and `cloudflareApiKey`) accept explicit credential sources. Use `$NAME` or `${NAME}` to read one named environment variable, or prefix a trusted local shell command with `!` to resolve one value at provider request time. Escape `$$` as a literal leading `$` and `$!` as a literal leading `!`:
571
+ All provider API-key fields (`openaiApiKey`, `braveApiKey`, `parallelApiKey`, `tinyfishApiKey`, `search1apiApiKey`, `searchinfinityApiKey`, `queritApiKey`, `tavilyApiKey`, `jinaApiKey`, `serpdiveApiKey`, `kagiApiKey`, `bochaApiKey`, `ollamaApiKey`, `valyuApiKey`, `serpbaseApiKey`, `serpapiApiKey`, `serperApiKey`, `serplyApiKey`, `baizhiApiKey`, `anysearchApiKey`, `xcrawlApiKey`, `xaiApiKey`, `mistralApiKey`, `brightdataApiKey`, `firecrawlApiKey`, `crawl4aiApiToken`, `exaApiKey`, `perplexityApiKey`, `geminiApiKey`, `datalabApiKey`, and `cloudflareApiKey`) accept explicit credential sources. Use `$NAME` or `${NAME}` to read one named environment variable, or prefix a trusted local shell command with `!` to resolve one value at provider request time. Escape `$$` as a literal leading `$` and `$!` as a literal leading `!`:
568
572
 
569
573
  ```json
570
574
  {
@@ -575,7 +579,7 @@ All provider API-key fields (`openaiApiKey`, `braveApiKey`, `parallelApiKey`, `t
575
579
  }
576
580
  ```
577
581
 
578
- This syntax applies to provider credentials only; other configuration fields are not interpolated. `firecrawlApiKey`, `crawl4aiApiToken`, `kagiApiKey`, `ollamaApiKey`, `valyuApiKey`, `serpbaseApiKey`, `serpapiApiKey`, `serperApiKey`, `serplyApiKey`, `mistralApiKey`, and `brightdataApiKey` use the same credential-source rules, while `braveBaseUrl`, `exaBaseUrl`, `tavilyBaseUrl`, `firecrawlBaseUrl`, `firecrawlApiVersion`, `firecrawlFreshScrape`, `crawl4aiBaseUrl`, `brightdataSerpZone`, and `brightdataUnlockerZone` are literal config values.
582
+ This syntax applies to provider credentials only; other configuration fields are not interpolated. `firecrawlApiKey`, `crawl4aiApiToken`, `kagiApiKey`, `ollamaApiKey`, `valyuApiKey`, `serpbaseApiKey`, `serpapiApiKey`, `serperApiKey`, `serplyApiKey`, `baizhiApiKey`, `mistralApiKey`, and `brightdataApiKey` use the same credential-source rules, while `braveBaseUrl`, `exaBaseUrl`, `tavilyBaseUrl`, `firecrawlBaseUrl`, `firecrawlApiVersion`, `firecrawlFreshScrape`, `crawl4aiBaseUrl`, `brightdataSerpZone`, and `brightdataUnlockerZone` are literal config values.
579
583
 
580
584
  A command source is not run while the extension loads or registers tools. Each selected provider request runs it again with a five-second timeout, a 16 KiB output limit, a minimized environment, and a one-line non-empty stdout requirement. Command text and stderr are omitted from errors. These commands are trusted local configuration, not a same-user process isolation boundary; use absolute executable paths and protect the config file. `OP_SESSION_*` and `OP_SERVICE_ACCOUNT_TOKEN`, when present in Pi's environment, are forwarded to trusted resolver commands so 1Password CLI sessions and service accounts can be reused without storing their credentials in config. For example, `"braveApiKey": "!/absolute/path/to/op read 'op://Automation/Brave/credential'"` resolves that item after you replace the executable placeholder with your trusted installation's absolute path, but the `op://` argument is not an authorization boundary: every configured resolver command that receives the token can exercise all vault and item permissions granted to its service account. Prefer a narrowly scoped service account. An explicit source overrides legacy provider environment variables and fails that provider locally rather than falling back with a stale credential. Direct Google Gemini API requests send the resolved key only in the `x-goog-api-key` header, never in the URL.
581
585
 
@@ -621,11 +625,15 @@ Bright Data Web Unlocker is a paid `fetch_content` fallback after Parallel and b
621
625
 
622
626
  **Serper.** Set `serperApiKey` or `SERPER_API_KEY` and select `provider: "serper"` to query Serper's Google Search API. Serper is explicit-only: it is never chosen by `auto` or `provider: "all"`, but it can be configured as the named provider or added to `searchRouting`.
623
627
 
628
+ **Baizhi.** Set `BAIZHI_API_KEY` (or `baizhiApiKey: "$BAIZHI_API_KEY"` in `web-search.json`) and explicitly select `provider: "baizhi"` to call [Baizhi Cloud Agent Toolkit](https://baizhi.cloud/landing/agent-toolkit)'s `websearch_search` over Streamable HTTP MCP. It is excluded from automatic selection and `all`; you may explicitly configure it as the default or in `searchRouting`. The client initializes an MCP session, sends your own key only in the Bearer header, and closes the session after each search. Requests and response bodies share a 60-second deadline and respect cancellation. Redirects are rejected. Queries and filters are sent to the hosted commercial service, and calls may consume credits; review your account permissions and balance. Domain include/exclude filters map to the tool's `filter` object; recency maps to `time_range`, which defaults to `month`. Generated summaries are disabled.
629
+
630
+ The Baizhi input contract is based on the [published September 16 discovery snapshot](https://github.com/ct-jaryn/baizhi-agent-toolkit/blob/b774c117369b8da2a1e42c7d01bf9535bb9a41c6/tests/fixtures/tools-history-2026-09-16.json); it has no output schema. This first version preserves MCP text and structured content as the provider answer rather than guessing result item fields. Consequently it does not populate source cards or fetch pages with `includeContent`. Tests use a synthetic MCP backend; live service results and billing have not been validated. This integration does not imply the hosted backend is open source.
631
+
624
632
  **Serply.** Set `serplyApiKey` or `SERPLY_API_KEY` and select `provider: "serply"` to query [Serply](https://serply.io)'s Google Search API. Serply is explicit-only: it is never chosen by `auto` or `provider: "all"`, but it can be configured as the named provider or added to `searchRouting`. The key is sent in the `X-Api-Key` request header. Domain filters are sent as Google `site:` clauses and reapplied locally; recency maps to Google's `tbs` time filter. Each request consumes Serply search credits; see the [API documentation](https://serply.io/docs).
625
633
 
626
634
  **Parallel MCP.** Select `provider: "parallel-mcp"` to use Parallel Search MCP without an API key, or add it to `searchRouting`. It is explicit-only and is never chosen by `auto` or `provider: "all"`; the existing `parallel` provider remains the key-required REST API. A configured `parallelApiKey` or `PARALLEL_API_KEY` is sent as an optional Bearer token for higher MCP limits. To use MCP `web_fetch`, add `parallel-mcp` to `fetchRouting.providers` and set `fetchRouting.allowRemoteHostedProviders` to `true`; it is not part of the default fetch route.
627
635
 
628
- Without an explicit `$` or `!` source, `OPENAI_API_KEY`, `BRAVE_API_KEY`, `PARALLEL_API_KEY`, `TINYFISH_API_KEY`, `SEARCH1API_KEY`, `SEARCHINFINITY_API_KEY`, `QUERIT_API_KEY`, `TAVILY_API_KEY`, `JINA_API_KEY`, `SERPDIVE_API_KEY`, `KAGI_API_KEY`, `BOCHA_API_KEY`, `OLLAMA_API_KEY`, `SERPBASE_API_KEY`, `SERPAPI_KEY`, `SERPER_API_KEY`, `SERPLY_API_KEY`, `ANYSEARCH_API_KEY`, `XCRAWL_API_KEY`, `VALYU_API_KEY`, `XAI_API_KEY`, `MISTRAL_API_KEY`, `BRIGHTDATA_API_KEY`, `FIRECRAWL_API_KEY`, `CRAWL4AI_API_TOKEN`, `EXA_API_KEY`, `GEMINI_API_KEY`, `DATALAB_API_KEY`, `DATALAB_PROCESSING_LOCATION`, `DATALAB_MODE`, `DATALAB_API_BASE`, `PERPLEXITY_API_KEY`, `GOOGLE_GEMINI_BASE_URL`, and `CLOUDFLARE_API_KEY` env vars retain their existing precedence over literal config file values. `openaiResponsesUrl` can point OpenAI `web_search` and `source_check` at a third-party gateway that supports the OpenAI Responses API and web search tool; it is an explicit endpoint override, not derived from Pi model provider settings, and defaults to `https://api.openai.com/v1/responses`. `openaiSearchModel` pins the model id used for OpenAI `web_search`, bypassing automatic selection (newest terra-tier model); the id is sent verbatim with whichever OpenAI auth resolves, so gateway-only model ids work too. `xaiSearchModel` similarly pins the xAI search model. `openaiSearchProviders` sets which Pi model providers OpenAI `web_search` resolves login credentials from, in priority order; it defaults to `["openai-codex", "openai"]`, entries that are not registered or not signed in are skipped, and an empty array skips Pi credentials entirely so the `openaiApiKey` / `OPENAI_API_KEY` fallback applies. Useful for choosing between multiple Codex accounts (for example a second account registered by an extension) or forcing API-key billing while signed into Codex. Configured Exa API keys use Exa's own account limits directly; any legacy local `exa-usage.json` file is ignored. `GOOGLE_GEMINI_BASE_URL` overrides the Gemini API host for Gemini generate-content calls such as search, URL context, YouTube, and local video analysis. Set it to a bare host with no trailing slash and no version segment, for example `https://my-gateway.example.com/gemini`; `geminiBaseUrl` is the config-file equivalent. When the configured host contains `gateway.ai.cloudflare.com`, authentication uses `cf-aig-authorization: Bearer <token>` from `CLOUDFLARE_API_KEY` or `cloudflareApiKey`, and `GEMINI_API_KEY` is not required for generate-content calls. Alternatively, set `geminiAuth` to `"adc"` to authenticate Gemini generate-content calls with Google Application Default Credentials (ADC) instead of an API key; calls go to the Vertex AI endpoint (`aiplatform.googleapis.com`) with an OAuth bearer token minted from the ADC file (`GOOGLE_APPLICATION_CREDENTIALS` or `~/.config/gcloud/application_default_credentials.json`, i.e. `gcloud auth application-default login`). `geminiProject`/`geminiLocation` set the Vertex project and location and fall back to the `GOOGLE_CLOUD_PROJECT`/`GOOGLE_CLOUD_LOCATION` (or `GCLOUD_PROJECT`) env vars; project and location are required. ADC supports `authorized_user` (OAuth refresh token) and `service_account` (JWT assertion) credential files, and tokens are cached and refreshed from expiry. ADC mode covers search, URL context, and PDF/inline-data extraction; YouTube and local video analysis still go through the Gemini Files API, so they fall back to Gemini Web unless a `GEMINI_API_KEY` is also configured. The access token is treated as a credential and is redacted from errors. Local video file upload still uses Google's Files API directly, so gateway-only video extraction falls back to Gemini Web unless a `GEMINI_API_KEY` is also configured. `provider` or `searchProvider` sets the default search provider and is used when a tool call omits `provider` or sends `"auto"`: `"all"`, `"openai"`, `"brave"`, `"parallel"`, `"parallel-mcp"`, `"tinyfish"`, `"search1api"`, `"searchinfinity"`, `"querit"`, `"tavily"`, `"firecrawl"`, `"jina"`, `"serpdive"`, `"kagi"`, `"bocha"`, `"ollama"`, `"anysearch"`, `"xcrawl"`, `"valyu"`, `"xai"`, `"mistral"`, `"brightdata"`, `"serpbase"`, `"serpapi"`, `"serper"`, `"serply"`, `"searxng"`, `"exa"`, `"perplexity"`, or `"gemini"`. Parallel MCP, AnySearch, XCrawl, Valyu, xAI, Mistral, Bright Data, SerpBase, SerpApi, Serper, and Serply are never selected by `auto`; choose them explicitly or place them in `searchRouting`. If either single-provider field is configured, it takes precedence over `searchRouting`. Otherwise, `searchRouting` can opt into an ordered `providers` list and an explicit `fallbackOn` list containing `"transient"`, `"quota"`, `"network"`, and/or `"invalid-response"`; only those typed failures continue to the next available candidate. `"all"` is not valid inside `searchRouting.providers`, because that list defines sequential fallback rather than multi-provider aggregation. Named providers remain strict, and exhausted routes return per-provider diagnostics. `provider` can also be a non-empty array of named providers such as `["brave", "exa"]`; those providers run concurrently using the same aggregation path as `"all"`, while `"auto"` and `"all"` are invalid inside arrays. Random, weighted, sticky, and cooldown routing are not enabled. This is also updated automatically when you change the provider in the curator UI. Set `webSearch.enabled` to `false` to unregister the configured search and source-check tools while leaving fetch/content tools available. `toolNames` can opt into alternate public tool names for environments where another extension or model reserves the defaults, without changing behavior: `webSearch`, `sourceCheck`, `fetchContent`, and `getSearchContent` default to `web_search`, `source_check`, `fetch_content`, and `get_search_content`. `workflow` sets the default search workflow: `"none"` (default; raw results, no curator), `"summary-review"` (opens curator with an auto-generated summary draft), or `"auto-summary"` (returns a model-generated summary without opening the browser curator). Overridden per-call via the `workflow` parameter on the configured search tool, or toggled at runtime with `/curator`. `browserCookies.profile` pins Gemini Web cookie lookup to a specific Chromium profile. When omitted, detected Chromium profiles are scanned in stable order and the first profile containing the required Gemini cookies is used. macOS discovery supports Helium, Chrome, Brave, and Arc; Linux discovery supports Chromium and Chrome. `allowBrowserCookies` enables Chromium cookie extraction for Gemini Web; it defaults to `false` to avoid browser data access and surprise macOS Keychain prompts. You can also set `PI_ALLOW_BROWSER_COOKIES=1`. Cookie databases are copied to a temporary read-only working copy; the reader uses `node:sqlite` when available and otherwise tries the `sqlite3` CLI or Python's standard-library SQLite module. `searchModel` overrides the Gemini API model used by the configured search tool without changing URL, YouTube, or video extraction defaults. Gemini API grounded search uses `gemini-3.6-flash` by default; set `searchModel` to choose another model. Gemini Web browser-cookie fallback uses its separate `gemini-3.1-pro` default because Gemini Web relies on private header values; explicitly configured unsupported Web models fail instead of silently falling back to 2.5 Flash. `summaryModel` sets the default model used for generating summary drafts in the curator UI and `auto-summary` mode (e.g. `"anthropic/claude-haiku-4-5"`, `"openai-codex/gpt-5.3-codex-spark"`, or `"openrouter/nvidia/nemotron-3-super-120b-a12b:free"`). Preferred summary and query-rewrite models also resolve through routed provider registrations such as OpenRouter when the native provider is unavailable. When Pi `enabledModels` is configured, summaries are limited to that allowlist; if no enabled summary model is available, the tool returns a deterministic summary instead of calling an unrelated model. `summaryGenerationDeadlineMs` sets the maximum time for one summary model attempt in the curator UI and `auto-summary` mode. It defaults to `30000`, must be a positive integer, and is capped at `600000`. `maxInlineContentChars` sets the direct `fetch_content` content slice and the default and maximum `get_search_content` slice. It defaults to `30000`, must be a positive integer, and is capped at `200000`; full fetched content remains stored for later retrieval. `curatorTimeoutSeconds` controls the initial curator idle timeout (default `20`, max `600`); users can still adjust the timer in the curator UI. `ssrf.allowRanges` lists CIDR ranges (e.g. `"198.18.0.0/15"`, `"fd00::/8"`) exempted from the SSRF guard that otherwise blocks private/reserved IP ranges. This unblocks `fetch_content`/`web_search` on hosts whose network proxy runs in TUN + fake-IP mode (Surge, Clash, Mihomo, Stash, ...), where public domains resolve into a synthetic reserved range. It is **off by default** — the guard stays fully enabled unless you list ranges here. Use the narrowest range that covers your proxy's fake-IP pool. All-address CIDRs such as `0.0.0.0/0` and `::/0` are rejected. `ssrf.trustEnvProxy` is a separate opt-in for sandboxed environments with valid HTTP(S) proxy env vars; it skips local DNS preflight only for proxied hostnames and still blocks localhost, literal private IP targets, and `NO_PROXY` matches. It does not configure proxy transport.
636
+ Without an explicit `$` or `!` source, `OPENAI_API_KEY`, `BRAVE_API_KEY`, `PARALLEL_API_KEY`, `TINYFISH_API_KEY`, `SEARCH1API_KEY`, `SEARCHINFINITY_API_KEY`, `QUERIT_API_KEY`, `TAVILY_API_KEY`, `JINA_API_KEY`, `SERPDIVE_API_KEY`, `KAGI_API_KEY`, `BOCHA_API_KEY`, `OLLAMA_API_KEY`, `SERPBASE_API_KEY`, `SERPAPI_KEY`, `SERPER_API_KEY`, `SERPLY_API_KEY`, `BAIZHI_API_KEY`, `ANYSEARCH_API_KEY`, `XCRAWL_API_KEY`, `VALYU_API_KEY`, `XAI_API_KEY`, `MISTRAL_API_KEY`, `BRIGHTDATA_API_KEY`, `FIRECRAWL_API_KEY`, `CRAWL4AI_API_TOKEN`, `EXA_API_KEY`, `GEMINI_API_KEY`, `DATALAB_API_KEY`, `DATALAB_PROCESSING_LOCATION`, `DATALAB_MODE`, `DATALAB_API_BASE`, `PERPLEXITY_API_KEY`, `GOOGLE_GEMINI_BASE_URL`, and `CLOUDFLARE_API_KEY` env vars retain their existing precedence over literal config file values. `openaiResponsesUrl` can point OpenAI `web_search` and `source_check` at a third-party gateway that supports the OpenAI Responses API and web search tool; it is an explicit endpoint override, not derived from Pi model provider settings, and defaults to `https://api.openai.com/v1/responses`. `openaiSearchModel` pins the model id used for OpenAI `web_search`, bypassing automatic selection (newest terra-tier model); the id is sent verbatim with whichever OpenAI auth resolves, so gateway-only model ids work too. `xaiSearchModel` similarly pins the xAI search model. `openaiSearchProviders` sets which Pi model providers OpenAI `web_search` resolves login credentials from, in priority order; it defaults to `["openai-codex", "openai"]`, entries that are not registered or not signed in are skipped, and an empty array skips Pi credentials entirely so the `openaiApiKey` / `OPENAI_API_KEY` fallback applies. Useful for choosing between multiple Codex accounts (for example a second account registered by an extension) or forcing API-key billing while signed into Codex. Configured Exa API keys use Exa's own account limits directly; any legacy local `exa-usage.json` file is ignored. `GOOGLE_GEMINI_BASE_URL` overrides the Gemini API host for Gemini generate-content calls such as search, URL context, YouTube, and local video analysis. Set it to a bare host with no trailing slash and no version segment, for example `https://my-gateway.example.com/gemini`; `geminiBaseUrl` is the config-file equivalent. When the configured host contains `gateway.ai.cloudflare.com`, authentication uses `cf-aig-authorization: Bearer <token>` from `CLOUDFLARE_API_KEY` or `cloudflareApiKey`, and `GEMINI_API_KEY` is not required for generate-content calls. Alternatively, set `geminiAuth` to `"adc"` to authenticate Gemini generate-content calls with Google Application Default Credentials (ADC) instead of an API key; calls go to the Vertex AI endpoint (`aiplatform.googleapis.com`) with an OAuth bearer token minted from the ADC file (`GOOGLE_APPLICATION_CREDENTIALS` or `~/.config/gcloud/application_default_credentials.json`, i.e. `gcloud auth application-default login`). `geminiProject`/`geminiLocation` set the Vertex project and location and fall back to the `GOOGLE_CLOUD_PROJECT`/`GOOGLE_CLOUD_LOCATION` (or `GCLOUD_PROJECT`) env vars; project and location are required. ADC supports `authorized_user` (OAuth refresh token) and `service_account` (JWT assertion) credential files, and tokens are cached and refreshed from expiry. ADC mode covers search, URL context, and PDF/inline-data extraction; YouTube and local video analysis still go through the Gemini Files API, so they fall back to Gemini Web unless a `GEMINI_API_KEY` is also configured. The access token is treated as a credential and is redacted from errors. Local video file upload still uses Google's Files API directly, so gateway-only video extraction falls back to Gemini Web unless a `GEMINI_API_KEY` is also configured. `provider` or `searchProvider` sets the default search provider and is used when a tool call omits `provider` or sends `"auto"`: `"all"`, `"openai"`, `"brave"`, `"parallel"`, `"parallel-mcp"`, `"tinyfish"`, `"search1api"`, `"searchinfinity"`, `"querit"`, `"tavily"`, `"firecrawl"`, `"jina"`, `"serpdive"`, `"kagi"`, `"bocha"`, `"ollama"`, `"anysearch"`, `"xcrawl"`, `"valyu"`, `"xai"`, `"mistral"`, `"brightdata"`, `"serpbase"`, `"serpapi"`, `"serper"`, `"serply"`, `"baizhi"`, `"searxng"`, `"exa"`, `"perplexity"`, or `"gemini"`. Parallel MCP, AnySearch, XCrawl, Valyu, xAI, Mistral, Bright Data, SerpBase, SerpApi, Serper, Serply, and Baizhi are never selected by `auto`; choose them explicitly or place them in `searchRouting`. If either single-provider field is configured, it takes precedence over `searchRouting`. Otherwise, `searchRouting` can opt into an ordered `providers` list and an explicit `fallbackOn` list containing `"transient"`, `"quota"`, `"network"`, and/or `"invalid-response"`; only those typed failures continue to the next available candidate. `"all"` is not valid inside `searchRouting.providers`, because that list defines sequential fallback rather than multi-provider aggregation. Named providers remain strict, and exhausted routes return per-provider diagnostics. `provider` can also be a non-empty array of named providers such as `["brave", "exa"]`; those providers run concurrently using the same aggregation path as `"all"`, while `"auto"` and `"all"` are invalid inside arrays. Random, weighted, sticky, and cooldown routing are not enabled. This is also updated automatically when you change the provider in the curator UI. Set `webSearch.enabled` to `false` to unregister the configured search and source-check tools while leaving fetch/content tools available. `toolNames` can opt into alternate public tool names for environments where another extension or model reserves the defaults, without changing behavior: `webSearch`, `sourceCheck`, `fetchContent`, and `getSearchContent` default to `web_search`, `source_check`, `fetch_content`, and `get_search_content`. `workflow` sets the default search workflow: `"none"` (default; raw results, no curator), `"summary-review"` (opens curator with an auto-generated summary draft), or `"auto-summary"` (returns a model-generated summary without opening the browser curator). Overridden per-call via the `workflow` parameter on the configured search tool, or toggled at runtime with `/curator`. `browserCookies.profile` pins Gemini Web cookie lookup to a specific Chromium profile. When omitted, detected Chromium profiles are scanned in stable order and the first profile containing the required Gemini cookies is used. macOS discovery supports Helium, Chrome, Brave, and Arc; Linux discovery supports Chromium and Chrome. `allowBrowserCookies` enables Chromium cookie extraction for Gemini Web; it defaults to `false` to avoid browser data access and surprise macOS Keychain prompts. You can also set `PI_ALLOW_BROWSER_COOKIES=1`. Cookie databases are copied to a temporary read-only working copy; the reader uses `node:sqlite` when available and otherwise tries the `sqlite3` CLI or Python's standard-library SQLite module. `searchModel` overrides the Gemini API model used by the configured search tool without changing URL, YouTube, or video extraction defaults. Gemini API grounded search uses `gemini-3.6-flash` by default; set `searchModel` to choose another model. Gemini Web browser-cookie fallback uses its separate `gemini-3.1-pro` default because Gemini Web relies on private header values; explicitly configured unsupported Web models fail instead of silently falling back to 2.5 Flash. `summaryModel` sets the default model used for generating summary drafts in the curator UI and `auto-summary` mode (e.g. `"anthropic/claude-haiku-4-5"`, `"openai-codex/gpt-5.3-codex-spark"`, or `"openrouter/nvidia/nemotron-3-super-120b-a12b:free"`). Preferred summary and query-rewrite models also resolve through routed provider registrations such as OpenRouter when the native provider is unavailable. When Pi `enabledModels` is configured, summaries are limited to that allowlist; if no enabled summary model is available, the tool returns a deterministic summary instead of calling an unrelated model. `summaryGenerationDeadlineMs` sets the maximum time for one summary model attempt in the curator UI and `auto-summary` mode. It defaults to `30000`, must be a positive integer, and is capped at `600000`. `maxInlineContentChars` sets the direct `fetch_content` content slice and the default and maximum `get_search_content` slice. It defaults to `30000`, must be a positive integer, and is capped at `200000`; full fetched content remains stored for later retrieval. `curatorTimeoutSeconds` controls the initial curator idle timeout (default `20`, max `600`); users can still adjust the timer in the curator UI. `ssrf.allowRanges` lists CIDR ranges (e.g. `"198.18.0.0/15"`, `"fd00::/8"`) exempted from the SSRF guard that otherwise blocks private/reserved IP ranges. This unblocks `fetch_content`/`web_search` on hosts whose network proxy runs in TUN + fake-IP mode (Surge, Clash, Mihomo, Stash, ...), where public domains resolve into a synthetic reserved range. It is **off by default** — the guard stays fully enabled unless you list ranges here. Use the narrowest range that covers your proxy's fake-IP pool. All-address CIDRs such as `0.0.0.0/0` and `::/0` are rejected. `ssrf.trustEnvProxy` is a separate opt-in for sandboxed environments with valid HTTP(S) proxy env vars; it skips local DNS preflight only for proxied hostnames and still blocks localhost, literal private IP targets, and `NO_PROXY` matches. It does not configure proxy transport.
629
637
  ### Kimi Code Plan
630
638
 
631
639
  Run `/login kimi-coding` in Pi and complete sign-in for an active Kimi Code Plan. Then select `provider: "kimi"`, include `"kimi"` in an explicit provider array, or add it to `searchRouting.providers`. The extension resolves a model with provider `kimi-coding` from Pi's model registry and reuses Pi's refreshed OAuth credential; no Moonshot Open Platform key is configured here.
@@ -636,7 +644,7 @@ Kimi is explicit-only: it is never chosen by `auto` and never participates in `p
636
644
 
637
645
  ### All providers
638
646
 
639
- Set `provider: "all"` on `web_search` or `source_check`, or configure `"provider": "all"` as the default, to run the same query against every eligible search provider simultaneously. Parallel MCP, DuckDuckGo, Kimi, AnySearch, XCrawl, Valyu, xAI, Mistral, Bright Data, SerpBase, SerpApi, Serper, and Serply are always excluded because they are explicit-only; Bright Data, SerpBase, SerpApi, Serper, and Serply are paid Google SERP providers, while Kimi and Mistral may consume account quota or paid search-tool usage, so `all` never spends either resource without an explicit request. Exa remains eligible through its zero-config MCP path, OpenAI can use Pi auth, and other API-backed search providers participate when their API key, local endpoint, or gateway makes them available. Browser-cookie access alone does not opt Gemini into `all`; select Gemini explicitly or configure its API/gateway.
647
+ Set `provider: "all"` on `web_search` or `source_check`, or configure `"provider": "all"` as the default, to run the same query against every eligible search provider simultaneously. Parallel MCP, DuckDuckGo, Kimi, AnySearch, XCrawl, Valyu, xAI, Mistral, Bright Data, SerpBase, SerpApi, Serper, Serply, and Baizhi are always excluded because they are explicit-only; Bright Data, SerpBase, SerpApi, Serper, and Serply are paid Google SERP providers, while Baizhi, Kimi, and Mistral may consume account quota or paid search-tool usage, so `all` never spends either resource without an explicit request. Exa remains eligible through its zero-config MCP path, OpenAI can use Pi auth, and other API-backed search providers participate when their API key, local endpoint, or gateway makes them available. Browser-cookie access alone does not opt Gemini into `all`; select Gemini explicitly or configure its API/gateway.
640
648
 
641
649
  Successful provider answers are preserved separately while source URLs and inline content are deduplicated, and one provider failure does not discard the other results. If every participating provider fails, the tool returns per-provider diagnostics. Configured Firecrawl participates in `all` like other eligible providers. In the Curator, **All** can also be selected like the other provider buttons. Each participating provider gets its own result card, including a provider badge and independent selection checkbox; failed providers get their own disabled error card. The final summary is generated from the selected provider cards and is what Pi receives. Outside the Curator, the same provider answers remain available as labeled sections in one tool response.
642
650
 
@@ -975,6 +983,10 @@ Remote curator sessions print the URL instead of trying to open a browser by def
975
983
 
976
984
  When `false`, a requested Curator session never tries to open a Glimpse window or a browser and always prints the URL for you to open manually. For requested local-only Curator sessions it defaults to `true`; remote curator sessions print the URL unless you set `autoOpenBrowser: true` explicitly. This is worth setting locally when you would rather paste the link into a specific browser than have one launched for you. It changes nothing about where the server binds; that is `curatorRemote`'s job alone.
977
985
 
986
+ ### Tool activation
987
+
988
+ Pi starts normal sessions with the compact `web_enable` tool. Its guidance lists only the web capabilities enabled in `web-search.json`. The model can call it without a human toggle; the configured search, source-check, fetch, and stored-content tools then appear on the immediately following model request. Disabled tools stay unregistered, and alternate `toolNames` are preserved. Warm and resumed sessions retain their recorded tool selection.
989
+
978
990
  ### Shortcuts
979
991
 
980
992
  Both shortcuts are configurable via `~/.pi/agent/web-search.json`:
@@ -1033,6 +1045,7 @@ Rate limits: Perplexity is capped at 10 requests/minute (client-side). Jina Sear
1033
1045
  | `serpapi.ts` | Explicit-only SerpApi Google Search provider |
1034
1046
  | `serper.ts` | Explicit-only Serper Google SERP provider |
1035
1047
  | `serply.ts` | Explicit-only Serply Google SERP provider |
1048
+ | `baizhi.ts` | Explicit-only Baizhi Streamable HTTP MCP search provider |
1036
1049
  | `anysearch.ts` | Explicit-only AnySearch search provider |
1037
1050
  | `xcrawl.ts` | Explicit-only XCrawl search provider |
1038
1051
  | `valyu.ts` | Explicit-only Valyu research search provider |