@herbertgao/pi-extensions 2026.9.12 → 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 (90) hide show
  1. package/README.md +4 -4
  2. package/THIRD_PARTY_NOTICES.md +1 -1
  3. package/node_modules/@herbertgao/pi-bark/package.json +2 -2
  4. package/node_modules/@herbertgao/pi-cc-extensions/package.json +4 -4
  5. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +14 -0
  6. package/node_modules/@herbertgao/pi-subagents/package.json +4 -4
  7. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +5 -4
  8. package/node_modules/@herbertgao/pi-subagents/src/mention-clone.ts +39 -16
  9. package/node_modules/@herbertgao/resume-from/package.json +2 -2
  10. package/node_modules/@herbertgao/sol-pi/package.json +5 -5
  11. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +2 -0
  12. package/node_modules/@narumitw/pi-btw/README.md +21 -4
  13. package/node_modules/@narumitw/pi-btw/dist/index.ts +1668 -958
  14. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
  15. package/node_modules/@narumitw/pi-btw/docs/workflows.md +10 -2
  16. package/node_modules/@narumitw/pi-btw/package.json +1 -1
  17. package/node_modules/@narumitw/pi-btw/src/btw.ts +17 -79
  18. package/node_modules/@narumitw/pi-btw/src/conversation-context.ts +74 -0
  19. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +196 -7
  20. package/node_modules/@narumitw/pi-btw/src/main-thread-updates.ts +40 -0
  21. package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -2
  22. package/node_modules/@narumitw/pi-btw/src/settings.ts +49 -0
  23. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +9 -1
  24. package/node_modules/@narumitw/pi-btw/src/workspace-layout.ts +559 -0
  25. package/node_modules/pi-mcp-adapter/CHANGELOG.md +26 -0
  26. package/node_modules/pi-mcp-adapter/README.md +39 -9
  27. package/node_modules/pi-mcp-adapter/cli.js +33 -22
  28. package/node_modules/pi-mcp-adapter/commands.ts +3 -3
  29. package/node_modules/pi-mcp-adapter/config.ts +9 -1
  30. package/node_modules/pi-mcp-adapter/dist/config.js +9 -1
  31. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
  32. package/node_modules/pi-mcp-adapter/dist/jev-client.d.ts +1 -2
  33. package/node_modules/pi-mcp-adapter/dist/jev-client.js +59 -32
  34. package/node_modules/pi-mcp-adapter/dist/jev-client.js.map +1 -1
  35. package/node_modules/pi-mcp-adapter/dist/jev-contracts.d.ts +1 -1
  36. package/node_modules/pi-mcp-adapter/dist/jev-key-store.d.ts +33 -4
  37. package/node_modules/pi-mcp-adapter/dist/jev-key-store.js +148 -28
  38. package/node_modules/pi-mcp-adapter/dist/jev-key-store.js.map +1 -1
  39. package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js +1 -2
  40. package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js.map +1 -1
  41. package/node_modules/pi-mcp-adapter/dist/types.d.ts +7 -1
  42. package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
  43. package/node_modules/pi-mcp-adapter/index.ts +51 -38
  44. package/node_modules/pi-mcp-adapter/jev-client.ts +58 -31
  45. package/node_modules/pi-mcp-adapter/jev-contracts.ts +2 -2
  46. package/node_modules/pi-mcp-adapter/jev-key-store.ts +144 -31
  47. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
  48. package/node_modules/pi-mcp-adapter/mcp-code.ts +6 -6
  49. package/node_modules/pi-mcp-adapter/package.json +5 -5
  50. package/node_modules/pi-mcp-adapter/semantic-search.ts +3 -3
  51. package/node_modules/pi-mcp-adapter/types.ts +7 -1
  52. package/node_modules/pi-multi-account/CHANGELOG.md +29 -0
  53. package/node_modules/pi-multi-account/README.md +39 -11
  54. package/node_modules/pi-multi-account/index.ts +371 -96
  55. package/node_modules/pi-multi-account/package.json +5 -4
  56. package/node_modules/pi-multi-account/provider-payload-stream.ts +36 -28
  57. package/node_modules/pi-multi-account/usage.ts +31 -2
  58. package/node_modules/pi-typesafe/README.md +3 -1
  59. package/node_modules/pi-typesafe/dist/auth.d.ts +10 -3
  60. package/node_modules/pi-typesafe/dist/auth.js +13 -7
  61. package/node_modules/pi-typesafe/dist/backends.d.ts +41 -0
  62. package/node_modules/pi-typesafe/dist/backends.js +59 -0
  63. package/node_modules/pi-typesafe/dist/client.d.ts +4 -10
  64. package/node_modules/pi-typesafe/dist/client.js +72 -44
  65. package/node_modules/pi-typesafe/dist/credentials.d.ts +11 -5
  66. package/node_modules/pi-typesafe/dist/credentials.js +15 -8
  67. package/node_modules/pi-typesafe/dist/errors.d.ts +10 -2
  68. package/node_modules/pi-typesafe/dist/errors.js +28 -7
  69. package/node_modules/pi-typesafe/dist/extension.js +6 -2
  70. package/node_modules/pi-typesafe/dist/index.d.ts +1 -1
  71. package/node_modules/pi-typesafe/dist/index.js +1 -1
  72. package/node_modules/pi-typesafe/dist/login.d.ts +13 -7
  73. package/node_modules/pi-typesafe/dist/login.js +17 -8
  74. package/node_modules/pi-typesafe/dist/schema.js +19 -13
  75. package/node_modules/pi-typesafe/package.json +4 -4
  76. package/node_modules/pi-web-access/CHANGELOG.md +22 -0
  77. package/node_modules/pi-web-access/README.md +21 -8
  78. package/node_modules/pi-web-access/baizhi.ts +124 -0
  79. package/node_modules/pi-web-access/crawl4ai.ts +2 -10
  80. package/node_modules/pi-web-access/curator-page.ts +3 -1
  81. package/node_modules/pi-web-access/curator-server.ts +1 -0
  82. package/node_modules/pi-web-access/dist/index.js +540 -220
  83. package/node_modules/pi-web-access/firecrawl.ts +2 -10
  84. package/node_modules/pi-web-access/gemini-search.ts +5 -10
  85. package/node_modules/pi-web-access/index.ts +134 -23
  86. package/node_modules/pi-web-access/package.json +5 -1
  87. package/node_modules/pi-web-access/storage.ts +1 -0
  88. package/node_modules/pi-web-access/tool-activation.ts +134 -0
  89. package/node_modules/pi-web-access/utils.ts +9 -1
  90. package/package.json +14 -14
