@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.
- package/README.md +4 -4
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/node_modules/@herbertgao/pi-bark/package.json +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +4 -4
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +14 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +4 -4
- package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +5 -4
- package/node_modules/@herbertgao/pi-subagents/src/mention-clone.ts +39 -16
- package/node_modules/@herbertgao/resume-from/package.json +2 -2
- package/node_modules/@herbertgao/sol-pi/package.json +5 -5
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +2 -0
- package/node_modules/@narumitw/pi-btw/README.md +21 -4
- package/node_modules/@narumitw/pi-btw/dist/index.ts +1668 -958
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
- package/node_modules/@narumitw/pi-btw/docs/workflows.md +10 -2
- package/node_modules/@narumitw/pi-btw/package.json +1 -1
- package/node_modules/@narumitw/pi-btw/src/btw.ts +17 -79
- package/node_modules/@narumitw/pi-btw/src/conversation-context.ts +74 -0
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +196 -7
- package/node_modules/@narumitw/pi-btw/src/main-thread-updates.ts +40 -0
- package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -2
- package/node_modules/@narumitw/pi-btw/src/settings.ts +49 -0
- package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +9 -1
- package/node_modules/@narumitw/pi-btw/src/workspace-layout.ts +559 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +26 -0
- package/node_modules/pi-mcp-adapter/README.md +39 -9
- package/node_modules/pi-mcp-adapter/cli.js +33 -22
- package/node_modules/pi-mcp-adapter/commands.ts +3 -3
- package/node_modules/pi-mcp-adapter/config.ts +9 -1
- package/node_modules/pi-mcp-adapter/dist/config.js +9 -1
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/jev-client.d.ts +1 -2
- package/node_modules/pi-mcp-adapter/dist/jev-client.js +59 -32
- package/node_modules/pi-mcp-adapter/dist/jev-client.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/jev-contracts.d.ts +1 -1
- package/node_modules/pi-mcp-adapter/dist/jev-key-store.d.ts +33 -4
- package/node_modules/pi-mcp-adapter/dist/jev-key-store.js +148 -28
- package/node_modules/pi-mcp-adapter/dist/jev-key-store.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js +1 -2
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +7 -1
- package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
- package/node_modules/pi-mcp-adapter/index.ts +51 -38
- package/node_modules/pi-mcp-adapter/jev-client.ts +58 -31
- package/node_modules/pi-mcp-adapter/jev-contracts.ts +2 -2
- package/node_modules/pi-mcp-adapter/jev-key-store.ts +144 -31
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
- package/node_modules/pi-mcp-adapter/mcp-code.ts +6 -6
- package/node_modules/pi-mcp-adapter/package.json +5 -5
- package/node_modules/pi-mcp-adapter/semantic-search.ts +3 -3
- package/node_modules/pi-mcp-adapter/types.ts +7 -1
- package/node_modules/pi-multi-account/CHANGELOG.md +29 -0
- package/node_modules/pi-multi-account/README.md +39 -11
- package/node_modules/pi-multi-account/index.ts +371 -96
- package/node_modules/pi-multi-account/package.json +5 -4
- package/node_modules/pi-multi-account/provider-payload-stream.ts +36 -28
- package/node_modules/pi-multi-account/usage.ts +31 -2
- package/node_modules/pi-typesafe/README.md +3 -1
- package/node_modules/pi-typesafe/dist/auth.d.ts +10 -3
- package/node_modules/pi-typesafe/dist/auth.js +13 -7
- package/node_modules/pi-typesafe/dist/backends.d.ts +41 -0
- package/node_modules/pi-typesafe/dist/backends.js +59 -0
- package/node_modules/pi-typesafe/dist/client.d.ts +4 -10
- package/node_modules/pi-typesafe/dist/client.js +72 -44
- package/node_modules/pi-typesafe/dist/credentials.d.ts +11 -5
- package/node_modules/pi-typesafe/dist/credentials.js +15 -8
- package/node_modules/pi-typesafe/dist/errors.d.ts +10 -2
- package/node_modules/pi-typesafe/dist/errors.js +28 -7
- package/node_modules/pi-typesafe/dist/extension.js +6 -2
- package/node_modules/pi-typesafe/dist/index.d.ts +1 -1
- package/node_modules/pi-typesafe/dist/index.js +1 -1
- package/node_modules/pi-typesafe/dist/login.d.ts +13 -7
- package/node_modules/pi-typesafe/dist/login.js +17 -8
- package/node_modules/pi-typesafe/dist/schema.js +19 -13
- package/node_modules/pi-typesafe/package.json +4 -4
- package/node_modules/pi-web-access/CHANGELOG.md +22 -0
- package/node_modules/pi-web-access/README.md +21 -8
- package/node_modules/pi-web-access/baizhi.ts +124 -0
- package/node_modules/pi-web-access/crawl4ai.ts +2 -10
- package/node_modules/pi-web-access/curator-page.ts +3 -1
- package/node_modules/pi-web-access/curator-server.ts +1 -0
- package/node_modules/pi-web-access/dist/index.js +540 -220
- package/node_modules/pi-web-access/firecrawl.ts +2 -10
- package/node_modules/pi-web-access/gemini-search.ts +5 -10
- package/node_modules/pi-web-access/index.ts +134 -23
- package/node_modules/pi-web-access/package.json +5 -1
- package/node_modules/pi-web-access/storage.ts +1 -0
- package/node_modules/pi-web-access/tool-activation.ts +134 -0
- package/node_modules/pi-web-access/utils.ts +9 -1
- 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,
|
|
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,
|
|
23
|
+
const credentials = new WeakMap<McpExtensionState, CredentialCacheEntry>();
|
|
24
24
|
|
|
25
|
-
|
|
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() :
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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: "
|
|
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:
|
|
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: "
|
|
239
|
-
if (signal?.aborted || (error instanceof Error && error.name === "APIUserAbortError")) return { ok: false, error: { code: "aborted", message: "
|
|
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: "
|
|
242
|
-
if (status ===
|
|
243
|
-
|
|
244
|
-
if (
|
|
245
|
-
if (status ===
|
|
246
|
-
if (
|
|
247
|
-
return { ok: false, error: { code: "
|
|
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
|
|
255
|
-
if ((options.purpose === "script" && !settings.scriptEvaluation) || (options.purpose === "semantic-search" && !settings.semanticSearch)) return { ok: false, error: { code: "disabled", message: "
|
|
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: "
|
|
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("
|
|
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: "
|
|
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
|
-
|
|
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:
|
|
9
|
-
|
|
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(`
|
|
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("
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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.
|
|
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(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
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
|
|
72
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
}
|
|
@@ -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: "
|
|
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: "
|
|
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
|
|
235
|
-
if (serialized === undefined) return { ok: false, error: { code: "invalid_request", message: "Invalid
|
|
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: "
|
|
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: "
|
|
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.
|
|
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.
|
|
206
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
207
|
-
"@earendil-works/pi-tui": "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
|
|
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: "
|
|
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: "
|
|
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
|
-
/**
|
|
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
|