@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
@@ -2,10 +2,9 @@ import { TypeSafeClient } from "@typesafe-ai/sdk";
2
2
  import type { McpExtensionState } from "./state.ts";
3
3
  import { isServerDisabled } from "./types.ts";
4
4
  import { combineAbortSignals } from "./runtime-owner.ts";
5
- import { resolveJevCredential, TYPESAFE_API_ORIGIN, type JevCredentialResolution } from "./jev-key-store.ts";
5
+ import { JEV_SDK_PATH, resolveJevCredential, resolveJevEndpoint, type JevCredentialResolution, type ResolvedJevEndpoint } from "./jev-key-store.ts";
6
6
  import type { JevAnswer, JevBudget, JevEvaluateInput, JevEvaluationData, JevEvaluationEnvelope, JevJson, JevQuestion, ResolvedJevSettings } from "./jev-contracts.ts";
7
7
 
8
- export { TYPESAFE_API_ORIGIN };
9
8
  export type { ResolvedJevSettings } from "./jev-contracts.ts";
10
9
 
11
10
  const DEFAULTS: ResolvedJevSettings = {
@@ -18,18 +17,28 @@ const DEFAULTS: ResolvedJevSettings = {
18
17
  const DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
19
18
 
20
19
  type JevSettingsInput = Partial<ResolvedJevSettings>;
21
- type Host = { client: TypeSafeClient };
20
+ type Host = { endpoint: ResolvedJevEndpoint; client: TypeSafeClient };
21
+ type CredentialCacheEntry = { endpoint: string; credential: Extract<JevCredentialResolution, { status: "present" }> };
22
22
  const hosts = new WeakMap<McpExtensionState, Host>();
23
- const credentials = new WeakMap<McpExtensionState, Extract<JevCredentialResolution, { status: "present" }>>();
23
+ const credentials = new WeakMap<McpExtensionState, CredentialCacheEntry>();
24
24
 
25
- function resolveCredential(state: McpExtensionState): JevCredentialResolution {
25
+ /**
26
+ * Cached per endpoint so a changed `SYSTEMONE_ENDPOINT` cannot reuse another provider's credential. Callers
27
+ * re-resolve the endpoint on every operation so a switched or newly invalid endpoint takes effect immediately.
28
+ */
29
+ function resolveCredential(state: McpExtensionState, endpoint: ResolvedJevEndpoint): JevCredentialResolution {
26
30
  const existing = credentials.get(state);
27
- if (existing) return existing;
28
- const credential = resolveJevCredential();
29
- if (credential.status === "present") credentials.set(state, credential);
31
+ if (existing && existing.endpoint === endpoint.href) return existing.credential;
32
+ const credential = resolveJevCredential(process.env, endpoint);
33
+ if (credential.status === "present") credentials.set(state, { endpoint: endpoint.href, credential });
30
34
  return credential;
31
35
  }
32
36
 
37
+ function credentialForState(state: McpExtensionState): JevCredentialResolution {
38
+ const resolution = resolveJevEndpoint();
39
+ return resolution.status === "unavailable" ? resolution : resolveCredential(state, resolution.endpoint);
40
+ }
41
+
33
42
  export function areJevSourcesAllowed(state: McpExtensionState, settings: ResolvedJevSettings, sources: Iterable<string>): boolean {
34
43
  for (const source of sources) {
35
44
  const server = state.config.mcpServers[source];
@@ -90,7 +99,7 @@ export function resolveSemanticJevSettings(
90
99
  const configured = state.config.settings?.jev;
91
100
  const settings = validateJevSettings(configured);
92
101
  if (configured === false || configured?.semanticSearch === false) return settings;
93
- const semanticSearch = settings.semanticSearch || (credentialResolver ? credentialResolver() : resolveCredential(state)).status === "present";
102
+ const semanticSearch = settings.semanticSearch || (credentialResolver ? credentialResolver() : credentialForState(state)).status === "present";
94
103
  const hasExplicitAllowlist = configured !== undefined && Object.hasOwn(configured, "allowedServers");
95
104
  const allowedServers = hasExplicitAllowlist
96
105
  ? settings.allowedServers
@@ -219,55 +228,73 @@ function validateResponse(value: unknown, input: JevEvaluateInput): JevEvaluatio
219
228
  return { answers: validated, model: response.model, usage: { inputTokens: safeNumber(usage.input_tokens, "input tokens", true), outputTokens: safeNumber(usage.output_tokens, "output tokens", true) } };
220
229
  }
221
230
 
222
- function fixedOriginFetch(input: string, init?: RequestInit): Promise<Response> {
223
- const url = new URL(input);
224
- if (url.origin !== TYPESAFE_API_ORIGIN) throw new Error("TypeSafe request origin rejected");
225
- return fetch(input, { ...init, redirect: "error" });
231
+ /**
232
+ * Pins every request to the configured endpoint: the SDK appends its own fixed path to the base URL, so this
233
+ * rewrites that path onto the configured one and then rejects anything that does not match the endpoint exactly.
234
+ * Redirects stay refused, so a provider cannot bounce a request carrying the API key somewhere else.
235
+ */
236
+ function createPinnedEndpointFetch(endpoint: ResolvedJevEndpoint): (input: string, init?: RequestInit) => Promise<Response> {
237
+ return (input, init) => {
238
+ // A replacer function keeps `$&`, `$$`, and friends in the configured path literal.
239
+ const target = input.replace(JEV_SDK_PATH, () => endpoint.path);
240
+ const url = new URL(target);
241
+ if (url.origin !== endpoint.origin || url.pathname !== endpoint.path || url.search !== "" || url.hash !== "") {
242
+ throw new Error("Jev request endpoint rejected");
243
+ }
244
+ return fetch(target, { ...init, redirect: "error" });
245
+ };
226
246
  }
227
247
  function getHost(state: McpExtensionState, settings: ResolvedJevSettings): Host | JevEvaluationEnvelope {
248
+ const resolution = resolveJevEndpoint();
249
+ if (resolution.status === "unavailable") return { ok: false, error: { code: "endpoint_unavailable", message: resolution.message } };
250
+ const { endpoint } = resolution;
228
251
  const existing = hosts.get(state);
229
- if (existing) return existing;
230
- const credential = resolveCredential(state);
231
- if (credential.status === "missing") return { ok: false, error: { code: "credential_missing", message: "TypeSafe API key is not configured." } };
252
+ if (existing && existing.endpoint.href === endpoint.href) return existing;
253
+ const credential = resolveCredential(state, endpoint);
254
+ if (credential.status === "missing") return { ok: false, error: { code: "credential_missing", message: "Jev API key is not configured." } };
232
255
  if (credential.status === "unavailable") return { ok: false, error: { code: "credential_unavailable", message: credential.message } };
233
- const host = { client: new TypeSafeClient({ apiKey: credential.apiKey, baseURL: TYPESAFE_API_ORIGIN, defaultModel: settings.model, logLevel: "off", retry: { maxRetries: settings.maxRetries }, timeout: settings.requestTimeoutMs, defaultHeaders: {}, fetch: fixedOriginFetch }) };
256
+ const host: Host = { endpoint, client: new TypeSafeClient({ apiKey: credential.apiKey, baseURL: endpoint.origin, defaultModel: settings.model, logLevel: "off", retry: { maxRetries: settings.maxRetries }, timeout: settings.requestTimeoutMs, defaultHeaders: {}, fetch: createPinnedEndpointFetch(endpoint) }) };
234
257
  hosts.set(state, host);
235
258
  return host;
236
259
  }
237
260
  function failure(error: unknown, signal: AbortSignal | undefined, timedOut: boolean): JevEvaluationEnvelope {
238
- if (timedOut) return { ok: false, error: { code: "timeout", message: "TypeSafe evaluation timed out.", retryable: true } };
239
- if (signal?.aborted || (error instanceof Error && error.name === "APIUserAbortError")) return { ok: false, error: { code: "aborted", message: "TypeSafe evaluation was aborted." } };
261
+ if (timedOut) return { ok: false, error: { code: "timeout", message: "Jev evaluation timed out.", retryable: true } };
262
+ if (signal?.aborted || (error instanceof Error && error.name === "APIUserAbortError")) return { ok: false, error: { code: "aborted", message: "Jev evaluation was aborted." } };
263
+ // Provider and gateway bodies are never echoed; only the status is used to classify the failure.
240
264
  const status = typeof error === "object" && error !== null && "status" in error ? (error as { status?: unknown }).status : undefined;
241
- if (status === 401 || status === 403) return { ok: false, error: { code: "authentication_failed", message: "TypeSafe authentication failed." } };
242
- if (status === 408) return { ok: false, error: { code: "timeout", message: "TypeSafe evaluation timed out.", retryable: true } };
243
- if (status === 429) return { ok: false, error: { code: "rate_limited", message: "TypeSafe rate limit exceeded.", retryable: true } };
244
- if (typeof status === "number" && status >= 500) return { ok: false, error: { code: "service_unavailable", message: "TypeSafe service is unavailable.", retryable: true } };
245
- if (status === 400 || status === 422) return { ok: false, error: { code: "invalid_request", message: "TypeSafe rejected the evaluation request." } };
246
- if (error instanceof Error && (error.name === "APIConnectionError" || error.name === "APITimeoutError")) return { ok: false, error: { code: "service_unavailable", message: "TypeSafe service is unavailable.", retryable: true } };
247
- return { ok: false, error: { code: "invalid_response", message: "TypeSafe returned an invalid response." } };
265
+ if (status === 401 || status === 403) return { ok: false, error: { code: "authentication_failed", message: "Jev authentication failed." } };
266
+ if (status === 402) return { ok: false, error: { code: "payment_required", message: "Jev provider requires payment for this account." } };
267
+ // A configurable endpoint makes a wrong path a configuration error, not a malformed response.
268
+ if (status === 404 || status === 405 || status === 410) return { ok: false, error: { code: "endpoint_unavailable", message: "Jev endpoint was not found; check SYSTEMONE_ENDPOINT." } };
269
+ if (status === 408) return { ok: false, error: { code: "timeout", message: "Jev evaluation timed out.", retryable: true } };
270
+ if (status === 429) return { ok: false, error: { code: "rate_limited", message: "Jev rate limit exceeded.", retryable: true } };
271
+ if (typeof status === "number" && status >= 500) return { ok: false, error: { code: "service_unavailable", message: "Jev service is unavailable.", retryable: true } };
272
+ if (typeof status === "number" && status >= 400) return { ok: false, error: { code: "invalid_request", message: `Jev rejected the evaluation request (HTTP ${status}).` } };
273
+ if (error instanceof Error && (error.name === "APIConnectionError" || error.name === "APITimeoutError")) return { ok: false, error: { code: "service_unavailable", message: "Jev service is unavailable.", retryable: true } };
274
+ return { ok: false, error: { code: "invalid_response", message: "Jev returned an invalid response." } };
248
275
  }
249
276
 
250
277
  export async function evaluateJev(state: McpExtensionState, value: JevEvaluateInput, options: { purpose: "script" | "semantic-search"; signal?: AbortSignal; budget?: JevBudget; observedSources?: readonly string[] }): Promise<JevEvaluationEnvelope> {
251
278
  let settings: ResolvedJevSettings;
252
279
  let input: JevEvaluateInput;
253
280
  try { settings = options.purpose === "semantic-search" ? resolveSemanticJevSettings(state) : validateJevSettings(state.config.settings?.jev); input = validateJevEvaluateInput(value, settings); }
254
- catch { return { ok: false, error: { code: "invalid_request", message: "Invalid TypeSafe evaluation request or settings." } }; }
255
- if ((options.purpose === "script" && !settings.scriptEvaluation) || (options.purpose === "semantic-search" && !settings.semanticSearch)) return { ok: false, error: { code: "disabled", message: "TypeSafe evaluation is disabled." } };
281
+ catch { return { ok: false, error: { code: "invalid_request", message: "Invalid Jev evaluation request or settings." } }; }
282
+ if ((options.purpose === "script" && !settings.scriptEvaluation) || (options.purpose === "semantic-search" && !settings.semanticSearch)) return { ok: false, error: { code: "disabled", message: "Jev evaluation is disabled." } };
256
283
  const sources = new Set([...(input.sources ?? []), ...(options.observedSources ?? [])]);
257
284
  if (!areJevSourcesAllowed(state, settings, sources)) return { ok: false, error: { code: "data_policy_denied", message: "Evaluation sources are not allowed by policy." } };
258
285
  const bytes = Buffer.byteLength(JSON.stringify(input));
259
- if (options.budget && !options.budget.consume(bytes)) return { ok: false, error: { code: "budget_exhausted", message: "TypeSafe evaluation budget exhausted." } };
286
+ if (options.budget && !options.budget.consume(bytes)) return { ok: false, error: { code: "budget_exhausted", message: "Jev evaluation budget exhausted." } };
260
287
  const resolved = getHost(state, settings);
261
288
  if (!("client" in resolved)) return resolved;
262
289
  const deadline = new AbortController();
263
290
  let timedOut = false;
264
- const timer = setTimeout(() => { timedOut = true; deadline.abort(new Error("TypeSafe evaluation deadline exceeded")); }, settings.requestTimeoutMs);
291
+ const timer = setTimeout(() => { timedOut = true; deadline.abort(new Error("Jev evaluation deadline exceeded")); }, settings.requestTimeoutMs);
265
292
  const signal = combineAbortSignals(options.signal, state.owner.signal, deadline.signal);
266
293
  try {
267
294
  const request = { state: input.state, questions: input.questions, model: settings.model };
268
295
  const raw = await resolved.client.systemOne(request as Parameters<TypeSafeClient["systemOne"]>[0], { ...(signal ? { signal } : {}), timeout: settings.requestTimeoutMs, retry: { maxRetries: settings.maxRetries } });
269
296
  try { return { ok: true, data: validateResponse(raw, input) }; }
270
- catch { return { ok: false, error: { code: "invalid_response", message: "TypeSafe returned an invalid response." } }; }
297
+ catch { return { ok: false, error: { code: "invalid_response", message: "Jev returned an invalid response." } }; }
271
298
  } catch (error) { return failure(error, signal, timedOut); }
272
299
  finally { clearTimeout(timer); }
273
300
  }
@@ -25,8 +25,8 @@ export interface JevEvaluationData {
25
25
 
26
26
  export type JevErrorCode =
27
27
  | "disabled" | "invalid_request" | "data_policy_denied" | "budget_exhausted"
28
- | "credential_missing" | "credential_unavailable" | "authentication_failed"
29
- | "timeout" | "aborted" | "rate_limited" | "service_unavailable" | "invalid_response";
28
+ | "endpoint_unavailable" | "credential_missing" | "credential_unavailable" | "authentication_failed"
29
+ | "payment_required" | "timeout" | "aborted" | "rate_limited" | "service_unavailable" | "invalid_response";
30
30
 
31
31
  export type JevEvaluationEnvelope =
32
32
  | { ok: true; data: JevEvaluationData }
@@ -1,20 +1,44 @@
1
1
  import { createSecureKeyringStore, type SecureKeyringStore } from "./secure-keyring.ts";
2
2
 
3
+ /** The Jev decisions endpoint used when `SYSTEMONE_ENDPOINT` is not set. */
4
+ export const JEV_DEFAULT_ENDPOINT = "https://api.typesafe.ai/v1/systemone";
5
+ const SYSTEMONE_ENDPOINT_ENV = "SYSTEMONE_ENDPOINT";
6
+ const SYSTEMONE_API_KEY_ENV = "SYSTEMONE_API_KEY";
7
+ /** TypeSafe-issued key: honored only for the default endpoint and never sent anywhere else. */
8
+ const LEGACY_TYPESAFE_API_KEY_ENV = "TYPESAFE_API_KEY";
9
+ /** Origin recorded in version 1 credential records. */
3
10
  export const TYPESAFE_API_ORIGIN = "https://api.typesafe.ai";
4
- export const JEV_KEYRING_SERVICE = "pi-mcp-adapter.service-key";
11
+ /** Version 1 credential account, still read for the default endpoint. */
5
12
  export const JEV_KEYRING_ACCOUNT = "typesafe@sha256(https://api.typesafe.ai)";
13
+ export const JEV_KEYRING_SERVICE = "pi-mcp-adapter.service-key";
14
+
15
+ const MAX_ENDPOINT_LENGTH = 512;
16
+ /** The path `@typesafe-ai/sdk` appends to whatever base URL it is given. */
17
+ export const JEV_SDK_PATH = "/v1/systemone";
18
+
19
+ export interface ResolvedJevEndpoint {
20
+ /** Canonical absolute URL sent to the provider, for example `https://opencode.ai/zen/v1/systemone`. */
21
+ readonly href: string;
22
+ /** URL origin, used as the SDK base URL. */
23
+ readonly origin: string;
24
+ /** URL path, rewritten onto the SDK's fixed path by the pinned fetch. */
25
+ readonly path: string;
26
+ }
27
+
28
+ export type JevEndpointResolution =
29
+ | { status: "resolved"; source: "environment" | "default"; endpoint: ResolvedJevEndpoint }
30
+ | { status: "unavailable"; message: string };
6
31
 
7
32
  interface StoredJevKey {
8
- version: 1;
9
- provider: "typesafe";
10
- origin: typeof TYPESAFE_API_ORIGIN;
33
+ version: 2;
34
+ endpoint: string;
11
35
  apiKey: string;
12
36
  }
13
37
 
14
38
  export class JevCredentialStoreError extends Error {
15
39
  readonly code = "JEV_CREDENTIAL_STORE_UNAVAILABLE";
16
40
  constructor(readonly operation: "read" | "write" | "remove", cause: unknown) {
17
- super(`TypeSafe API key secure credential store unavailable during ${operation}. Configure or unlock the OS credential store and retry.`, { cause });
41
+ super(`Jev API key secure credential store unavailable during ${operation}. Configure or unlock the OS credential store and retry.`, { cause });
18
42
  this.name = "JevCredentialStoreError";
19
43
  }
20
44
  }
@@ -26,54 +50,143 @@ export type JevCredentialResolution =
26
50
 
27
51
  function validateApiKey(value: unknown): string {
28
52
  if (typeof value !== "string" || value.trim().length === 0 || /[\u0000-\u001f\u007f]/.test(value)) {
29
- throw new Error("TypeSafe API key must be a non-empty string without control characters");
53
+ throw new Error("Jev API key must be a non-empty string without control characters");
30
54
  }
31
55
  return value;
32
56
  }
33
57
 
58
+ /**
59
+ * Constrains the operator-supplied endpoint to an absolute HTTPS URL without credentials, query, or fragment, so
60
+ * the pinned fetch can match it exactly and a stray value cannot redirect requests or smuggle a query string.
61
+ */
62
+ function parseEndpoint(raw: string, label: string): ResolvedJevEndpoint {
63
+ if (typeof raw !== "string" || raw.trim().length === 0) throw new Error(`${label} must be a non-empty URL`);
64
+ if (raw.length > MAX_ENDPOINT_LENGTH) throw new Error(`${label} must be at most ${MAX_ENDPOINT_LENGTH} characters`);
65
+ if (/[\u0000-\u001f\u007f\s]/.test(raw)) throw new Error(`${label} must not contain whitespace or control characters`);
66
+ let url: URL;
67
+ try { url = new URL(raw); }
68
+ catch { throw new Error(`${label} must be an absolute URL`); }
69
+ if (url.protocol !== "https:") throw new Error(`${label} must use https`);
70
+ if (url.username !== "" || url.password !== "") throw new Error(`${label} must not embed credentials`);
71
+ if (url.search !== "") throw new Error(`${label} must not include a query string`);
72
+ if (url.hash !== "") throw new Error(`${label} must not include a fragment`);
73
+ if (url.pathname === "" || url.pathname === "/") throw new Error(`${label} must include a path such as ${JEV_SDK_PATH}`);
74
+ return { href: `${url.origin}${url.pathname}`, origin: url.origin, path: url.pathname };
75
+ }
76
+
77
+ /**
78
+ * Resolves the Jev endpoint. An invalid `SYSTEMONE_ENDPOINT` resolves to `unavailable` instead of silently falling
79
+ * back to the default: sending an operator's judgment payload to a provider they did not ask for would be a data leak.
80
+ */
81
+ export function resolveJevEndpoint(env: NodeJS.ProcessEnv = process.env): JevEndpointResolution {
82
+ if (Object.hasOwn(env, SYSTEMONE_ENDPOINT_ENV)) {
83
+ try { return { status: "resolved", source: "environment", endpoint: parseEndpoint(env[SYSTEMONE_ENDPOINT_ENV] as string, SYSTEMONE_ENDPOINT_ENV) }; }
84
+ catch (error) { return { status: "unavailable", message: error instanceof Error ? error.message : `${SYSTEMONE_ENDPOINT_ENV} is invalid.` }; }
85
+ }
86
+ return { status: "resolved", source: "default", endpoint: parseEndpoint(JEV_DEFAULT_ENDPOINT, "the default Jev endpoint") };
87
+ }
88
+
89
+ /** Keyring account for an endpoint, so keys for different providers coexist instead of overwriting each other. */
90
+ export function jevKeyringAccount(endpoint: ResolvedJevEndpoint): string {
91
+ return `systemone@sha256(${endpoint.href})`;
92
+ }
93
+
34
94
  function store(): SecureKeyringStore {
35
95
  return createSecureKeyringStore(JEV_KEYRING_SERVICE);
36
96
  }
37
97
 
38
- function readStoredKey(secretStore: SecureKeyringStore = store()): string | undefined {
39
- let payload: string | undefined;
40
- try { payload = secretStore.read(JEV_KEYRING_ACCOUNT); }
41
- catch (error) { throw new JevCredentialStoreError("read", error); }
42
- if (payload === undefined) return undefined;
43
- try {
44
- const value: unknown = JSON.parse(payload);
45
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid record");
46
- const record = value as Partial<StoredJevKey>;
47
- const keys = Object.keys(record);
98
+ function requireEndpoint(endpoint: ResolvedJevEndpoint | undefined): ResolvedJevEndpoint {
99
+ if (endpoint) return endpoint;
100
+ const resolution = resolveJevEndpoint();
101
+ if (resolution.status === "unavailable") throw new Error(resolution.message);
102
+ return resolution.endpoint;
103
+ }
104
+
105
+ /**
106
+ * Version 1 records were written before the endpoint was configurable, so they are accepted only for the default
107
+ * endpoint.
108
+ */
109
+ function parseStoredKey(payload: string, endpoint: ResolvedJevEndpoint): string {
110
+ let value: unknown;
111
+ try { value = JSON.parse(payload); }
112
+ catch { throw new Error("invalid record"); }
113
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid record");
114
+ const record = value as Record<string, unknown>;
115
+ const keys = Object.keys(record);
116
+ if (record.version === 2) {
117
+ if (keys.length !== 3 || !["version", "endpoint", "apiKey"].every(key => Object.hasOwn(record, key))) throw new Error("invalid record fields");
118
+ if (record.endpoint !== endpoint.href) throw new Error("invalid or mismatched record");
119
+ return validateApiKey(record.apiKey);
120
+ }
121
+ if (record.version === 1) {
48
122
  if (keys.length !== 4 || !["version", "provider", "origin", "apiKey"].every(key => Object.hasOwn(record, key))) throw new Error("invalid record fields");
49
- if (record.version !== 1 || record.provider !== "typesafe" || record.origin !== TYPESAFE_API_ORIGIN) throw new Error("invalid or mismatched record");
123
+ if (record.provider !== "typesafe" || record.origin !== TYPESAFE_API_ORIGIN) throw new Error("invalid or mismatched record");
124
+ if (endpoint.href !== JEV_DEFAULT_ENDPOINT) throw new Error("invalid or mismatched record");
50
125
  return validateApiKey(record.apiKey);
126
+ }
127
+ throw new Error("invalid record version");
128
+ }
129
+
130
+ function readStoredKey(secretStore: SecureKeyringStore, endpoint: ResolvedJevEndpoint): string | undefined {
131
+ try {
132
+ let payload = secretStore.read(jevKeyringAccount(endpoint));
133
+ if (payload === undefined && endpoint.href === JEV_DEFAULT_ENDPOINT) payload = secretStore.read(JEV_KEYRING_ACCOUNT);
134
+ return payload === undefined ? undefined : parseStoredKey(payload, endpoint);
51
135
  } catch (error) {
52
136
  throw new JevCredentialStoreError("read", error);
53
137
  }
54
138
  }
55
139
 
56
- export function resolveJevCredential(env: NodeJS.ProcessEnv = process.env, secretStore?: SecureKeyringStore): JevCredentialResolution {
57
- if (Object.hasOwn(env, "TYPESAFE_API_KEY")) {
58
- try { return { status: "present", source: "environment", apiKey: validateApiKey(env.TYPESAFE_API_KEY) }; }
59
- catch { return { status: "unavailable", message: "TYPESAFE_API_KEY is present but invalid." }; }
140
+ export function resolveJevCredential(
141
+ env: NodeJS.ProcessEnv = process.env,
142
+ endpoint?: ResolvedJevEndpoint,
143
+ secretStore: SecureKeyringStore = store(),
144
+ ): JevCredentialResolution {
145
+ let target: ResolvedJevEndpoint;
146
+ if (endpoint) target = endpoint;
147
+ else {
148
+ const resolution = resolveJevEndpoint(env);
149
+ if (resolution.status === "unavailable") return resolution;
150
+ target = resolution.endpoint;
60
151
  }
61
- try {
62
- const apiKey = readStoredKey(secretStore ?? store());
63
- return apiKey === undefined ? { status: "missing" } : { status: "present", source: "keyring", apiKey };
64
- } catch (error) {
152
+ if (Object.hasOwn(env, SYSTEMONE_API_KEY_ENV)) {
153
+ try { return { status: "present", source: "environment", apiKey: validateApiKey(env[SYSTEMONE_API_KEY_ENV]) }; }
154
+ catch { return { status: "unavailable", message: `${SYSTEMONE_API_KEY_ENV} is present but invalid.` }; }
155
+ }
156
+ const legacySet = Object.hasOwn(env, LEGACY_TYPESAFE_API_KEY_ENV);
157
+ if (legacySet && target.href === JEV_DEFAULT_ENDPOINT) {
158
+ try { return { status: "present", source: "environment", apiKey: validateApiKey(env[LEGACY_TYPESAFE_API_KEY_ENV]) }; }
159
+ catch { return { status: "unavailable", message: `${LEGACY_TYPESAFE_API_KEY_ENV} is present but invalid.` }; }
160
+ }
161
+ let stored: string | undefined;
162
+ try { stored = readStoredKey(secretStore, target); }
163
+ catch (error) {
65
164
  if (!(error instanceof JevCredentialStoreError)) throw error;
66
165
  return { status: "unavailable", message: error.message };
67
166
  }
167
+ if (stored !== undefined) return { status: "present", source: "keyring", apiKey: stored };
168
+ // A TypeSafe-issued credential is never sent to another endpoint, but it must not mask a credential stored for
169
+ // this endpoint either, so it is only reported once nothing for this endpoint resolves.
170
+ if (legacySet) {
171
+ return {
172
+ status: "unavailable",
173
+ message: `${LEGACY_TYPESAFE_API_KEY_ENV} is a TypeSafe credential and is not sent to ${target.href}; set ${SYSTEMONE_API_KEY_ENV} for that endpoint.`,
174
+ };
175
+ }
176
+ return { status: "missing" };
68
177
  }
69
178
 
70
- export function saveJevApiKey(apiKey: string, secretStore: SecureKeyringStore = store()): void {
71
- const record: StoredJevKey = { version: 1, provider: "typesafe", origin: TYPESAFE_API_ORIGIN, apiKey: validateApiKey(apiKey) };
72
- try { secretStore.write(JEV_KEYRING_ACCOUNT, JSON.stringify(record)); }
179
+ export function saveJevApiKey(apiKey: string, endpoint?: ResolvedJevEndpoint, secretStore: SecureKeyringStore = store()): void {
180
+ const target = requireEndpoint(endpoint);
181
+ const record: StoredJevKey = { version: 2, endpoint: target.href, apiKey: validateApiKey(apiKey) };
182
+ try { secretStore.write(jevKeyringAccount(target), JSON.stringify(record)); }
73
183
  catch (error) { throw new JevCredentialStoreError("write", error); }
74
184
  }
75
185
 
76
- export function removeJevApiKey(secretStore: SecureKeyringStore = store()): void {
77
- try { secretStore.remove(JEV_KEYRING_ACCOUNT); }
78
- catch (error) { throw new JevCredentialStoreError("remove", error); }
186
+ export function removeJevApiKey(endpoint?: ResolvedJevEndpoint, secretStore: SecureKeyringStore = store()): void {
187
+ const target = requireEndpoint(endpoint);
188
+ try {
189
+ secretStore.remove(jevKeyringAccount(target));
190
+ if (target.href === JEV_DEFAULT_ENDPOINT) secretStore.remove(JEV_KEYRING_ACCOUNT);
191
+ } catch (error) { throw new JevCredentialStoreError("remove", error); }
79
192
  }
@@ -1191,9 +1191,8 @@ export async function getValidToken(
1191
1191
  }
1192
1192
  }
1193
1193
 
1194
- // No expiration info or no refresh token, assume valid
1195
1194
  authority()
1196
- return entry.tokens
1195
+ return null
1197
1196
  }
1198
1197
 
1199
1198
  /**
@@ -213,7 +213,7 @@ export async function runMcpScript(
213
213
  let evaluationAttempts = 0;
214
214
  let evaluationBytes = 0;
215
215
  let evaluationTokensRemaining = jevSettings.maxEvaluationTokensPerScript;
216
- const tokenBudgetExhausted = (): JevEvaluationEnvelope => ({ ok: false, error: { code: "budget_exhausted", message: "TypeSafe evaluation token budget exhausted." } });
216
+ const tokenBudgetExhausted = (): JevEvaluationEnvelope => ({ ok: false, error: { code: "budget_exhausted", message: "Jev evaluation token budget exhausted." } });
217
217
  const chargeEvaluationTokens = (envelope: JevEvaluationEnvelope): JevEvaluationEnvelope => {
218
218
  if (!envelope.ok) return envelope;
219
219
  const used = envelope.data.usage.inputTokens + envelope.data.usage.outputTokens;
@@ -226,16 +226,16 @@ export async function runMcpScript(
226
226
  };
227
227
  const admitEvaluation = (input: unknown): JevEvaluationEnvelope | undefined => {
228
228
  if (++evaluationAttempts > jevSettings.maxEvaluationsPerScript) {
229
- return { ok: false, error: { code: "budget_exhausted", message: "TypeSafe evaluation count budget exhausted." } };
229
+ return { ok: false, error: { code: "budget_exhausted", message: "Jev evaluation count budget exhausted." } };
230
230
  }
231
231
  if (evaluationTokensRemaining === 0) return tokenBudgetExhausted();
232
232
  let serialized: string | undefined;
233
233
  try { serialized = JSON.stringify(input); }
234
- catch { return { ok: false, error: { code: "invalid_request", message: "Invalid TypeSafe evaluation request." } }; }
235
- if (serialized === undefined) return { ok: false, error: { code: "invalid_request", message: "Invalid TypeSafe evaluation request." } };
234
+ catch { return { ok: false, error: { code: "invalid_request", message: "Invalid Jev evaluation request." } }; }
235
+ if (serialized === undefined) return { ok: false, error: { code: "invalid_request", message: "Invalid Jev evaluation request." } };
236
236
  const bytes = Buffer.byteLength(serialized, "utf8");
237
237
  if (bytes > jevSettings.maxEvaluationBytesPerScript - evaluationBytes) {
238
- return { ok: false, error: { code: "budget_exhausted", message: "TypeSafe evaluation byte budget exhausted." } };
238
+ return { ok: false, error: { code: "budget_exhausted", message: "Jev evaluation byte budget exhausted." } };
239
239
  }
240
240
  evaluationBytes += bytes;
241
241
  return undefined;
@@ -257,7 +257,7 @@ export async function runMcpScript(
257
257
  envelope = chargeEvaluationTokens(envelope);
258
258
  throwIfAborted(callSignal);
259
259
  if (!reserveIntermediateBytes(JSON.stringify(envelope))) {
260
- envelope = { ok: false, error: { code: "budget_exhausted", message: "TypeSafe evaluation exceeds the remaining mcpScript intermediate transfer budget (16 MiB per script)." } };
260
+ envelope = { ok: false, error: { code: "budget_exhausted", message: "Jev evaluation exceeds the remaining mcpScript intermediate transfer budget (16 MiB per script)." } };
261
261
  }
262
262
  calls[index] = envelope.ok
263
263
  ? {
@@ -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,35 @@ 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
+
14
+ ## [1.23.0] — 2026-09-23
15
+
16
+ ### Added
17
+
18
+ - `/multi-account pick` offers only the current account's models without filtering the shared registry or changing Pi core. `/multi-account save-default` saves the current model and effective thinking level together for new sessions, without making automatic failover overwrite startup preferences.
19
+ - Added `resumeAfterAllAccountsRecover` (default `true`) as a separate live-session liveness control. A real quota/rate-limit wall across every compatible account remains armed even when immediate `autoContinue` is disabled, polls recovery independently of footer visibility, and resumes on the first account that is genuinely usable.
20
+
21
+ ### Compatibility
22
+
23
+ - Pi and pi-ai are peer dependencies in the tested range >=0.85.1, <0.88.0. Transport imports bind to the running host rather than an extension-local 0.85 copy. CI covers Pi 0.85.1, 0.86.1 and 0.87.1; standalone SDK callers must use their installed pi-ai context format.
24
+
25
+ ### Fixed
26
+
27
+ - Native provider wrappers preserve system instructions and tool definitions on Pi's transcript-based context, avoiding tool-free Anthropic requests and old token-estimator crashes (#65, #66).
28
+ - Partial Codex usage headers preserve account/plan display metadata for the same credential without refreshing old serviceability, credits or missing quota windows. Includes a routing regression for stale `serviceable: true` plus fresh 100% quota (adapted from #67, thanks @HerbertGao).
29
+ - Kimi OAuth and unauthenticated spare slots remain in the login picker but are no longer published as unusable static aliases. Only authenticated API-key aliases are published; existing user models.json entries remain untouched (#68).
30
+ - Updated the Anthropic billing-header Claude Code version to 2.1.280 (#64).
31
+ - A continuation that exhausted its newly selected fallback no longer stops after one hop: the next quota-driven switch receives its own continuation.
32
+ - Asynchronous rejection of Pi's injected follow-up no longer loses the interrupted task; the selected fallback remains armed for a bounded retry.
33
+ - `neverFailoverProviders` now also bypasses foreground startup/input preflights for unmanaged providers. Stale cooldowns, invalidations, or unknown auth no longer silently replace an opted-out route before its request. Automatic switch and pending-resume boundaries enforce the same exemption; explicit manual switches remain available.
34
+ - A bodyless `429 status code (no body)` from an unmanaged provider now retries the same route instead of being treated as provider-wide credit exhaustion. The retry honors `Retry-After` when available and otherwise uses the transient delay, avoiding both an unrelated-provider failover and a false six-hour bench for providers such as Cerebras.
35
+ - Numbered Codex OAuth slots no longer fall back to the public ChatGPT endpoint while the child proxy is enabled. A numbered alias is always registered against the loopback route that swaps in the real credential, from extension load onward, so the child-facing placeholder can never be sent to a provider that cannot read it (`Could not parse your authentication token`). Registration now requires an explicit route, so a future call site cannot silently reintroduce the public fallback.
36
+
8
37
  ## [1.22.0] — 2026-09-17
9
38
 
10
39
  ### Compatibility