@@ -1,6 +1,7 @@
1
1
  import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
+ import { DEFAULT_BACKEND, TYPESAFE_KEY_ENV, backendConfig, usesTypesafeKey } from "./backends.js";
4
5
  import { TypeSafeIntegrationError } from "./errors.js";
5
6
  /** Mirrors Pi's agent directory rule; every pi-typesafe file lives in this one directory. */
6
7
  export function piTypesafeDir() {
@@ -44,13 +45,19 @@ export function readStoredApiKey() {
44
45
  }
45
46
  }
46
47
  /**
47
- * What the environment, the login store, and file permissions add up to right now. Never throws; the "unusable" kind
48
- * carries the user-facing reason (a stored key that other local users can read).
48
+ * What the environment, the login store, and file permissions add up to right now for one judgment backend. Never
49
+ * throws; the "unusable" kind carries the user-facing reason (a stored key that other local users can read).
50
+ * The TypeSafe backend reads `TYPESAFE_API_KEY`, then the login store. Every other backend reads only its own
51
+ * environment variable, because the store holds a TypeSafe key and a login verifies against api.typesafe.ai.
49
52
  */
50
- export function keySituation() {
51
- const fromEnvironment = process.env.TYPESAFE_API_KEY?.trim();
53
+ export function keySituation(backend = DEFAULT_BACKEND) {
54
+ const config = backendConfig(backend);
55
+ const keyEnv = config.keyEnv ?? TYPESAFE_KEY_ENV;
56
+ const fromEnvironment = process.env[keyEnv]?.trim();
52
57
  if (fromEnvironment)
53
- return { kind: "environment", key: fromEnvironment };
58
+ return { kind: "environment", key: fromEnvironment, keyEnv };
59
+ if (!usesTypesafeKey(config))
60
+ return { kind: "missing" };
54
61
  const path = credentialsPath();
55
62
  try {
56
63
  const key = readStoredApiKey();
@@ -66,7 +73,7 @@ export function keySituation() {
66
73
  /** Short phrase naming the source; full sentences stay with the caller. */
67
74
  export function keySourceLabel(situation) {
68
75
  switch (situation.kind) {
69
- case "environment": return "TYPESAFE_API_KEY";
76
+ case "environment": return situation.keyEnv ?? TYPESAFE_KEY_ENV;
70
77
  case "stored": return "/typesafe login";
71
78
  case "missing": return "no key";
72
79
  case "unusable": return "unusable key";
@@ -78,8 +85,8 @@ export function keySourceLabel(situation) {
78
85
  * store must not be read. New code should call keySituation() instead: same precedence, never throws, and the
79
86
  * "must not be read" case arrives as `unusable` with the reason.
80
87
  */
81
- export function resolveApiKey() {
82
- const situation = keySituation();
88
+ export function resolveApiKey(backend = DEFAULT_BACKEND) {
89
+ const situation = keySituation(backend);
83
90
  if (situation.kind === "unusable")
84
91
  throw new TypeSafeIntegrationError("configuration", situation.reason);
85
92
  return situation.kind === "environment" || situation.kind === "stored" ? { key: situation.key, source: situation.kind } : undefined;
@@ -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";
@@ -11,7 +12,7 @@ const sample = {
11
12
  questions: {
12
13
  category: { type: "choice", instructions: "Which team should handle this message?", criteria: { billing: "Charges and payments", technical: "Software failures", other: "None of these" } },
13
14
  urgent: { type: "noul", instructions: "Does the sender request help today?" },
14
- frustration: { type: "score", instructions: "How frustrated does the sender sound?", criteria: ["A neutral request without expressed frustration", "Expressed frustration while remaining civil", "Explicit anger or threats"] },
15
+ frustration: { type: "score", instructions: "How frustrated does the sender sound?", criteria: ["Neutral request", "Frustrated but civil", "Angry or threatening"] },
15
16
  },
16
17
  };
17
18
  function format(result, expanded = false) {
@@ -69,6 +70,9 @@ export default function typesafeExtension(pi) {
69
70
  description: `Evaluate supplied state with independent Choice, Score, and Noul questions in one TypeSafe request. Each question judges the whole state, so when several items are involved, put each item in a named state field (e.g. \`reports.r1\`) and ask one question per item per dimension (e.g. \`r1_owner\`, \`r2_owner\`), naming the field in the instructions; never aggregate several items into one question. ${disclosure} Requires operator opt-in via /typesafe enable or PI_TYPESAFE_ENABLED=1. Limit: 32 questions, ${DEFAULT_MAX_INPUT_BYTES / 1024} KiB JSON, ${DEFAULT_MAX_REQUESTS} attempts per session; no retries.`,
70
71
  promptSnippet: "Ask batched structured questions with TypeSafe (external service; operator opt-in required)",
71
72
  promptGuidelines: [
73
+ // The payload shape is what models get wrong on the first call; the same sample the playground edits is the cheapest way to show it.
74
+ // Every session pays for this line on every tool listing, so the sample stays short.
75
+ `Request shape, all three question kinds in one call: ${JSON.stringify(sample)}`,
72
76
  "Use typesafe_evaluate only for requested semantic judgments, not calculations or exact lookups; send only the relevant permitted data.",
73
77
  "Batch independent typesafe_evaluate questions over the same state; use code or explicit permission rules for actions, never confidence as authorization.",
74
78
  "When typesafe_evaluate judges several items, give each item a named state field and ask one question per item per dimension, naming the field in the instructions; one question over many items returns an unusable blend.",
@@ -133,7 +137,7 @@ export default function typesafeExtension(pi) {
133
137
  ? `Today ${spend.today.requestsStarted} requests (${spend.today.requestsSucceeded} ok, ${spend.today.requestsFailed} failed), ${spend.today.inputTokens} input tokens, ~$${spend.today.estimatedUsd.toFixed(4)}.`
134
138
  : "";
135
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.` : "";
136
- 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");
137
141
  return;
138
142
  }
139
143
  if (action === "logout") {
@@ -1,4 +1,4 @@
1
- export { createTypeSafe, DEFAULT_MAX_REQUESTS, DECISIONS_BACKENDS } from "./client.js";
1
+ export { createTypeSafe, DEFAULT_MAX_REQUESTS, DECISIONS_BACKENDS, DEFAULT_BACKEND } from "./client.js";
2
2
  export type { TypeSafe, TypeSafeOptions, EvaluationOptions, Evaluation, UsageSnapshot, SpendReport, TypeSafeBackend, BackendConfig, } from "./client.js";
3
3
  export { ask, DEFAULT_ASK_TIMEOUT_MS } from "./ask.js";
4
4
  export type { AskAnswer, AskOptions, Judge } from "./ask.js";
@@ -1,4 +1,4 @@
1
- export { createTypeSafe, DEFAULT_MAX_REQUESTS, DECISIONS_BACKENDS } from "./client.js";
1
+ export { createTypeSafe, DEFAULT_MAX_REQUESTS, DECISIONS_BACKENDS, DEFAULT_BACKEND } from "./client.js";
2
2
  export { ask, DEFAULT_ASK_TIMEOUT_MS } from "./ask.js";
3
3
  export { fanOut, DEFAULT_CONCURRENCY, evaluateMany, evaluateAll, chunkEvaluationRequest } from "./batch.js";
4
4
  export { authState, authStatePath, clearAuthState, describeAuth, recordAuthFailure, recordAuthVerified } from "./auth.js";
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
+ import type { TypeSafeBackend } from "./backends.js";
2
3
  import type { KeySource } from "./credentials.js";
3
4
  export interface LoginResult {
4
5
  /** Where the verified key was saved. */
@@ -7,9 +8,10 @@ export interface LoginResult {
7
8
  models: number;
8
9
  }
9
10
  /**
10
- * Prompt for a key (hidden input), verify it with a model listing, and store it for every pi-typesafe consumer.
11
- * Resolves to undefined when the user cancels. Rejects with TypeSafeIntegrationError for an invalid or unverifiable key.
12
- * Refuses when TYPESAFE_API_KEY is set, because the environment would shadow the stored key.
11
+ * Prompt for a TypeSafe key (hidden input), verify it against api.typesafe.ai with a model listing, and store it for
12
+ * every pi-typesafe consumer. Resolves to undefined when the user cancels. Rejects with TypeSafeIntegrationError for an
13
+ * invalid or unverifiable key. Refuses when TYPESAFE_API_KEY is set, because the environment would shadow the stored
14
+ * key. Other backends have no login: their key is an environment variable only.
13
15
  */
14
16
  export declare function loginWithPrompt(ctx: ExtensionCommandContext): Promise<LoginResult | undefined>;
15
17
  export type EnsureApiKeyResult = {
@@ -20,8 +22,12 @@ export type EnsureApiKeyResult = {
20
22
  login: LoginResult;
21
23
  };
22
24
  /**
23
- * Use the configured key if there is one; otherwise run the login prompt. `undefined` means the user cancelled.
24
- * A store that must not be read throws `configuration` with the reason instead of prompting, so a permissions
25
- * problem stays visible; the result shape is frozen for existing callers.
25
+ * Use the configured key for the backend if there is one; otherwise run the login prompt. `undefined` means the user
26
+ * cancelled. A store that must not be read throws `configuration` with the reason instead of prompting, so a
27
+ * permissions problem stays visible; the result shape is frozen for existing callers. A backend without a login store
28
+ * (every backend except TypeSafe) throws `configuration` naming its environment variable instead of prompting, because
29
+ * the prompt would verify the key against the wrong service and store it where the TypeSafe key lives.
26
30
  */
27
- export declare function ensureApiKey(ctx: ExtensionCommandContext): Promise<EnsureApiKeyResult | undefined>;
31
+ export declare function ensureApiKey(ctx: ExtensionCommandContext, options?: {
32
+ backend?: TypeSafeBackend;
33
+ }): Promise<EnsureApiKeyResult | undefined>;
@@ -1,11 +1,13 @@
1
+ import { DEFAULT_BACKEND, backendConfig, usesTypesafeKey } from "./backends.js";
1
2
  import { createTypeSafe } from "./client.js";
2
3
  import { keySituation, normalizeApiKey, storeApiKey } from "./credentials.js";
3
4
  import { TypeSafeIntegrationError } from "./errors.js";
4
5
  import { promptForApiKey } from "./key-prompt.js";
5
6
  /**
6
- * Prompt for a key (hidden input), verify it with a model listing, and store it for every pi-typesafe consumer.
7
- * Resolves to undefined when the user cancels. Rejects with TypeSafeIntegrationError for an invalid or unverifiable key.
8
- * Refuses when TYPESAFE_API_KEY is set, because the environment would shadow the stored key.
7
+ * Prompt for a TypeSafe key (hidden input), verify it against api.typesafe.ai with a model listing, and store it for
8
+ * every pi-typesafe consumer. Resolves to undefined when the user cancels. Rejects with TypeSafeIntegrationError for an
9
+ * invalid or unverifiable key. Refuses when TYPESAFE_API_KEY is set, because the environment would shadow the stored
10
+ * key. Other backends have no login: their key is an environment variable only.
9
11
  */
10
12
  export async function loginWithPrompt(ctx) {
11
13
  if (process.env.TYPESAFE_API_KEY?.trim()) {
@@ -23,16 +25,23 @@ export async function loginWithPrompt(ctx) {
23
25
  return { path: storeApiKey(key), models: models.length };
24
26
  }
25
27
  /**
26
- * Use the configured key if there is one; otherwise run the login prompt. `undefined` means the user cancelled.
27
- * A store that must not be read throws `configuration` with the reason instead of prompting, so a permissions
28
- * problem stays visible; the result shape is frozen for existing callers.
28
+ * Use the configured key for the backend if there is one; otherwise run the login prompt. `undefined` means the user
29
+ * cancelled. A store that must not be read throws `configuration` with the reason instead of prompting, so a
30
+ * permissions problem stays visible; the result shape is frozen for existing callers. A backend without a login store
31
+ * (every backend except TypeSafe) throws `configuration` naming its environment variable instead of prompting, because
32
+ * the prompt would verify the key against the wrong service and store it where the TypeSafe key lives.
29
33
  */
30
- export async function ensureApiKey(ctx) {
31
- const situation = keySituation();
34
+ export async function ensureApiKey(ctx, options = {}) {
35
+ const backend = options.backend ?? DEFAULT_BACKEND;
36
+ const situation = keySituation(backend);
32
37
  if (situation.kind === "environment" || situation.kind === "stored")
33
38
  return { source: situation.kind };
34
39
  if (situation.kind === "unusable")
35
40
  throw new TypeSafeIntegrationError("configuration", situation.reason);
41
+ const config = backendConfig(backend);
42
+ if (!usesTypesafeKey(config)) {
43
+ throw new TypeSafeIntegrationError("configuration", `No ${config.label} key. Set ${config.keyEnv} in the environment; /typesafe login stores a TypeSafe key only.`);
44
+ }
36
45
  const login = await loginWithPrompt(ctx);
37
46
  return login ? { source: "stored", login } : undefined;
38
47
  }
@@ -5,42 +5,48 @@ import { TypeSafeIntegrationError } from "./errors.js";
5
5
  export const DEFAULT_MAX_INPUT_BYTES = 65_536;
6
6
  /** Questions one request may ask. More than this needs `chunkEvaluationRequest`, which splits and fans out. */
7
7
  export const DEFAULT_MAX_QUESTIONS = 32;
8
- // The API accepts structured descriptions, not only strings.
9
- const entry = Type.Union([
8
+ // The API accepts structured descriptions, not only strings. The schema is the only shape guidance the model gets
9
+ // before its first call, so every field the agent authors says what it means.
10
+ const entry = (description) => Type.Union([
10
11
  Type.String(),
11
12
  Type.Null(),
12
13
  Type.Array(Type.Unknown()),
13
14
  Type.Record(Type.String(), Type.Unknown()),
14
- ]);
15
- const instructions = Type.Optional(entry);
15
+ ], description === undefined ? {} : { description });
16
+ const instructions = Type.Optional(entry("One judgment about the whole state, phrased as a question or a statement."));
16
17
  const question = Type.Union([
17
18
  Type.Object({
18
- type: Type.Literal("noul"),
19
+ type: Type.Literal("noul", { description: "Yes or no: the probability the instructions hold." }),
19
20
  instructions,
20
21
  criteria: Type.Optional(Type.Union([
21
22
  Type.Null(),
22
- Type.Object({ true: Type.Optional(entry), false: Type.Optional(entry) }, { additionalProperties: false }),
23
- ])),
23
+ Type.Object({ true: Type.Optional(entry()), false: Type.Optional(entry()) }, { additionalProperties: false }),
24
+ ], { description: "Optional: what counts as yes and what counts as no, { true, false }." })),
24
25
  }, { additionalProperties: false }),
25
26
  Type.Object({
26
- type: Type.Literal("choice"),
27
+ type: Type.Literal("choice", { description: "Pick one criteria label." }),
27
28
  instructions,
28
- criteria: Type.Record(Type.String({ minLength: 1, maxLength: 200 }), entry, { minProperties: 1, maxProperties: 64 }),
29
+ criteria: Type.Record(Type.String({ minLength: 1, maxLength: 200 }), entry(), {
30
+ minProperties: 1,
31
+ maxProperties: 64,
32
+ description: "The options as a map from label to when it applies: { billing: \"Charges and payments\", other: null }. 1–64 entries.",
33
+ }),
29
34
  }, { additionalProperties: false }),
30
35
  Type.Object({
31
- type: Type.Literal("score"),
36
+ type: Type.Literal("score", { description: "Rate against the ordered criteria levels." }),
32
37
  instructions,
33
- criteria: Type.Array(entry, { minItems: 2, maxItems: 32 }),
38
+ criteria: Type.Array(entry(), { minItems: 2, maxItems: 32, description: "Ordered rubric levels, lowest first: [\"neutral\", \"angry\"]. 2–32 levels." }),
34
39
  }, { additionalProperties: false }),
35
40
  ]);
36
41
  /** The JSON schema used by both the Pi tool and the programmatic interface. */
37
42
  export const evaluationSchema = Type.Object({
38
- state: entry,
43
+ state: entry("What to judge: text, or an object whose fields the questions name."),
39
44
  questions: Type.Record(Type.String({ minLength: 1, maxLength: 100 }), question, {
40
45
  minProperties: 1,
41
46
  maxProperties: DEFAULT_MAX_QUESTIONS,
47
+ description: "Questions keyed by a short id, as an object map, not an array: { \"urgent\": { type: \"noul\", instructions: ... } }.",
42
48
  }),
43
- model: Type.Optional(Type.String({ minLength: 1, maxLength: 100 })),
49
+ model: Type.Optional(Type.String({ minLength: 1, maxLength: 100, description: "Jev model id, e.g. jev-latest. Omit for the default." })),
44
50
  }, { additionalProperties: false });
45
51
  const usage = `Expected { state, questions: { <id>: { type: "choice", instructions, criteria: { label: description|null } } | { type: "score", instructions, criteria: [level0, level1, ...] } | { type: "noul", instructions } } }; 1–${DEFAULT_MAX_QUESTIONS} questions, Choice 1–64 options, Score 2–32 levels.`;
46
52
  /** Paths and messages only; never the submitted values. */
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-typesafe",
3
- "version": "0.6.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 |